diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index 5f30a89b7e5..db8d9c28949 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -147,6 +147,8 @@ public class Constants { public static final String TAG_SUBSETTED_SYSTEM = "http://hl7.org/fhir/v3/ObservationValue"; public static final String URL_TOKEN_HISTORY = "_history"; public static final String URL_TOKEN_METADATA = "metadata"; + public static final String PARAM_CONTENT = "_content"; + public static final String PARAM_TEXT = "_text"; static { Map valToEncoding = new HashMap(); diff --git a/hapi-fhir-jpaserver-base/.gitignore b/hapi-fhir-jpaserver-base/.gitignore index eaa09ffd910..4532b9d8844 100644 --- a/hapi-fhir-jpaserver-base/.gitignore +++ b/hapi-fhir-jpaserver-base/.gitignore @@ -1,6 +1,7 @@ target/ /bin nohup.out +lucene_indexes/ # Created by https://www.gitignore.io diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.cfe deleted file mode 100644 index cfffd3c842f..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.cfs deleted file mode 100644 index 1dc99d53197..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.si deleted file mode 100644 index e5669e5ee06..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_0.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.cfe deleted file mode 100644 index ed29c3e23b6..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.cfs deleted file mode 100644 index 93250da550b..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.si deleted file mode 100644 index 60c08eb6ba0..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_1.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.cfe deleted file mode 100644 index 82e3e0d7051..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.cfs deleted file mode 100644 index ce2f91e8074..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.si deleted file mode 100644 index ac42b2c309c..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_2.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.cfe deleted file mode 100644 index f345407f8fe..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.cfs deleted file mode 100644 index 73b409208e5..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.si deleted file mode 100644 index 4d622db6803..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/_3.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/segments_5 b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/segments_5 deleted file mode 100644 index 1bce37ddb62..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString/segments_5 and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.cfe deleted file mode 100644 index 60a9401621c..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.cfs deleted file mode 100644 index a26fbaa7377..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.si deleted file mode 100644 index e5f8ebbc472..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_0.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.cfe deleted file mode 100644 index 9dc809bc4b8..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.cfs deleted file mode 100644 index 813783af8b9..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.si deleted file mode 100644 index 5da78ffc630..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_1.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.cfe deleted file mode 100644 index 91ea7c3833b..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.cfs deleted file mode 100644 index 9e373e400ae..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.si deleted file mode 100644 index a693e5df988..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_2.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.cfe b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.cfe deleted file mode 100644 index 1eeedddb0d9..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.cfe and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.cfs b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.cfs deleted file mode 100644 index c9c1ee47e8e..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.cfs and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.si b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.si deleted file mode 100644 index 39d782d857d..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/_3.si and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/segments_5 b/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/segments_5 deleted file mode 100644 index 7a0781a26f9..00000000000 Binary files a/hapi-fhir-jpaserver-base/lucene_indexes/ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken/segments_5 and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 90539b37746..0fa5f897246 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -45,6 +45,8 @@ import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.XMLEvent; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; @@ -250,8 +252,7 @@ public abstract class BaseHapiFhirDao implements IDao { try { resourceDefinition = getContext().getResourceDefinition(typeString); } catch (DataFormatException e) { - throw new InvalidRequestException( - "Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextValue.getReference().getValue()); + throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextValue.getReference().getValue()); } Class type = resourceDefinition.getImplementingClass(); @@ -287,8 +288,7 @@ public abstract class BaseHapiFhirDao implements IDao { } if (!typeString.equals(target.getResourceType())) { - throw new UnprocessableEntityException("Resource contains reference to " + nextValue.getReference().getValue() + " but resource with ID " + nextValue.getReference().getIdPart() - + " is actually of type " + target.getResourceType()); + throw new UnprocessableEntityException("Resource contains reference to " + nextValue.getReference().getValue() + " but resource with ID " + nextValue.getReference().getIdPart() + " is actually of type " + target.getResourceType()); } /* @@ -716,9 +716,11 @@ public abstract class BaseHapiFhirDao implements IDao { } /** - * This method is called when an update to an existing resource detects that the resource supplied for update is missing a tag/profile/security label that the currently persisted resource holds. + * This method is called when an update to an existing resource detects that the resource supplied for update is + * missing a tag/profile/security label that the currently persisted resource holds. *

- * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour. + * The default implementation removes any profile declarations, but leaves tags and security labels in place. + * Subclasses may choose to override and change this behaviour. *

* * @param theEntity @@ -726,7 +728,8 @@ public abstract class BaseHapiFhirDao implements IDao { * @param theTag * The tag * @return Retturns true if the tag should be removed - * @see Updates to Tags, Profiles, and Security Labels for a description of the logic that the default behaviour folows. + * @see Updates to Tags, Profiles, and Security + * Labels for a description of the logic that the default behaviour folows. */ protected boolean shouldDroppedTagBeRemovedOnUpdate(ResourceTable theEntity, ResourceTag theTag) { if (theTag.getTag().getTagType() == TagTypeEnum.PROFILE) { @@ -763,13 +766,13 @@ public abstract class BaseHapiFhirDao implements IDao { RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType); SearchParameterMap paramMap = translateMatchUrl(theMatchUrl, resourceDef); - + if (paramMap.isEmpty()) { throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters"); } IFhirResourceDao dao = getDao(theResourceType); - Set ids = dao.searchForIdsWithAndOr(paramMap); + Set ids = dao.searchForIdsWithAndOr(paramMap, new HashSet()); return ids; } @@ -845,7 +848,7 @@ public abstract class BaseHapiFhirDao implements IDao { if (RESOURCE_META_PARAMS.containsKey(nextParamName)) { if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) { - throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier()); + throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier()); } IQueryParameterAnd type = newInstanceAnd(nextParamName); type.setValuesAsQueryTokens((paramList)); @@ -857,7 +860,7 @@ public abstract class BaseHapiFhirDao implements IDao { if (paramDef == null) { throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); } - + IQueryParameterAnd param = MethodUtil.parseQueryParams(paramDef, nextParamName, paramList); paramMap.add(nextParamName, param); } @@ -1100,7 +1103,8 @@ public abstract class BaseHapiFhirDao implements IDao { } } else if (theForHistoryOperation) { /* - * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT. + * If the create and update times match, this was when the resource was created so we should mark it as a POST. + * Otherwise, it's a PUT. */ Date published = theEntity.getPublished().getValue(); Date updated = theEntity.getUpdated().getValue(); @@ -1194,8 +1198,7 @@ public abstract class BaseHapiFhirDao implements IDao { } @SuppressWarnings("unchecked") - protected ResourceTable updateEntity(final IResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, - boolean theUpdateVersion, Date theUpdateTime) { + protected ResourceTable updateEntity(final IResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion, Date theUpdateTime) { /* * This should be the very first thing.. @@ -1204,8 +1207,7 @@ public abstract class BaseHapiFhirDao implements IDao { validateResourceForStorage((T) theResource, theEntity); String resourceType = myContext.getResourceDefinition(theResource).getName(); if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) { - throw new UnprocessableEntityException( - "Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]"); + throw new UnprocessableEntityException("Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]"); } } @@ -1261,6 +1263,7 @@ public abstract class BaseHapiFhirDao implements IDao { links = Collections.emptySet(); theEntity.setDeleted(theDeletedTimestampOrNull); theEntity.setUpdated(theDeletedTimestampOrNull); + theEntity.setNarrativeTextParsedIntoWords(null); } else { @@ -1309,6 +1312,7 @@ public abstract class BaseHapiFhirDao implements IDao { theEntity.setResourceLinks(links); theEntity.setHasLinks(links.isEmpty() == false); theEntity.setIndexStatus(INDEX_STATUS_INDEXED); + theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource)); } else { @@ -1425,7 +1429,8 @@ public abstract class BaseHapiFhirDao implements IDao { } /** - * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the first time. + * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the + * first time. * * @param theEntity * The resource @@ -1437,7 +1442,8 @@ public abstract class BaseHapiFhirDao implements IDao { } /** - * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the first time. + * Subclasses may override to provide behaviour. Called when a resource has been inserved into the database for the + * first time. * * @param theEntity * The resource @@ -1449,8 +1455,9 @@ public abstract class BaseHapiFhirDao implements IDao { } /** - * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow the DAO to ensure that it is valid for persistence. By default, checks for the - * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check. + * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow + * the DAO to ensure that it is valid for persistence. By default, checks for the "subsetted" tag and rejects + * resources which have it. Subclasses should call the superclass implementation to preserve this check. * * @param theResource * The resource that is about to be persisted @@ -1481,4 +1488,18 @@ public abstract class BaseHapiFhirDao implements IDao { return new String(out).toUpperCase(); } + private static String parseNarrativeTextIntoWords(IResource theResource) { + StringBuilder b = new StringBuilder(); + List xmlEvents = theResource.getText().getDiv().getValue(); + if (xmlEvents != null) { + for (XMLEvent next : xmlEvents) { + if (next.isCharacters()) { + Characters characters = next.asCharacters(); + b.append(characters.getData()).append(" "); + } + } + } + return b.toString(); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 3cc97b102b8..ab66be400a5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -158,6 +158,9 @@ public abstract class BaseHapiFhirResourceDao extends BaseH @Autowired private DaoConfig myDaoConfig; + @Autowired(required=false) + private ISearchDao mySearchDao; + private String myResourceName; private Class myResourceType; private String mySecondaryPrimaryKeyParamName; @@ -2147,7 +2150,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH StopWatch w = new StopWatch(); final InstantDt now = InstantDt.withCurrentTime(); - Set loadPids; + Collection loadPids; if (theParams.getEverythingMode() != null) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); @@ -2185,12 +2188,24 @@ public abstract class BaseHapiFhirResourceDao extends BaseH if (loadPids.isEmpty()) { return new SimpleBundleProvider(); } - } else { - loadPids = searchForIdsWithAndOr(theParams); + } else { + + List searchResultPids; + if (mySearchDao == null) { + searchResultPids = null; + } else { + searchResultPids = mySearchDao.search(getResourceName(), theParams); + } + if (theParams.isEmpty()) { + loadPids = searchResultPids; + } else { + loadPids = searchForIdsWithAndOr(theParams, searchResultPids); + } if (loadPids.isEmpty()) { return new SimpleBundleProvider(); } + } // // Load _include and _revinclude before filter and sort in everything mode @@ -2279,15 +2294,15 @@ public abstract class BaseHapiFhirResourceDao extends BaseH return retVal; } - private List filterResourceIdsByLastUpdated(Set loadPids, final DateRangeParam lu) { + private List filterResourceIdsByLastUpdated(Collection thePids, final DateRangeParam theLastUpdated) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery cq = builder.createQuery(Long.class); Root from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); - Predicate predicateIds = (from.get("myId").in(loadPids)); - Predicate predicateLower = lu.getLowerBoundAsInstant() != null ? builder.greaterThanOrEqualTo(from. get("myUpdated"), lu.getLowerBoundAsInstant()) : null; - Predicate predicateUpper = lu.getUpperBoundAsInstant() != null ? builder.lessThanOrEqualTo(from. get("myUpdated"), lu.getUpperBoundAsInstant()) : null; + Predicate predicateIds = (from.get("myId").in(thePids)); + Predicate predicateLower = theLastUpdated.getLowerBoundAsInstant() != null ? builder.greaterThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getLowerBoundAsInstant()) : null; + Predicate predicateUpper = theLastUpdated.getUpperBoundAsInstant() != null ? builder.lessThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getUpperBoundAsInstant()) : null; if (predicateLower != null && predicateUpper != null) { cq.where(predicateIds, predicateLower, predicateUpper); } else if (predicateLower != null) { @@ -2313,20 +2328,20 @@ public abstract class BaseHapiFhirResourceDao extends BaseH return query; } - private List processSort(final SearchParameterMap theParams, Set theLoadPids) { + private List processSort(final SearchParameterMap theParams, Collection theLoadPids) { final List pids; - Set loadPids = theLoadPids; +// Set loadPids = theLoadPids; if (theParams.getSort() != null && isNotBlank(theParams.getSort().getParamName())) { List orders = new ArrayList(); List predicates = new ArrayList(); CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery cq = builder.createTupleQuery(); Root from = cq.from(ResourceTable.class); - predicates.add(from.get("myId").in(loadPids)); + predicates.add(from.get("myId").in(theLoadPids)); createSort(builder, from, theParams.getSort(), orders, predicates); if (orders.size() > 0) { - Set originalPids = loadPids; - loadPids = new LinkedHashSet(); + Collection originalPids = theLoadPids; + LinkedHashSet loadPids = new LinkedHashSet(); cq.multiselect(from.get("myId").as(Long.class)); cq.where(predicates.toArray(new Predicate[0])); cq.orderBy(orders); @@ -2349,10 +2364,20 @@ public abstract class BaseHapiFhirResourceDao extends BaseH } } else { - pids = new ArrayList(loadPids); + pids = toList(theLoadPids); } } else { - pids = new ArrayList(loadPids); + pids = toList(theLoadPids); + } + return pids; + } + + private List toList(Collection theLoadPids) { + final List pids; + if (theLoadPids instanceof List) { + pids = (List) theLoadPids; + } else { + pids = new ArrayList(theLoadPids); } return pids; } @@ -2368,7 +2393,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH for (Entry nextEntry : theParams.entrySet()) { map.add(nextEntry.getKey(), (nextEntry.getValue())); } - return searchForIdsWithAndOr(map); + return searchForIdsWithAndOr(map, null); } @Override @@ -2377,7 +2402,7 @@ public abstract class BaseHapiFhirResourceDao extends BaseH } @Override - public Set searchForIdsWithAndOr(SearchParameterMap theParams) { + public Set searchForIdsWithAndOr(SearchParameterMap theParams, Collection theInitialPids) { SearchParameterMap params = theParams; if (params == null) { params = new SearchParameterMap(); @@ -2386,6 +2411,9 @@ public abstract class BaseHapiFhirResourceDao extends BaseH RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceType); Set pids = new HashSet(); + if (theInitialPids != null) { + pids.addAll(theInitialPids); + } for (Entry>> nextParamEntry : params.entrySet()) { String nextParamName = nextParamEntry.getKey(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSearchDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSearchDao.java new file mode 100644 index 00000000000..eb60e1dec87 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSearchDao.java @@ -0,0 +1,98 @@ +package ca.uhn.fhir.jpa.dao; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; + +import org.apache.commons.lang3.StringUtils; +import org.apache.lucene.search.Query; +import org.hibernate.search.jpa.FullTextEntityManager; +import org.hibernate.search.jpa.FullTextQuery; +import org.hibernate.search.query.dsl.BooleanJunction; +import org.hibernate.search.query.dsl.QueryBuilder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.transaction.annotation.Transactional; + +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.Constants; + +public class FhirSearchDao extends BaseHapiFhirDao implements ISearchDao { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSearchDao.class); + + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + private EntityManager myEntityManager; + + @Transactional() + @Override + public List search(String theResourceName, SearchParameterMap theParams) { + FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); + + QueryBuilder qb = em + .getSearchFactory() + .buildQueryBuilder() + .forEntity(ResourceTable.class).get(); + + BooleanJunction bool = qb.bool(); + + List> contentAndTerms = theParams.remove(Constants.PARAM_CONTENT); + addTextSearch(qb, bool, contentAndTerms, "myParamsString.myValueComplete"); + + List> textAndTerms = theParams.remove(Constants.PARAM_TEXT); + addTextSearch(qb, bool, textAndTerms, "myNarrativeText"); + + if (bool.isEmpty()) { + return null; + } + + if (isNotBlank(theResourceName)) { + bool.must(qb.keyword().onField("myResourceType").matching(theResourceName).createQuery()); + } + + Query luceneQuery = bool.createQuery(); + + // wrap Lucene query in a javax.persistence.Query + FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, ResourceTable.class); + jpaQuery.setProjection("myId"); + + // execute search + List result = jpaQuery.getResultList(); + + ArrayList retVal = new ArrayList(); + for (Object object : result) { + Object[] nextArray = (Object[]) object; + retVal.add((Long)nextArray[0]); + } + + return retVal; + } + + private void addTextSearch(QueryBuilder qb, BooleanJunction bool, List> contentAndTerms, String field) { + if (contentAndTerms == null) { + return; + } + for (List nextAnd : contentAndTerms) { + Set terms = new HashSet(); + for (IQueryParameterType nextOr : nextAnd) { + StringParam nextOrString = (StringParam) nextOr; + String nextValueTrimmed = StringUtils.defaultString(nextOrString.getValue()).trim(); + if (isNotBlank(nextValueTrimmed)) { + terms.add(nextValueTrimmed); + } + } + if (terms.isEmpty() == false) { + String joinedTerms = StringUtils.join(terms, ' '); + bool.must(qb.keyword().onField(field).matching(joinedTerms).createQuery()); + } + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDao.java index afff97a4728..268bb99c023 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDao.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.jpa.dao; +import java.util.Collection; + /* * #%L * HAPI FHIR JPA Server @@ -121,7 +123,7 @@ public interface IFhirResourceDao extends IDao { Set searchForIds(String theParameterName, IQueryParameterType theValue); - Set searchForIdsWithAndOr(SearchParameterMap theParams); + Set searchForIdsWithAndOr(SearchParameterMap theParams, Collection theInitialPids); DaoMethodOutcome update(T theResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchDao.java index a88683d4b8f..f273037aadb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchDao.java @@ -1,11 +1,9 @@ package ca.uhn.fhir.jpa.dao; -import ca.uhn.fhir.rest.server.IBundleProvider; +import java.util.List; public interface ISearchDao { - public static final String FULL_TEXT_PARAM_NAME = "fullTextSearch"; - - IBundleProvider search(SearchParameterMap theParams); + List search(String theResourceName, SearchParameterMap theParams); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchDao.java deleted file mode 100644 index bca666da509..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchDao.java +++ /dev/null @@ -1,53 +0,0 @@ -package ca.uhn.fhir.jpa.dao; - -import java.util.List; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.PersistenceContextType; - -import org.hibernate.search.jpa.FullTextEntityManager; -import org.hibernate.search.jpa.FullTextQuery; -import org.hibernate.search.query.dsl.QueryBuilder; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.springframework.transaction.annotation.Transactional; - -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.rest.server.IBundleProvider; - -public class SearchDao extends BaseHapiFhirDao implements ISearchDao { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchDao.class); - - @PersistenceContext(type = PersistenceContextType.TRANSACTION) - private EntityManager myEntityManager; - - @Transactional() - @Override - public IBundleProvider search(SearchParameterMap theParams) { - - FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); - - for (String nextParamName : theParams.keySet()) { - if (nextParamName.equals(FULL_TEXT_PARAM_NAME)) { - QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(ResourceIndexedSearchParamString.class).get(); - org.apache.lucene.search.Query luceneQuery = qb - .keyword() - .onFields("myValueComplete") - .matching("AAAS") - .createQuery(); - - // wrap Lucene query in a javax.persistence.Query - FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, ResourceIndexedSearchParamString.class); - - // execute search - List result = jpaQuery.getResultList(); - for (Object object : result) { - ourLog.info(""+ object); - } - } - } - - return null; - } - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java index a8f5cb7a7bf..b1ae0695232 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java @@ -32,7 +32,6 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; //@formatter:off -@Indexed @Entity @Table(name = "HFJ_SPIDX_COORDS" /* , indexes = { @Index(name = "IDX_SP_TOKEN", columnList = "SP_SYSTEM,SP_VALUE") } */) @org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_COORDS", indexes = { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java index 76d40772872..62b372fc60f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java @@ -36,7 +36,6 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; //@formatter:off -@Indexed @Entity @Table(name = "HFJ_SPIDX_DATE" /*, indexes= {@Index(name="IDX_SP_DATE", columnList= "SP_VALUE_LOW,SP_VALUE_HIGH")}*/) @org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_DATE", indexes= { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java index acd946705d8..34d99264ce2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java @@ -31,10 +31,13 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; +import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Indexed; +import org.hibernate.search.annotations.NumericField; + +import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; //@formatter:off -@Indexed @Entity @Table(name = "HFJ_SPIDX_NUMBER" /*, indexes= {@Index(name="IDX_SP_NUMBER", columnList="SP_VALUE")}*/ ) @org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_NUMBER", indexes= { @@ -47,6 +50,8 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP @Column(name = "SP_VALUE", nullable = true) @Field + @NumericField + @FieldBridge(impl = BigDecimalNumericFieldBridge.class) public BigDecimal myValue; public ResourceIndexedSearchParamNumber() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java index b6bedef0aa1..5fcbfeb5f6c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java @@ -31,10 +31,13 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; +import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Indexed; +import org.hibernate.search.annotations.NumericField; + +import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; //@formatter:off -@Indexed @Entity @Table(name = "HFJ_SPIDX_QUANTITY" /*, indexes= {@Index(name="IDX_SP_NUMBER", columnList="SP_VALUE")}*/ ) @org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_QUANTITY", indexes= { @@ -57,6 +60,8 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc @Column(name = "SP_VALUE", nullable = true) @Field + @NumericField + @FieldBridge(impl = BigDecimalNumericFieldBridge.class) public BigDecimal myValue; public ResourceIndexedSearchParamQuantity() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java index d13aa1df534..6cb4406e1ff 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java @@ -32,7 +32,6 @@ import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; -@Indexed @Entity @Table(name = "HFJ_SPIDX_STRING"/* , indexes= {@Index(name="IDX_SP_STRING", columnList="SP_VALUE_NORMALIZED")} */) @org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_STRING", indexes = { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java index 750728e4341..79164dc37db 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java @@ -32,7 +32,6 @@ import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; -@Indexed @Entity @Table(name = "HFJ_SPIDX_TOKEN" /* , indexes = { @Index(name = "IDX_SP_TOKEN", columnList = "SP_SYSTEM,SP_VALUE") } */) @org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_TOKEN", indexes = { @org.hibernate.annotations.Index(name = "IDX_SP_TOKEN", columnNames = { "RES_TYPE", "SP_NAME", "SP_SYSTEM", "SP_VALUE" }), diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java index ed28bd2ad49..51b0409c681 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java @@ -33,7 +33,6 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; //@formatter:off -@Indexed @Entity @Table(name = "HFJ_SPIDX_URI" /* , indexes = { @Index(name = "IDX_SP_TOKEN", columnList = "SP_SYSTEM,SP_VALUE") } */) @org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_URI", indexes = { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java index 2286bca9b18..6e41ca931b1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java @@ -35,14 +35,21 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Index; +import javax.persistence.Lob; import javax.persistence.OneToMany; import javax.persistence.Table; +import org.hibernate.search.annotations.Field; +import org.hibernate.search.annotations.Indexed; +import org.hibernate.search.annotations.IndexedEmbedded; + +import ca.uhn.fhir.jpa.search.IndexNonDeletedInterceptor; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; //@formatter:off +@Indexed(interceptor=IndexNonDeletedInterceptor.class) @Entity @Table(name = "HFJ_RESOURCE", uniqueConstraints = {}, indexes= { @Index(name = "IDX_RES_DATE", columnList="RES_UPDATED"), @@ -76,43 +83,58 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Column(name = "RES_LANGUAGE", length = MAX_LANGUAGE_LENGTH, nullable = true) private String myLanguage; + /** + * Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB + */ + @Column(name = "SP_NARRATIVE_TEXT") + @Lob + @Field() + private String myNarrativeText; + @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @IndexedEmbedded private Collection myParamsCoords; @Column(name = "SP_COORDS_PRESENT") private boolean myParamsCoordsPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @IndexedEmbedded private Collection myParamsDate; @Column(name = "SP_DATE_PRESENT") private boolean myParamsDatePopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @IndexedEmbedded private Collection myParamsNumber; @Column(name = "SP_NUMBER_PRESENT") private boolean myParamsNumberPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @IndexedEmbedded private Collection myParamsQuantity; @Column(name = "SP_QUANTITY_PRESENT") private boolean myParamsQuantityPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @IndexedEmbedded private Collection myParamsString; @Column(name = "SP_STRING_PRESENT") private boolean myParamsStringPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @IndexedEmbedded private Collection myParamsToken; @Column(name = "SP_TOKEN_PRESENT") private boolean myParamsTokenPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @IndexedEmbedded private Collection myParamsUri; @Column(name = "SP_URI_PRESENT") @@ -125,6 +147,7 @@ public class ResourceTable extends BaseHasResource implements Serializable { private Collection myResourceLinks; @Column(name = "RES_TYPE", length = RESTYPE_LEN) + @Field private String myResourceType; @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) @@ -297,6 +320,10 @@ public class ResourceTable extends BaseHasResource implements Serializable { myLanguage = theLanguage; } + public void setNarrativeTextParsedIntoWords(String theNarrativeText) { + myNarrativeText = theNarrativeText; + } + public void setParamsCoords(Collection theParamsCoords) { if (!isParamsTokenPopulated() && theParamsCoords.isEmpty()) { return; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java new file mode 100644 index 00000000000..e708e6bca0d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.jpa.search; + +import org.hibernate.search.indexes.interceptor.EntityIndexingInterceptor; +import org.hibernate.search.indexes.interceptor.IndexingOverride; + +import ca.uhn.fhir.jpa.entity.ResourceTable; + +/** + * Only store non-deleted resources + */ +public class IndexNonDeletedInterceptor implements EntityIndexingInterceptor { + + @Override + public IndexingOverride onAdd(ResourceTable entity) { + if (entity.getDeleted() == null) { + return IndexingOverride.APPLY_DEFAULT; + } + return IndexingOverride.SKIP; + } + + @Override + public IndexingOverride onUpdate(ResourceTable entity) { + if (entity.getDeleted() == null) { + return IndexingOverride.UPDATE; + } + return IndexingOverride.REMOVE; + } + + @Override + public IndexingOverride onDelete(ResourceTable entity) { + return IndexingOverride.APPLY_DEFAULT; + } + + @Override + public IndexingOverride onCollectionUpdate(ResourceTable entity) { + return onUpdate(entity); + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/BigDecimalNumericFieldBridge.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/BigDecimalNumericFieldBridge.java new file mode 100644 index 00000000000..444383addd1 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/BigDecimalNumericFieldBridge.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.jpa.util; + +import java.math.BigDecimal; + +import org.apache.lucene.document.Document; +import org.apache.lucene.index.IndexableField; +import org.hibernate.search.bridge.LuceneOptions; +import org.hibernate.search.bridge.TwoWayFieldBridge; + +public class BigDecimalNumericFieldBridge implements TwoWayFieldBridge { + @Override + public void set(String name, Object value, Document document, LuceneOptions luceneOptions) { + if (value == null) { + if (luceneOptions.indexNullAs() != null) { + luceneOptions.addFieldToDocument(name, luceneOptions.indexNullAs(), document); + } + } else { + BigDecimal bdValue = (BigDecimal)value; + applyToLuceneOptions(luceneOptions, name, bdValue.doubleValue(), document); + } + } + + @Override + public final String objectToString(final Object object) { + return object == null ? null : object.toString(); + } + + @Override + public Object get(final String name, final Document document) { + final IndexableField field = document.getField(name); + if (field != null) { + Double doubleVal = (Double)field.numericValue(); + return new BigDecimal(doubleVal); + } else { + return null; + } + } + + protected void applyToLuceneOptions(LuceneOptions luceneOptions, String name, Number value, Document document) { + luceneOptions.addNumericFieldToDocument(name, value, document); + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/main/resources/fhir-spring-search-config-dstu2.xml b/hapi-fhir-jpaserver-base/src/main/resources/fhir-spring-search-config-dstu2.xml index 43ee2ce9434..2e9740395a7 100644 --- a/hapi-fhir-jpaserver-base/src/main/resources/fhir-spring-search-config-dstu2.xml +++ b/hapi-fhir-jpaserver-base/src/main/resources/fhir-spring-search-config-dstu2.xml @@ -16,6 +16,6 @@ - + \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java index 273b44164e0..4c64baa7f29 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2Test.java @@ -168,7 +168,7 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { } protected T loadResourceFromClasspath(Class type, String resourceName) throws IOException { - InputStream stream = FhirResourceDaoDstu2SearchTest.class.getResourceAsStream(resourceName); + InputStream stream = FhirResourceDaoDstu2SearchNoFtTest.class.getResourceAsStream(resourceName); if (stream == null) { fail("Unable to load resource: " + resourceName); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchFtTest.java similarity index 99% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchFtTest.java index b2a65417e20..083835f0439 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchFtTest.java @@ -94,8 +94,8 @@ import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @SuppressWarnings("unchecked") -public class FhirResourceDaoDstu2SearchTest extends BaseJpaDstu2Test { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2SearchTest.class); +public class FhirResourceDaoDstu2SearchFtTest extends BaseJpaDstu2Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2SearchFtTest.class); @Test public void testSearchWithEmptySort() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchNoFtTest.java new file mode 100644 index 00000000000..5f327bfcbc2 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2SearchNoFtTest.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.jpa.dao; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.Test; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.server.Constants; + +@SuppressWarnings("unchecked") +public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2SearchNoFtTest.class); + + @Test + public void testSearchWithChainedParams() { + String methodName = "testSearchWithChainedParams"; + IIdType pId1; + { + Patient patient = new Patient(); + patient.addName().addGiven("methodName"); + patient.addAddress().addLine("My fulltext address"); + pId1 = myPatientDao.create(patient).getId(); + } + + Observation obs = new Observation(); + obs.getSubject().setReference(pId1); + obs.setValue(new StringDt("This is the fulltext of the observation")); + IIdType oId1 = myObservationDao.create(obs).getId().toUnqualifiedVersionless(); + + obs = new Observation(); + obs.getSubject().setReference(pId1); + obs.setValue(new StringDt("Another fulltext")); + IIdType oId2 = myObservationDao.create(obs).getId().toUnqualifiedVersionless(); + + SearchParameterMap params = new SearchParameterMap(); + params.add(Constants.PARAM_CONTENT, new StringDt("fulltext")); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, containsInAnyOrder(oId1, oId2)); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSearchDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSearchDaoDstu2Test.java index 3cf2db6394d..55d9975b61d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSearchDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirSearchDaoDstu2Test.java @@ -1,13 +1,26 @@ package ca.uhn.fhir.jpa.dao; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; + +import java.util.List; + +import org.hibernate.search.jpa.FullTextEntityManager; +import org.hibernate.search.jpa.Search; +import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.model.dstu2.resource.Organization; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.rest.param.StringAndListParam; import ca.uhn.fhir.rest.param.StringOrListParam; import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.Constants; @ContextConfiguration(locations = { "classpath:fhir-spring-search-config-dstu2.xml" }) public class FhirSearchDaoDstu2Test extends BaseJpaDstu2Test { @@ -15,25 +28,159 @@ public class FhirSearchDaoDstu2Test extends BaseJpaDstu2Test { @Autowired private ISearchDao mySearchDao; + @Before + @Transactional + public void beforeFlushFT() { + FullTextEntityManager ftem = Search.getFullTextEntityManager(myEntityManager); + ftem.purgeAll(ResourceTable.class); + ftem.flushToIndexes(); + } + @Test - public void testStringSearch() { + public void testContentSearch() { + Long id1; { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("001"); patient.addName().addGiven("testSearchStringParamWithNonNormalized_h\u00F6ra"); patient.addName().addFamily("AAAS"); - myPatientDao.create(patient); + patient.addName().addFamily("CCC"); + id1 = myPatientDao.create(patient).getId().toUnqualifiedVersionless().getIdPartAsLong(); } + Long id2; { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("002"); patient.addName().addGiven("testSearchStringParamWithNonNormalized_HORA"); - myPatientDao.create(patient); + patient.addName().addFamily("AAAB"); + patient.addName().addFamily("CCC"); + id2 = myPatientDao.create(patient).getId().toUnqualifiedVersionless().getIdPartAsLong(); + } + Long id3; + { + Organization org = new Organization(); + org.setName("DDD"); + id3 = myOrganizationDao.create(org).getId().toUnqualifiedVersionless().getIdPartAsLong(); } SearchParameterMap map = new SearchParameterMap(); - map.add(ISearchDao.FULL_TEXT_PARAM_NAME, new StringAndListParam().addAnd(new StringOrListParam().addOr(new StringParam("AAA")))); - mySearchDao.search(map); + String resourceName = "Patient"; + + // One term + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAS"))); + + map.add(Constants.PARAM_CONTENT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1)); + } + // OR + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAS")).addOr(new StringParam("AAAB"))); + + map.add(Constants.PARAM_CONTENT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1, id2)); + } + // AND + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAS"))); + content.addAnd(new StringOrListParam().addOr(new StringParam("CCC"))); + + map.add(Constants.PARAM_CONTENT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1)); + } + // AND OR + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAB")).addOr(new StringParam("AAAS"))); + content.addAnd(new StringOrListParam().addOr(new StringParam("CCC"))); + + map.add(Constants.PARAM_CONTENT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1, id2)); + } + // All Resource Types + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("CCC")).addOr(new StringParam("DDD"))); + + map.add(Constants.PARAM_CONTENT, content); + List found = mySearchDao.search(null, map); + assertThat(found, containsInAnyOrder(id1, id2, id3)); + } + } + @Test + public void testNarrativeSearch() { + Long id1; + { + Patient patient = new Patient(); + patient.getText().setDiv("
AAAS

FOO

CCC
"); + id1 = myPatientDao.create(patient).getId().toUnqualifiedVersionless().getIdPartAsLong(); + } + Long id2; + { + Patient patient = new Patient(); + patient.getText().setDiv("
AAAB

FOO

CCC
"); + id2 = myPatientDao.create(patient).getId().toUnqualifiedVersionless().getIdPartAsLong(); + } + + SearchParameterMap map = new SearchParameterMap(); + String resourceName = "Patient"; + + // One term + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAS"))); + + map.add(Constants.PARAM_TEXT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1)); + } + // OR + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAS")).addOr(new StringParam("AAAB"))); + + map.add(Constants.PARAM_TEXT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1, id2)); + } + // AND + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAS"))); + content.addAnd(new StringOrListParam().addOr(new StringParam("CCC"))); + + map.add(Constants.PARAM_TEXT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1)); + } + // AND OR + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("AAAB")).addOr(new StringParam("AAAS"))); + content.addAnd(new StringOrListParam().addOr(new StringParam("CCC"))); + + map.add(Constants.PARAM_TEXT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, containsInAnyOrder(id1, id2)); + } + // Tag Contents + { + StringAndListParam content = new StringAndListParam(); + content.addAnd(new StringOrListParam().addOr(new StringParam("div"))); + + map.add(Constants.PARAM_TEXT, content); + List found = mySearchDao.search(resourceName, map); + assertThat(found, empty()); + } + } + } diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm index 968cbab880a..7836f855a3d 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm @@ -50,6 +50,14 @@ public class ${className}ResourceProvider extends @OptionalParam(name="_language") StringAndListParam theResourceLanguage, + @Description(shortDefinition="Search the contents of the resource's data using a fulltext search") + @OptionalParam(name=ca.uhn.fhir.rest.server.Constants.PARAM_CONTENT) + StringAndListParam theFtContent, + + @Description(shortDefinition="Search the contents of the resource's narrative using a fulltext search") + @OptionalParam(name=ca.uhn.fhir.rest.server.Constants.PARAM_TEXT) + StringAndListParam theFtText, + @Description(shortDefinition="Search for resources which have the given tag") @OptionalParam(name=ca.uhn.fhir.rest.server.Constants.PARAM_TAG) TokenAndListParam theSearchForTag, @@ -128,6 +136,8 @@ public class ${className}ResourceProvider extends SearchParameterMap paramMap = new SearchParameterMap(); paramMap.add("_id", theId); paramMap.add("_language", theResourceLanguage); + paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_CONTENT, theFtContent); + paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_TEXT, theFtText); paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_TAG, theSearchForTag); paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_SECURITY, theSearchForSecurity); paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_PROFILE, theSearchForProfile);