diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index e190a60ed4d..d3df0dd67e2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.context; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.trim; import java.util.*; @@ -50,7 +51,12 @@ public class RuntimeSearchParam { } public RuntimeSearchParam(IIdType theId, String theUri, String theName, String theDescription, String thePath, RestSearchParameterTypeEnum theParamType, List theCompositeOf, - Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus) { + Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus) { + this(theId, theUri, theName, theDescription, thePath, theParamType, theCompositeOf, theProvidesMembershipInCompartments, theTargets, theStatus, null); + } + + public RuntimeSearchParam(IIdType theId, String theUri, String theName, String theDescription, String thePath, RestSearchParameterTypeEnum theParamType, List theCompositeOf, + Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus, Collection theBase) { super(); myId = theId; myUri = theUri; @@ -70,13 +76,19 @@ public class RuntimeSearchParam { } else { myTargets = null; } - - HashSet base = new HashSet(); - int indexOf = thePath.indexOf('.'); - if (indexOf != -1) { - base.add(trim(thePath.substring(0, indexOf))); + + if (theBase == null || theBase.isEmpty()) { + HashSet base = new HashSet<>(); + if (isNotBlank(thePath)) { + int indexOf = thePath.indexOf('.'); + if (indexOf != -1) { + base.add(trim(thePath.substring(0, indexOf))); + } + } + myBase = Collections.unmodifiableSet(base); + } else { + myBase = Collections.unmodifiableSet(new HashSet<>(theBase)); } - myBase = Collections.unmodifiableSet(base); } public Set getBase() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 90414d1b0f0..d95e7c77a92 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -33,6 +33,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.env.Environment; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.hibernate5.HibernateExceptionTranslator; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.SchedulingConfigurer; 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 28686bbadb1..102d51aa3f5 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 @@ -19,19 +19,49 @@ package ca.uhn.fhir.jpa.dao; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.*; -import java.io.UnsupportedEncodingException; -import java.text.Normalizer; -import java.util.*; -import java.util.Map.Entry; - -import javax.persistence.*; -import javax.persistence.criteria.*; -import javax.xml.stream.events.Characters; -import javax.xml.stream.events.XMLEvent; - -import org.apache.commons.lang3.*; +import ca.uhn.fhir.context.*; +import ca.uhn.fhir.jpa.dao.data.*; +import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; +import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; +import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; +import ca.uhn.fhir.jpa.util.DeleteConflict; +import ca.uhn.fhir.model.api.*; +import ca.uhn.fhir.model.base.composite.BaseCodingDt; +import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.model.primitive.XhtmlDt; +import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.parser.LenientErrorHandler; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.QualifiedParamList; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.CoverageIgnore; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.OperationOutcomeUtil; +import ca.uhn.fhir.util.UrlUtil; +import com.google.common.base.Charsets; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Sets; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.hl7.fhir.instance.model.api.*; @@ -40,49 +70,30 @@ import org.hl7.fhir.r4.model.Bundle.HTTPVerb; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; -import com.google.common.base.Charsets; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Sets; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; +import javax.persistence.*; +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 java.io.UnsupportedEncodingException; +import java.text.Normalizer; +import java.util.*; +import java.util.Map.Entry; -import ca.uhn.fhir.context.*; -import ca.uhn.fhir.jpa.dao.data.*; -import ca.uhn.fhir.jpa.entity.*; -import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; -import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; -import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; -import ca.uhn.fhir.jpa.util.DeleteConflict; -import ca.uhn.fhir.model.api.*; -import ca.uhn.fhir.model.base.composite.BaseCodingDt; -import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; -import ca.uhn.fhir.model.primitive.*; -import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; -import ca.uhn.fhir.parser.*; -import ca.uhn.fhir.rest.api.*; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.*; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.util.*; +import static org.apache.commons.lang3.StringUtils.*; public abstract class BaseHapiFhirDao implements IDao { - static final Set EXCLUDE_ELEMENTS_IN_ENCODED; - public static final long INDEX_STATUS_INDEXED = Long.valueOf(1L); public static final long INDEX_STATUS_INDEXING_FAILED = Long.valueOf(2L); public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile"; public static final String OO_SEVERITY_ERROR = "error"; public static final String OO_SEVERITY_INFO = "information"; public static final String OO_SEVERITY_WARN = "warning"; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); - private static final Map ourRetrievalContexts = new HashMap(); - private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; + public static final String UCUM_NS = "http://unitsofmeasure.org"; + static final Set EXCLUDE_ELEMENTS_IN_ENCODED; /** * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)} */ @@ -91,7 +102,9 @@ public abstract class BaseHapiFhirDao implements IDao { * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)} */ static final Map> RESOURCE_META_PARAMS; - public static final String UCUM_NS = "http://unitsofmeasure.org"; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); + private static final Map ourRetrievalContexts = new HashMap(); + private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; static { Map> resourceMetaParams = new HashMap>(); @@ -115,52 +128,47 @@ public abstract class BaseHapiFhirDao implements IDao { EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded); } - @Autowired(required = true) - private DaoConfig myConfig; - private FhirContext myContext; @PersistenceContext(type = PersistenceContextType.TRANSACTION) protected EntityManager myEntityManager; @Autowired protected IForcedIdDao myForcedIdDao; @Autowired(required = false) protected IFulltextSearchSvc myFulltextSearchSvc; - @Autowired - private PlatformTransactionManager myPlatformTransactionManager; - - @Autowired - private List> myResourceDaos; - - @Autowired - private IResourceHistoryTableDao myResourceHistoryTableDao; - @Autowired() protected IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; - - private Map, IFhirResourceDao> myResourceTypeToDao; - @Autowired protected ISearchCoordinatorSvc mySearchCoordinatorSvc; - - @Autowired - private ISearchDao mySearchDao; - - @Autowired - private ISearchParamExtractor mySearchParamExtractor; - - @Autowired - private ISearchParamPresenceSvc mySearchParamPresenceSvc; - - @Autowired - private ISearchParamRegistry mySearchParamRegistry; - - @Autowired - private ISearchResultDao mySearchResultDao; - @Autowired protected ISearchParamRegistry mySerarchParamRegistry; - @Autowired() protected IHapiTerminologySvc myTerminologySvc; + @Autowired(required = true) + private DaoConfig myConfig; + private FhirContext myContext; + @Autowired + private PlatformTransactionManager myPlatformTransactionManager; + @Autowired + private List> myResourceDaos; + @Autowired + private IResourceHistoryTableDao myResourceHistoryTableDao; + private Map, IFhirResourceDao> myResourceTypeToDao; + @Autowired + private ISearchDao mySearchDao; + @Autowired + private ISearchParamExtractor mySearchParamExtractor; + @Autowired + private ISearchParamPresenceSvc mySearchParamPresenceSvc; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private ISearchResultDao mySearchResultDao; + @Autowired + private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; + + private void autoCreateResource(T theResource) { + IFhirResourceDao dao = (IFhirResourceDao) getDao(theResource.getClass()); + dao.create(theResource); + } protected void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { if (theRequestDetails != null) { @@ -188,13 +196,84 @@ public abstract class BaseHapiFhirDao implements IDao { return InstantDt.withCurrentTime(); } + private Set extractCompositeStringUniques(ResourceTable theEntity, Set theStringParams, Set theTokenParams, Set theNumberParams, Set theQuantityParams, Set theDateParams, Set theUriParams, Set theLinks) { + Set compositeStringUniques; + compositeStringUniques = new HashSet<>(); + List uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(theEntity.getResourceType()); + for (JpaRuntimeSearchParam next : uniqueSearchParams) { + + List> partsChoices = new ArrayList<>(); + + for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) { + Set paramsListForCompositePart = null; + Set linksForCompositePart = null; + switch (nextCompositeOf.getParamType()) { + case NUMBER: + paramsListForCompositePart = theNumberParams; + break; + case DATE: + paramsListForCompositePart = theDateParams; + break; + case STRING: + paramsListForCompositePart = theStringParams; + break; + case TOKEN: + paramsListForCompositePart = theTokenParams; + break; + case REFERENCE: + linksForCompositePart = theLinks; + break; + case QUANTITY: + paramsListForCompositePart = theQuantityParams; + break; + case URI: + paramsListForCompositePart = theUriParams; + break; + case COMPOSITE: + case HAS: + break; + } + + ArrayList nextChoicesList = new ArrayList<>(); + partsChoices.add(nextChoicesList); + + String key = UrlUtil.escape(nextCompositeOf.getName()); + if (paramsListForCompositePart != null) { + for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) { + if (nextParam.getParamName().equals(nextCompositeOf.getName())) { + IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType(); + String value = nextParamAsClientParam.getValueAsQueryToken(getContext()); + value = UrlUtil.escape(value); + nextChoicesList.add(key + "=" + value); + } + } + } + if (linksForCompositePart != null) { + for (ResourceLink nextLink : linksForCompositePart) { + String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue(); + value = UrlUtil.escape(value); + nextChoicesList.add(key + "=" + value); + } + } + } + + Set queryStringsToPopulate = extractCompositeStringUniquesValueChains(theEntity.getResourceType(), partsChoices); + + for (String nextQueryString : queryStringsToPopulate) { + compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString)); + } + } + + return compositeStringUniques; + } + /** * @return Returns a set containing all of the parameter names that - * were found to have a value + * were found to have a value */ @SuppressWarnings("unchecked") protected Set extractResourceLinks(ResourceTable theEntity, IBaseResource theResource, Set theLinks, Date theUpdateTime) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); /* * For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing.. @@ -243,12 +322,12 @@ public abstract class BaseHapiFhirDao implements IDao { /* * This can only really happen if the DAO is being called * programatically with a Bundle (not through the FHIR REST API) - * but Smile does this + * but Smile does this */ if (nextId.isEmpty() && nextValue.getResource() != null) { nextId = nextValue.getResource().getIdElement(); } - + if (nextId.isEmpty() || nextId.getValue().startsWith("#")) { // This is a blank or contained resource reference continue; @@ -291,7 +370,7 @@ public abstract class BaseHapiFhirDao implements IDao { 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 - " + nextId.getValue()); + "Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue()); } if (isNotBlank(baseUrl)) { @@ -352,14 +431,14 @@ public abstract class BaseHapiFhirDao implements IDao { if (!typeString.equals(target.getResourceType())) { throw new UnprocessableEntityException( - "Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType()); + "Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType()); } if (target.getDeleted() != null) { String resName = targetResourceDef.getName(); throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit); } - + if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) { continue; } @@ -375,11 +454,6 @@ public abstract class BaseHapiFhirDao implements IDao { return retVal; } - private void autoCreateResource(T theResource) { - IFhirResourceDao dao = (IFhirResourceDao) getDao(theResource.getClass()); - dao.create(theResource); - } - protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); } @@ -509,7 +583,7 @@ public abstract class BaseHapiFhirDao implements IDao { @SuppressWarnings("unchecked") private void findMissingSearchParams(ResourceTable theEntity, Set> activeSearchParams, RestSearchParameterTypeEnum type, - Set paramCollection) { + Set paramCollection) { for (Entry nextEntry : activeSearchParams) { String nextParamName = nextEntry.getKey(); if (nextEntry.getValue().getParamType() == type) { @@ -569,11 +643,20 @@ public abstract class BaseHapiFhirDao implements IDao { return myConfig; } + public void setConfig(DaoConfig theConfig) { + myConfig = theConfig; + } + @Override public FhirContext getContext() { return myContext; } + @Autowired + public void setContext(FhirContext theContext) { + myContext = theContext; + } + public FhirContext getContext(FhirVersionEnum theVersion) { Validate.notNull(theVersion, "theVersion must not be null"); synchronized (ourRetrievalContexts) { @@ -606,6 +689,10 @@ public abstract class BaseHapiFhirDao implements IDao { return dao; } + public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao() { + return myResourceIndexedCompositeStringUniqueDao; + } + @Override public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) { Map params = mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName()); @@ -621,23 +708,23 @@ public abstract class BaseHapiFhirDao implements IDao { if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) { return null; } - + CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery cq = builder.createQuery(TagDefinition.class); Root from = cq.from(TagDefinition.class); if (isNotBlank(theScheme)) { cq.where( - builder.and( - builder.equal(from.get("myTagType"), theTagType), - builder.equal(from.get("mySystem"), theScheme), - builder.equal(from.get("myCode"), theTerm))); + builder.and( + builder.equal(from.get("myTagType"), theTagType), + builder.equal(from.get("mySystem"), theScheme), + builder.equal(from.get("myCode"), theTerm))); } else { cq.where( - builder.and( - builder.equal(from.get("myTagType"), theTagType), - builder.isNull(from.get("mySystem")), - builder.equal(from.get("myCode"), theTerm))); + builder.and( + builder.equal(from.get("myTagType"), theTagType), + builder.isNull(from.get("mySystem")), + builder.equal(from.get("myCode"), theTerm))); } TypedQuery q = myEntityManager.createQuery(cq); @@ -665,7 +752,7 @@ public abstract class BaseHapiFhirDao implements IDao { } } - Set tagIds = new HashSet(); + Set tagIds = new HashSet<>(); findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceTag.class); findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceHistoryTag.class); if (tagIds.isEmpty()) { @@ -764,8 +851,8 @@ public abstract class BaseHapiFhirDao implements IDao { @Override public SearchBuilder newSearchBuilder() { SearchBuilder builder = new SearchBuilder(getContext(), myEntityManager, myFulltextSearchSvc, this, myResourceIndexedSearchParamUriDao, - myForcedIdDao, - myTerminologySvc, mySerarchParamRegistry); + myForcedIdDao, + myTerminologySvc, mySerarchParamRegistry); return builder; } @@ -773,7 +860,7 @@ public abstract class BaseHapiFhirDao implements IDao { if (theRequestDetails.getId() != null && theRequestDetails.getId().hasResourceType() && isNotBlank(theRequestDetails.getResourceType())) { if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) { throw new InternalErrorException( - "Inconsistent server state - Resource types don't match: " + theRequestDetails.getId().getResourceType() + " / " + theRequestDetails.getResourceType()); + "Inconsistent server state - Resource types don't match: " + theRequestDetails.getId().getResourceType() + " / " + theRequestDetails.getResourceType()); } } @@ -791,7 +878,7 @@ public abstract class BaseHapiFhirDao implements IDao { @SuppressWarnings("rawtypes") List childElements = getContext().newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class); for (@SuppressWarnings("rawtypes") - IPrimitiveType nextType : childElements) { + IPrimitiveType nextType : childElements) { if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) { String nextValue = nextType.getValueAsString(); if (isNotBlank(nextValue)) { @@ -1063,11 +1150,9 @@ public abstract class BaseHapiFhirDao implements IDao { /** * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time. - * - * @param theEntity - * The entity being updated (Do not modify the entity! Undefined behaviour will occur!) - * @param theResource - * The resource being persisted + * + * @param theEntity The entity being updated (Do not modify the entity! Undefined behaviour will occur!) + * @param theResource The resource being persisted */ protected void postPersist(ResourceTable theEntity, T theResource) { // nothing @@ -1075,11 +1160,9 @@ public abstract class BaseHapiFhirDao implements IDao { /** * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database - * - * @param theEntity - * The resource - * @param theResource - * The resource being persisted + * + * @param theEntity The resource + * @param theResource The resource being persisted */ protected void postUpdate(ResourceTable theEntity, T theResource) { // nothing @@ -1111,15 +1194,6 @@ public abstract class BaseHapiFhirDao implements IDao { throw new NotImplementedException(""); } - public void setConfig(DaoConfig theConfig) { - myConfig = theConfig; - } - - @Autowired - public void setContext(FhirContext theContext) { - myContext = theContext; - } - public void setEntityManager(EntityManager theEntityManager) { myEntityManager = theEntityManager; } @@ -1142,11 +1216,9 @@ public abstract class BaseHapiFhirDao implements IDao { *

* See Updates to Tags, Profiles, and Security Labels for a description of the logic that the default behaviour folows. *

- * - * @param theEntity - * The entity being updated (Do not modify the entity! Undefined behaviour will occur!) - * @param theTag - * The tag + * + * @param theEntity The entity being updated (Do not modify the entity! Undefined behaviour will occur!) + * @param theTag The tag * @return Retturns true if the tag should be removed */ protected boolean shouldDroppedTagBeRemovedOnUpdate(ResourceTable theEntity, ResourceTag theTag) { @@ -1156,6 +1228,13 @@ public abstract class BaseHapiFhirDao implements IDao { return false; } + @Override + public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) { + RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType()); + Class resourceType = type.getImplementingClass(); + return toResource(resourceType, theEntity, theForHistoryOperation); + } + // protected ResourceTable toEntity(IResource theResource) { // ResourceTable retVal = new ResourceTable(); // @@ -1164,13 +1243,6 @@ public abstract class BaseHapiFhirDao implements IDao { // return retVal; // } - @Override - public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) { - RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType()); - Class resourceType = type.getImplementingClass(); - return toResource(resourceType, theEntity, theForHistoryOperation); - } - @SuppressWarnings("unchecked") @Override public R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation) { @@ -1268,7 +1340,7 @@ public abstract class BaseHapiFhirDao implements IDao { @SuppressWarnings("unchecked") protected ResourceTable updateEntity(final IBaseResource theResource, ResourceTable theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing, - boolean theUpdateVersion, Date theUpdateTime, boolean theForceUpdate, boolean theCreateNewHistoryEntry) { + boolean theUpdateVersion, Date theUpdateTime, boolean theForceUpdate, boolean theCreateNewHistoryEntry) { ourLog.debug("Starting entity update"); /* @@ -1281,7 +1353,7 @@ public abstract class BaseHapiFhirDao implements IDao { 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 + "]"); + "Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]"); } } @@ -1291,38 +1363,42 @@ public abstract class BaseHapiFhirDao implements IDao { theEntity.setPublished(theUpdateTime); } - Collection paramsString = new ArrayList<>(); + Collection existingStringParams = new ArrayList<>(); if (theEntity.isParamsStringPopulated()) { - paramsString.addAll(theEntity.getParamsString()); + existingStringParams.addAll(theEntity.getParamsString()); } - Collection paramsToken = new ArrayList<>(); + Collection existingTokenParams = new ArrayList<>(); if (theEntity.isParamsTokenPopulated()) { - paramsToken.addAll(theEntity.getParamsToken()); + existingTokenParams.addAll(theEntity.getParamsToken()); } - Collection paramsNumber = new ArrayList<>(); + Collection existingNumberParams = new ArrayList<>(); if (theEntity.isParamsNumberPopulated()) { - paramsNumber.addAll(theEntity.getParamsNumber()); + existingNumberParams.addAll(theEntity.getParamsNumber()); } - Collection paramsQuantity = new ArrayList<>(); + Collection existingQuantityParams = new ArrayList<>(); if (theEntity.isParamsQuantityPopulated()) { - paramsQuantity.addAll(theEntity.getParamsQuantity()); + existingQuantityParams.addAll(theEntity.getParamsQuantity()); } - Collection paramsDate = new ArrayList<>(); + Collection existingDateParams = new ArrayList<>(); if (theEntity.isParamsDatePopulated()) { - paramsDate.addAll(theEntity.getParamsDate()); + existingDateParams.addAll(theEntity.getParamsDate()); } - Collection paramsUri = new ArrayList<>(); + Collection existingUriParams = new ArrayList<>(); if (theEntity.isParamsUriPopulated()) { - paramsUri.addAll(theEntity.getParamsUri()); + existingUriParams.addAll(theEntity.getParamsUri()); } - Collection paramsCoords = new ArrayList<>(); + Collection existingCoordsParams = new ArrayList<>(); if (theEntity.isParamsCoordsPopulated()) { - paramsCoords.addAll(theEntity.getParamsCoords()); + existingCoordsParams.addAll(theEntity.getParamsCoords()); } Collection existingResourceLinks = new ArrayList<>(); if (theEntity.isHasLinks()) { existingResourceLinks.addAll(theEntity.getResourceLinks()); } + Collection existingCompositeStringUniques = new ArrayList<>(); + if (theEntity.isParamsCompositeStringUniquePresent()) { + existingCompositeStringUniques.addAll(theEntity.getParamsCompositeStringUnique()); + } Set stringParams = null; Set tokenParams = null; @@ -1331,6 +1407,7 @@ public abstract class BaseHapiFhirDao implements IDao { Set dateParams = null; Set uriParams = null; Set coordsParams = null; + Set compositeStringUniques = null; Set links = null; Set populatedResourceLinkParameters = Collections.emptySet(); @@ -1365,10 +1442,9 @@ public abstract class BaseHapiFhirDao implements IDao { uriParams = extractSearchParamUri(theEntity, theResource); coordsParams = extractSearchParamCoords(theEntity, theResource); - // ourLog.info("Indexing resource: {}", entity.getId()); ourLog.trace("Storing date indexes: {}", dateParams); - tokenParams = new HashSet(); + tokenParams = new HashSet<>(); for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) { if (next instanceof ResourceIndexedSearchParamToken) { tokenParams.add((ResourceIndexedSearchParamToken) next); @@ -1395,8 +1471,7 @@ public abstract class BaseHapiFhirDao implements IDao { /* * Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the - * matching - * resource. + * matching resource. */ if (myConfig.isAllowInlineMatchUrlReferences()) { FhirTerser terser = getContext().newTerser(); @@ -1443,13 +1518,13 @@ public abstract class BaseHapiFhirDao implements IDao { } } - links = new HashSet(); + links = new HashSet<>(); populatedResourceLinkParameters = extractResourceLinks(theEntity, theResource, links, theUpdateTime); /* * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them */ - for (Iterator existingLinkIter = existingResourceLinks.iterator(); existingLinkIter.hasNext();) { + for (Iterator existingLinkIter = existingResourceLinks.iterator(); existingLinkIter.hasNext(); ) { ResourceLink nextExisting = existingLinkIter.next(); if (links.remove(nextExisting)) { existingLinkIter.remove(); @@ -1457,6 +1532,12 @@ public abstract class BaseHapiFhirDao implements IDao { } } + /* + * Handle composites + */ + compositeStringUniques = extractCompositeStringUniques(theEntity, stringParams, tokenParams, numberParams, quantityParams, dateParams, uriParams, links); + + changed = populateResourceIntoEntity(theResource, theEntity, true); theEntity.setUpdated(theUpdateTime); @@ -1479,6 +1560,8 @@ public abstract class BaseHapiFhirDao implements IDao { theEntity.setParamsUriPopulated(uriParams.isEmpty() == false); theEntity.setParamsCoords(coordsParams); theEntity.setParamsCoordsPopulated(coordsParams.isEmpty() == false); + theEntity.setParamsCompositeStringUnique(compositeStringUniques); + theEntity.setParamsCompositeStringUniquePresent(compositeStringUniques.isEmpty() == false); theEntity.setResourceLinks(links); theEntity.setHasLinks(links.isEmpty() == false); theEntity.setIndexStatus(INDEX_STATUS_INDEXED); @@ -1529,7 +1612,7 @@ public abstract class BaseHapiFhirDao implements IDao { /* * Update the "search param present" table which is used for the * ?foo:missing=true queries - * + * * Note that we're only populating this for reference params * because the index tables for all other types have a MISSING column * right on them for handling the :missing queries. We can't use the @@ -1567,28 +1650,28 @@ public abstract class BaseHapiFhirDao implements IDao { */ if (thePerformIndexing) { - for (ResourceIndexedSearchParamString next : paramsString) { + for (ResourceIndexedSearchParamString next : existingStringParams) { myEntityManager.remove(next); } for (ResourceIndexedSearchParamString next : stringParams) { myEntityManager.persist(next); } - for (ResourceIndexedSearchParamToken next : paramsToken) { + for (ResourceIndexedSearchParamToken next : existingTokenParams) { myEntityManager.remove(next); } for (ResourceIndexedSearchParamToken next : tokenParams) { myEntityManager.persist(next); } - for (ResourceIndexedSearchParamNumber next : paramsNumber) { + for (ResourceIndexedSearchParamNumber next : existingNumberParams) { myEntityManager.remove(next); } for (ResourceIndexedSearchParamNumber next : numberParams) { myEntityManager.persist(next); } - for (ResourceIndexedSearchParamQuantity next : paramsQuantity) { + for (ResourceIndexedSearchParamQuantity next : existingQuantityParams) { myEntityManager.remove(next); } for (ResourceIndexedSearchParamQuantity next : quantityParams) { @@ -1596,7 +1679,7 @@ public abstract class BaseHapiFhirDao implements IDao { } // Store date SP's - for (ResourceIndexedSearchParamDate next : paramsDate) { + for (ResourceIndexedSearchParamDate next : existingDateParams) { myEntityManager.remove(next); } for (ResourceIndexedSearchParamDate next : dateParams) { @@ -1604,7 +1687,7 @@ public abstract class BaseHapiFhirDao implements IDao { } // Store URI SP's - for (ResourceIndexedSearchParamUri next : paramsUri) { + for (ResourceIndexedSearchParamUri next : existingUriParams) { myEntityManager.remove(next); } for (ResourceIndexedSearchParamUri next : uriParams) { @@ -1612,7 +1695,7 @@ public abstract class BaseHapiFhirDao implements IDao { } // Store Coords SP's - for (ResourceIndexedSearchParamCoords next : paramsCoords) { + for (ResourceIndexedSearchParamCoords next : existingCoordsParams) { myEntityManager.remove(next); } for (ResourceIndexedSearchParamCoords next : coordsParams) { @@ -1629,6 +1712,14 @@ public abstract class BaseHapiFhirDao implements IDao { // make sure links are indexed theEntity.setResourceLinks(links); + // Store composite string uniques + for (ResourceIndexedCompositeStringUnique next : existingCompositeStringUniques) { + myEntityManager.remove(next); + } + for (ResourceIndexedCompositeStringUnique next : compositeStringUniques) { + myEntityManager.persist(next); + } + theEntity.toString(); } // if thePerformIndexing @@ -1693,7 +1784,7 @@ public abstract class BaseHapiFhirDao implements IDao { if (!referencedId.getValue().contains("?")) { if (!validTypes.contains(referencedId.getResourceType())) { throw new UnprocessableEntityException( - "Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path"); + "Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path"); } } } @@ -1740,17 +1831,15 @@ 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. - * - * @param theResource - * The resource that is about to be persisted - * @param theEntityToSave - * TODO + * + * @param theResource The resource that is about to be persisted + * @param theEntityToSave TODO */ protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) { Object tag = null; - + int totalMetaCount = 0; - + if (theResource instanceof IResource) { IResource res = (IResource) theResource; TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res); @@ -1778,7 +1867,77 @@ public abstract class BaseHapiFhirDao implements IDao { validateChildReferences(theResource, resName); validateMetaCount(totalMetaCount); - + + } + + /** + * This method is used to create a set of all possible combinations of + * parameters across a set of search parameters. An example of why + * this is needed: + *

+ * Let's say we have a unique index on (Patient:gender AND Patient:name). + * Then we pass in SMITH, John with a gender of male. + *

+ *

+ * In this case, because the name parameter matches both first and last name, + * we now need two unique indexes: + *

    + *
  • Patient?gender=male&name=SMITH
  • + *
  • Patient?gender=male&name=JOHN
  • + *
+ *

+ *

+ * So this recursive algorithm calculates those + *

+ * + * @param theResourceType E.g. Patient + * @param thePartsChoices E.g. [[gender=male], [name=SMITH, name=JOHN]] + */ + public static Set extractCompositeStringUniquesValueChains(String theResourceType, List> thePartsChoices) { + + Collections.sort(thePartsChoices, new Comparator>() { + @Override + public int compare(List o1, List o2) { + String str1=null; + String str2=null; + if (o1.size() > 0) { + str1 = o1.get(0); + } + if (o2.size() > 0) { + str2 = o2.get(0); + } + return StringUtils.compare(str1, str2); + } + }); + + List values = new ArrayList<>(); + Set queryStringsToPopulate = new HashSet<>(); + extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices, values, queryStringsToPopulate); + return queryStringsToPopulate; + } + + private static void extractCompositeStringUniquesValueChains(String theResourceType, List> thePartsChoices, List theValues, Set theQueryStringsToPopulate) { + if (thePartsChoices.size() > 0) { + List nextList = thePartsChoices.get(0); + Collections.sort(nextList); + for (String nextChoice : nextList) { + theValues.add(nextChoice); + extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices.subList(1, thePartsChoices.size()), theValues, theQueryStringsToPopulate); + theValues.remove(theValues.size() - 1); + } + } else { + if (theValues.size() > 0) { + StringBuilder uniqueString = new StringBuilder(); + uniqueString.append(theResourceType); + + for (int i = 0; i < theValues.size(); i++) { + uniqueString.append(i == 0 ? "?" : "&"); + uniqueString.append(theValues.get(i)); + } + + theQueryStringsToPopulate.add(uniqueString.toString()); + } + } } protected static boolean isValidPid(IIdType theId) { @@ -1990,7 +2149,7 @@ public abstract class BaseHapiFhirDao implements IDao { RuntimeSearchParam paramDef = theCallingDao.getSearchParamByName(resourceDef, nextParamName); if (paramDef == null) { throw new InvalidRequestException( - "Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); + "Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); } IQueryParameterAnd param = ParameterUtil.parseQueryParams(theContext, paramDef, nextParamName, paramList); @@ -2023,7 +2182,7 @@ public abstract class BaseHapiFhirDao implements IDao { public static void validateResourceType(BaseHasResource theEntity, String theResourceName) { if (!theResourceName.equals(theEntity.getResourceType())) { throw new ResourceNotFoundException( - "Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType()); + "Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType()); } } 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 4b315d2ed0c..51c4d600ad1 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 @@ -34,7 +34,10 @@ import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils; import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils; -import ca.uhn.fhir.model.api.*; +import ca.uhn.fhir.model.api.IQueryParameterAnd; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.PatchTypeEnum; import ca.uhn.fhir.rest.api.QualifiedParamList; @@ -384,7 +387,6 @@ public abstract class BaseHapiFhirResourceDao extends B // Notify JPA interceptors if (theRequestDetails != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theResource); theRequestDetails.getRequestOperationCallback().resourceCreated(theResource); } for (IServerInterceptor next : getConfig().getInterceptors()) { @@ -871,8 +873,7 @@ public abstract class BaseHapiFhirResourceDao extends B throw new ResourceNotFoundException(theId); } - //@formatter:off - for (BaseTag next : new ArrayList(entity.getTags())) { + for (BaseTag next : new ArrayList<>(entity.getTags())) { if (ObjectUtil.equals(next.getTag().getTagType(), theTagType) && ObjectUtil.equals(next.getTag().getSystem(), theScheme) && ObjectUtil.equals(next.getTag().getCode(), theTerm)) { @@ -880,7 +881,6 @@ public abstract class BaseHapiFhirResourceDao extends B entity.getTags().remove(next); } } - //@formatter:on if (entity.getTags().isEmpty()) { entity.setHasTags(false); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamRegistry.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamRegistry.java index 068cc8873d8..47cf35ac5da 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamRegistry.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamRegistry.java @@ -20,27 +20,28 @@ package ca.uhn.fhir.jpa.dao; * #L% */ -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import javax.annotation.PostConstruct; - -import org.apache.commons.lang3.Validate; -import org.springframework.beans.factory.annotation.Autowired; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.PostConstruct; +import java.util.*; public abstract class BaseSearchParamRegistry implements ISearchParamRegistry { + private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamRegistry.class); private Map> myBuiltInSearchParams; + private volatile Map> myActiveUniqueSearchParams; + private volatile Map, List>> myActiveParamNamesToUniqueSearchParams; @Autowired private FhirContext myCtx; - @Autowired private Collection> myDaos; @@ -75,18 +76,119 @@ public abstract class BaseSearchParamRegistry implements ISearchParamRegistry { return myBuiltInSearchParams.get(theResourceName); } + @Override + public List getActiveUniqueSearchParams(String theResourceName) { + refreshCacheIfNecessary(); + List retVal = myActiveUniqueSearchParams.get(theResourceName); + if (retVal == null) { + retVal = Collections.emptyList(); + } + return retVal; + } + + @Override + public List getActiveUniqueSearchParams(String theResourceName, Set theParamNames) { + refreshCacheIfNecessary(); + + Map, List> paramNamesToParams = myActiveParamNamesToUniqueSearchParams.get(theResourceName); + if (paramNamesToParams == null) { + return Collections.emptyList(); + } + + List retVal = paramNamesToParams.get(theParamNames); + if (retVal == null) { + retVal = Collections.emptyList(); + } + return Collections.unmodifiableList(retVal); + } + public Map> getBuiltInSearchParams() { return myBuiltInSearchParams; } + public void populateActiveSearchParams(Map> theActiveSearchParams) { + Map> activeUniqueSearchParams = new HashMap<>(); + Map, List>> activeParamNamesToUniqueSearchParams = new HashMap<>(); + + Map idToRuntimeSearchParam = new HashMap<>(); + List jpaSearchParams = new ArrayList<>(); + + /* + * Loop through parameters and find JPA params + */ + for (Map.Entry> nextResourceNameToEntries : theActiveSearchParams.entrySet()) { + List uniqueSearchParams = activeUniqueSearchParams.get(nextResourceNameToEntries.getKey()); + if (uniqueSearchParams == null) { + uniqueSearchParams = new ArrayList<>(); + activeUniqueSearchParams.put(nextResourceNameToEntries.getKey(), uniqueSearchParams); + } + Collection nextSearchParamsForResourceName = nextResourceNameToEntries.getValue().values(); + for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) { + + if (nextCandidate.getId() != null) { + idToRuntimeSearchParam.put(nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate); + } + + if (nextCandidate instanceof JpaRuntimeSearchParam) { + JpaRuntimeSearchParam nextCandidateCasted = (JpaRuntimeSearchParam) nextCandidate; + jpaSearchParams.add(nextCandidateCasted); + if (nextCandidateCasted.isUnique()) { + uniqueSearchParams.add(nextCandidateCasted); + } + } + } + + } + + Set haveSeen = new HashSet<>(); + for (JpaRuntimeSearchParam next : jpaSearchParams) { + if (!haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) { + continue; + } + + Set paramNames = new HashSet<>(); + for (JpaRuntimeSearchParam.Component nextComponent : next.getComponents()) { + String nextRef = nextComponent.getReference().getReferenceElement().toUnqualifiedVersionless().getValue(); + RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef); + if (componentTarget != null) { + next.getCompositeOf().add(componentTarget); + paramNames.add(componentTarget.getName()); + } else { + ourLog.warn("Search parameter {} refers to unknown component {}", next.getId().toUnqualifiedVersionless().getValue(), nextRef); + } + } + + if (next.getCompositeOf() != null) { + Collections.sort(next.getCompositeOf(), new Comparator() { + @Override + public int compare(RuntimeSearchParam theO1, RuntimeSearchParam theO2) { + return StringUtils.compare(theO1.getName(), theO2.getName()); + } + }); + for (String nextBase : next.getBase()) { + if (!activeParamNamesToUniqueSearchParams.containsKey(nextBase)) { + activeParamNamesToUniqueSearchParams.put(nextBase, new HashMap, List>()); + } + if (!activeParamNamesToUniqueSearchParams.get(nextBase).containsKey(paramNames)) { + activeParamNamesToUniqueSearchParams.get(nextBase).put(paramNames, new ArrayList()); + } + activeParamNamesToUniqueSearchParams.get(nextBase).get(paramNames).add(next); + } + } + } + + myActiveUniqueSearchParams = activeUniqueSearchParams; + myActiveParamNamesToUniqueSearchParams = activeParamNamesToUniqueSearchParams; + } + @PostConstruct public void postConstruct() { - Map> resourceNameToSearchParams = new HashMap>(); + Map> resourceNameToSearchParams = new HashMap<>(); for (IFhirResourceDao nextDao : myDaos) { RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(nextDao.getResourceType()); String nextResourceName = nextResDef.getName(); - HashMap nameToParam = new HashMap(); + HashMap nameToParam = new HashMap<>(); resourceNameToSearchParams.put(nextResourceName, nameToParam); for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) { @@ -97,4 +199,6 @@ public abstract class BaseSearchParamRegistry implements ISearchParamRegistry { myBuiltInSearchParams = Collections.unmodifiableMap(resourceNameToSearchParams); } + protected abstract void refreshCacheIfNecessary(); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java index 611ba169e05..e02e29775b8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java @@ -20,21 +20,27 @@ package ca.uhn.fhir.jpa.dao; * #L% */ -import java.util.Map; - import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; + +import java.util.List; +import java.util.Map; +import java.util.Set; public interface ISearchParamRegistry { void forceRefresh(); - Map> getActiveSearchParams(); - - Map getActiveSearchParams(String theResourceName); - /** * @return Returns {@literal null} if no match */ RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName); + Map getActiveSearchParams(String theResourceName); + + Map> getActiveSearchParams(); + + List getActiveUniqueSearchParams(String theResourceName); + + List getActiveUniqueSearchParams(String theResourceName, Set theParamNames); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index c3d9861097a..eace417ea9b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -19,19 +19,40 @@ package ca.uhn.fhir.jpa.dao; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.*; -import java.math.BigDecimal; -import java.math.MathContext; -import java.util.*; -import java.util.Map.Entry; - -import javax.persistence.EntityManager; -import javax.persistence.TypedQuery; -import javax.persistence.criteria.*; -import javax.persistence.criteria.CriteriaBuilder.In; - -import org.apache.commons.lang3.*; +import ca.uhn.fhir.context.*; +import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; +import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; +import ca.uhn.fhir.jpa.term.VersionIndependentConcept; +import ca.uhn.fhir.jpa.util.BaseIterator; +import ca.uhn.fhir.jpa.util.StopWatch; +import ca.uhn.fhir.model.api.*; +import ca.uhn.fhir.model.base.composite.BaseCodingDt; +import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; +import ca.uhn.fhir.model.base.composite.BaseQuantityDt; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.UrlUtil; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.tuple.Pair; @@ -39,28 +60,20 @@ import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.query.Query; import org.hl7.fhir.dstu3.model.BaseResource; -import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; -import com.google.common.collect.*; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.*; +import javax.persistence.criteria.CriteriaBuilder.In; +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.*; +import java.util.Map.Entry; -import ca.uhn.fhir.context.*; -import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; -import ca.uhn.fhir.jpa.entity.*; -import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; -import ca.uhn.fhir.jpa.term.VersionIndependentConcept; -import ca.uhn.fhir.jpa.util.BaseIterator; -import ca.uhn.fhir.jpa.util.StopWatch; -import ca.uhn.fhir.model.api.*; -import ca.uhn.fhir.model.base.composite.*; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.InstantDt; -import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.api.*; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.*; -import ca.uhn.fhir.util.UrlUtil; +import static org.apache.commons.lang3.StringUtils.*; /** * The SearchBuilder is responsible for actually forming the SQL query that handles @@ -69,8 +82,9 @@ import ca.uhn.fhir.util.UrlUtil; public class SearchBuilder implements ISearchBuilder { private static final List EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList()); - private static Long NO_MORE = Long.valueOf(-1); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class); + private static Long NO_MORE = Long.valueOf(-1); + private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest; private List myAlsoIncludePids; private CriteriaBuilder myBuilder; private BaseHapiFhirDao myCallingDao; @@ -94,8 +108,8 @@ public class SearchBuilder implements ISearchBuilder { * Constructor */ public SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager, IFulltextSearchSvc theFulltextSearchSvc, - BaseHapiFhirDao theDao, - IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao, IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry) { + BaseHapiFhirDao theDao, + IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao, IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry) { myContext = theFhirContext; myEntityManager = theEntityManager; myFulltextSearchSvc = theFulltextSearchSvc; @@ -135,7 +149,7 @@ public class SearchBuilder implements ISearchBuilder { return; } - List codePredicates = new ArrayList(); + List codePredicates = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { IQueryParameterType params = nextOr; Predicate p = createPredicateDate(params, theResourceName, theParamName, myBuilder, join); @@ -358,7 +372,7 @@ public class SearchBuilder implements ISearchBuilder { if (!ref.getValue().matches("[a-zA-Z]+\\/.*")) { RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); - resourceTypes = new ArrayList>(); + resourceTypes = new ArrayList<>(); Set targetTypes = param.getTargets(); @@ -457,7 +471,7 @@ public class SearchBuilder implements ISearchBuilder { IQueryParameterType chainValue; if (remainingChain != null) { if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { - ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", new Object[] { nextType.getSimpleName(), chain, remainingChain }); + ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", new Object[]{nextType.getSimpleName(), chain, remainingChain}); continue; } @@ -785,7 +799,7 @@ public class SearchBuilder implements ISearchBuilder { */ ourLog.info("Searching for candidate URI:above parameters for Resource[{}] param[{}]", myResourceName, theParamName); Collection candidates = myResourceIndexedSearchParamUriDao.findAllByResourceTypeAndParamName(myResourceName, theParamName); - List toFind = new ArrayList(); + List toFind = new ArrayList<>(); for (String next : candidates) { if (value.length() >= next.length()) { if (value.substring(0, next.length()).equals(next)) { @@ -798,12 +812,12 @@ public class SearchBuilder implements ISearchBuilder { continue; } - predicate = join. get("myUri").as(String.class).in(toFind); + predicate = join.get("myUri").as(String.class).in(toFind); } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { - predicate = myBuilder.like(join. get("myUri").as(String.class), createLeftMatchLikeExpression(value)); + predicate = myBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value)); } else { - predicate = myBuilder.equal(join. get("myUri").as(String.class), value); + predicate = myBuilder.equal(join.get("myUri").as(String.class), value); } codePredicates.add(predicate); } else { @@ -817,7 +831,7 @@ public class SearchBuilder implements ISearchBuilder { * just add a predicate that can never match */ if (codePredicates.isEmpty()) { - Predicate predicate = myBuilder.isNull(join. get("myMissing").as(String.class)); + Predicate predicate = myBuilder.isNull(join.get("myMissing").as(String.class)); myPredicates.add(predicate); return; } @@ -840,32 +854,32 @@ public class SearchBuilder implements ISearchBuilder { private Predicate createCompositeParamPart(String theResourceName, Root theRoot, RuntimeSearchParam theParam, IQueryParameterType leftValue) { Predicate retVal = null; switch (theParam.getParamType()) { - case STRING: { - From stringJoin = theRoot.join("myParamsString", JoinType.INNER); - retVal = createPredicateString(leftValue, theResourceName, theParam.getName(), myBuilder, stringJoin); - break; - } - case TOKEN: { - From tokenJoin = theRoot.join("myParamsToken", JoinType.INNER); - retVal = createPredicateToken(leftValue, theResourceName, theParam.getName(), myBuilder, tokenJoin); - break; - } - case DATE: { - From dateJoin = theRoot.join("myParamsDate", JoinType.INNER); - retVal = createPredicateDate(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); - break; - } - case QUANTITY: { - From dateJoin = theRoot.join("myParamsQuantity", JoinType.INNER); - retVal = createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); - break; - } - case COMPOSITE: - case HAS: - case NUMBER: - case REFERENCE: - case URI: - break; + case STRING: { + From stringJoin = theRoot.join("myParamsString", JoinType.INNER); + retVal = createPredicateString(leftValue, theResourceName, theParam.getName(), myBuilder, stringJoin); + break; + } + case TOKEN: { + From tokenJoin = theRoot.join("myParamsToken", JoinType.INNER); + retVal = createPredicateToken(leftValue, theResourceName, theParam.getName(), myBuilder, tokenJoin); + break; + } + case DATE: { + From dateJoin = theRoot.join("myParamsDate", JoinType.INNER); + retVal = createPredicateDate(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); + break; + } + case QUANTITY: { + From dateJoin = theRoot.join("myParamsQuantity", JoinType.INNER); + retVal = createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); + break; + } + case COMPOSITE: + case HAS: + case NUMBER: + case REFERENCE: + case URI: + break; } if (retVal == null) { @@ -880,27 +894,27 @@ public class SearchBuilder implements ISearchBuilder { Join join = null; switch (theType) { - case DATE: - join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT); - break; - case NUMBER: - join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT); - break; - case QUANTITY: - join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT); - break; - case REFERENCE: - join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); - break; - case STRING: - join = myResourceTableRoot.join("myParamsString", JoinType.LEFT); - break; - case URI: - join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT); - break; - case TOKEN: - join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); - break; + case DATE: + join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT); + break; + case NUMBER: + join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT); + break; + case QUANTITY: + join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT); + break; + case REFERENCE: + join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); + break; + case STRING: + join = myResourceTableRoot.join("myParamsString", JoinType.LEFT); + break; + case URI: + join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT); + break; + case TOKEN: + join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); + break; } JoinKey key = new JoinKey(theSearchParameterName, theType); @@ -938,8 +952,8 @@ public class SearchBuilder implements ISearchBuilder { Predicate lb = null; if (lowerBound != null) { - Predicate gt = theBuilder.greaterThanOrEqualTo(theFrom. get("myValueLow"), lowerBound); - Predicate lt = theBuilder.greaterThanOrEqualTo(theFrom. get("myValueHigh"), lowerBound); + Predicate gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); + Predicate lt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), lowerBound); if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) { lb = gt; } else { @@ -949,8 +963,8 @@ public class SearchBuilder implements ISearchBuilder { Predicate ub = null; if (upperBound != null) { - Predicate gt = theBuilder.lessThanOrEqualTo(theFrom. get("myValueLow"), upperBound); - Predicate lt = theBuilder.lessThanOrEqualTo(theFrom. get("myValueHigh"), upperBound); + Predicate gt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), upperBound); + Predicate lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { ub = lt; } else { @@ -968,45 +982,45 @@ public class SearchBuilder implements ISearchBuilder { } private Predicate createPredicateNumeric(String theResourceName, String theParamName, From theFrom, CriteriaBuilder builder, - IQueryParameterType theParam, ParamPrefixEnum thePrefix, BigDecimal theValue, final Expression thePath, - String invalidMessageName) { + IQueryParameterType theParam, ParamPrefixEnum thePrefix, BigDecimal theValue, final Expression thePath, + String invalidMessageName) { Predicate num; switch (thePrefix) { - case GREATERTHAN: - num = builder.gt(thePath, theValue); - break; - case GREATERTHAN_OR_EQUALS: - num = builder.ge(thePath, theValue); - break; - case LESSTHAN: - num = builder.lt(thePath, theValue); - break; - case LESSTHAN_OR_EQUALS: - num = builder.le(thePath, theValue); - break; - case APPROXIMATE: - case EQUAL: - case NOT_EQUAL: - BigDecimal mul = calculateFuzzAmount(thePrefix, theValue); - BigDecimal low = theValue.subtract(mul, MathContext.DECIMAL64); - BigDecimal high = theValue.add(mul, MathContext.DECIMAL64); - Predicate lowPred; - Predicate highPred; - if (thePrefix != ParamPrefixEnum.NOT_EQUAL) { - lowPred = builder.ge(thePath.as(BigDecimal.class), low); - highPred = builder.le(thePath.as(BigDecimal.class), high); - num = builder.and(lowPred, highPred); - ourLog.trace("Searching for {} <= val <= {}", low, high); - } else { - // Prefix was "ne", so reverse it! - lowPred = builder.lt(thePath.as(BigDecimal.class), low); - highPred = builder.gt(thePath.as(BigDecimal.class), high); - num = builder.or(lowPred, highPred); - } - break; - default: - String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, invalidMessageName, thePrefix.getValue(), theParam.getValueAsQueryToken(myContext)); - throw new InvalidRequestException(msg); + case GREATERTHAN: + num = builder.gt(thePath, theValue); + break; + case GREATERTHAN_OR_EQUALS: + num = builder.ge(thePath, theValue); + break; + case LESSTHAN: + num = builder.lt(thePath, theValue); + break; + case LESSTHAN_OR_EQUALS: + num = builder.le(thePath, theValue); + break; + case APPROXIMATE: + case EQUAL: + case NOT_EQUAL: + BigDecimal mul = calculateFuzzAmount(thePrefix, theValue); + BigDecimal low = theValue.subtract(mul, MathContext.DECIMAL64); + BigDecimal high = theValue.add(mul, MathContext.DECIMAL64); + Predicate lowPred; + Predicate highPred; + if (thePrefix != ParamPrefixEnum.NOT_EQUAL) { + lowPred = builder.ge(thePath.as(BigDecimal.class), low); + highPred = builder.le(thePath.as(BigDecimal.class), high); + num = builder.and(lowPred, highPred); + ourLog.trace("Searching for {} <= val <= {}", low, high); + } else { + // Prefix was "ne", so reverse it! + lowPred = builder.lt(thePath.as(BigDecimal.class), low); + highPred = builder.gt(thePath.as(BigDecimal.class), high); + num = builder.or(lowPred, highPred); + } + break; + default: + String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, invalidMessageName, thePrefix.getValue(), theParam.getValueAsQueryToken(myContext)); + throw new InvalidRequestException(msg); } if (theParamName == null) { @@ -1016,7 +1030,7 @@ public class SearchBuilder implements ISearchBuilder { } private Predicate createPredicateQuantity(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, - From theFrom) { + From theFrom) { String systemValue; String unitsValue; ParamPrefixEnum cmpValue; @@ -1069,7 +1083,7 @@ public class SearchBuilder implements ISearchBuilder { } private Predicate createPredicateString(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder, - From theFrom) { + From theFrom) { String rawSearchTerm; if (theParameter instanceof TokenParam) { TokenParam id = (TokenParam) theParameter; @@ -1089,7 +1103,7 @@ public class SearchBuilder implements ISearchBuilder { if (rawSearchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed (" - + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); + + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); } String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm); @@ -1121,7 +1135,7 @@ public class SearchBuilder implements ISearchBuilder { } private Predicate createPredicateToken(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder, - From theFrom) { + From theFrom) { String code; String system; TokenParamModifier modifier = null; @@ -1144,12 +1158,12 @@ public class SearchBuilder implements ISearchBuilder { if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { throw new InvalidRequestException( - "Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); + "Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); } if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { throw new InvalidRequestException( - "Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code); + "Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code); } /* @@ -1240,6 +1254,45 @@ public class SearchBuilder implements ISearchBuilder { myBuilder = myEntityManager.getCriteriaBuilder(); mySearchUuid = theSearchUuid; + /* + * Check if there is a unique key associated with the set + * of parameters passed in + */ + if (myParams.getIncludes().isEmpty()) { + if (myParams.getRevIncludes().isEmpty()) { + if (myParams.getEverythingMode() == null) { + if (myParams.isAllParametersHaveNoModifier()) { + Set paramNames = theParams.keySet(); + if (paramNames.isEmpty() == false) { + List searchParams = mySearchParamRegistry.getActiveUniqueSearchParams(myResourceName, paramNames); + if (searchParams.size() > 0) { + List> params = new ArrayList<>(); + for (Entry>> nextParamNameToValues : theParams.entrySet()) { + String nextParamName = nextParamNameToValues.getKey(); + nextParamName = UrlUtil.escape(nextParamName); + for (List nextAnd : nextParamNameToValues.getValue()) { + ArrayList nextValueList = new ArrayList<>(); + params.add(nextValueList); + for (IQueryParameterType nextOr : nextAnd) { + String nextOrValue = nextOr.getValueAsQueryToken(myContext); + nextOrValue = UrlUtil.escape(nextOrValue); + nextValueList.add(nextParamName + "=" + nextOrValue); + } + } + } + + Set uniqueQueryStrings = BaseHapiFhirDao.extractCompositeStringUniquesValueChains(myResourceName, params); + ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.UNIQUE_INDEX; + return new UniqueIndexIterator(uniqueQueryStrings); + + } + } + } + } + } + } + + ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.STANDARD_QUERY; return new QueryIterator(); } @@ -1334,7 +1387,7 @@ public class SearchBuilder implements ISearchBuilder { /* * Add a predicate to make sure we only include non-deleted resources, and only include * resources of the right type. - * + * * If we have any joins to index tables, we get this behaviour already guaranteed so we don't * need an explicit predicate for it. */ @@ -1370,7 +1423,7 @@ public class SearchBuilder implements ISearchBuilder { /** * @return Returns {@literal true} if any search parameter sorts were found, or false if - * no sorts were found, or only non-search parameters ones (e.g. _id, _lastUpdated) + * no sorts were found, or only non-search parameters ones (e.g. _id, _lastUpdated) */ private boolean createSort(CriteriaBuilder theBuilder, Root theFrom, SortSpec theSort, List theOrders, List thePredicates) { if (theSort == null || isBlank(theSort.getParamName())) { @@ -1411,43 +1464,43 @@ public class SearchBuilder implements ISearchBuilder { JoinEnum joinType; switch (param.getParamType()) { - case STRING: - joinAttrName = "myParamsString"; - sortAttrName = new String[] { "myValueExact" }; - joinType = JoinEnum.STRING; - break; - case DATE: - joinAttrName = "myParamsDate"; - sortAttrName = new String[] { "myValueLow" }; - joinType = JoinEnum.DATE; - break; - case REFERENCE: - joinAttrName = "myResourceLinks"; - sortAttrName = new String[] { "myTargetResourcePid" }; - joinType = JoinEnum.REFERENCE; - break; - case TOKEN: - joinAttrName = "myParamsToken"; - sortAttrName = new String[] { "mySystem", "myValue" }; - joinType = JoinEnum.TOKEN; - break; - case NUMBER: - joinAttrName = "myParamsNumber"; - sortAttrName = new String[] { "myValue" }; - joinType = JoinEnum.NUMBER; - break; - case URI: - joinAttrName = "myParamsUri"; - sortAttrName = new String[] { "myUri" }; - joinType = JoinEnum.URI; - break; - case QUANTITY: - joinAttrName = "myParamsQuantity"; - sortAttrName = new String[] { "myValue" }; - joinType = JoinEnum.QUANTITY; - break; - default: - throw new InvalidRequestException("This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName()); + case STRING: + joinAttrName = "myParamsString"; + sortAttrName = new String[]{"myValueExact"}; + joinType = JoinEnum.STRING; + break; + case DATE: + joinAttrName = "myParamsDate"; + sortAttrName = new String[]{"myValueLow"}; + joinType = JoinEnum.DATE; + break; + case REFERENCE: + joinAttrName = "myResourceLinks"; + sortAttrName = new String[]{"myTargetResourcePid"}; + joinType = JoinEnum.REFERENCE; + break; + case TOKEN: + joinAttrName = "myParamsToken"; + sortAttrName = new String[]{"mySystem", "myValue"}; + joinType = JoinEnum.TOKEN; + break; + case NUMBER: + joinAttrName = "myParamsNumber"; + sortAttrName = new String[]{"myValue"}; + joinType = JoinEnum.NUMBER; + break; + case URI: + joinAttrName = "myParamsUri"; + sortAttrName = new String[]{"myUri"}; + joinType = JoinEnum.URI; + break; + case QUANTITY: + joinAttrName = "myParamsQuantity"; + sortAttrName = new String[]{"myValue"}; + joinType = JoinEnum.QUANTITY; + break; + default: + throw new InvalidRequestException("This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName()); } /* @@ -1513,7 +1566,7 @@ public class SearchBuilder implements ISearchBuilder { } private void doLoadPids(List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, EntityManager entityManager, FhirContext context, IDao theDao, - Map position, Collection pids) { + Map position, Collection pids) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery cq = builder.createQuery(ResourceTable.class); Root from = cq.from(ResourceTable.class); @@ -1549,7 +1602,7 @@ public class SearchBuilder implements ISearchBuilder { @Override public void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, - EntityManager entityManager, FhirContext context, IDao theDao) { + EntityManager entityManager, FhirContext context, IDao theDao) { if (theIncludePids.isEmpty()) { ourLog.info("The include pids are empty"); // return; @@ -1557,7 +1610,7 @@ public class SearchBuilder implements ISearchBuilder { // Dupes will cause a crash later anyhow, but this is expensive so only do it // 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 position = new HashMap(); for (Long next : theIncludePids) { @@ -1572,7 +1625,7 @@ public class SearchBuilder implements ISearchBuilder { * but this should work too. Sigh. */ int maxLoad = 800; - List pids = new ArrayList(theIncludePids); + List pids = new ArrayList<>(theIncludePids); for (int i = 0; i < pids.size(); i += maxLoad) { int to = i + maxLoad; to = Math.min(to, pids.size()); @@ -1589,7 +1642,7 @@ public class SearchBuilder implements ISearchBuilder { */ @Override public HashSet loadReverseIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, - boolean theReverseMode, DateRangeParam theLastUpdated) { + boolean theReverseMode, DateRangeParam theLastUpdated) { if (theMatches.size() == 0) { return new HashSet(); } @@ -1599,9 +1652,9 @@ public class SearchBuilder implements ISearchBuilder { String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid"; Collection nextRoundMatches = theMatches; - HashSet allAdded = new HashSet(); - HashSet original = new HashSet(theMatches); - ArrayList includes = new ArrayList(theRevIncludes); + HashSet allAdded = new HashSet<>(); + HashSet original = new HashSet<>(theMatches); + ArrayList includes = new ArrayList<>(theRevIncludes); int roundCounts = 0; StopWatch w = new StopWatch(); @@ -1610,10 +1663,10 @@ public class SearchBuilder implements ISearchBuilder { do { roundCounts++; - HashSet pidsToInclude = new HashSet(); - Set nextRoundOmit = new HashSet(); + HashSet pidsToInclude = new HashSet<>(); + Set nextRoundOmit = new HashSet<>(); - for (Iterator iter = includes.iterator(); iter.hasNext();) { + for (Iterator iter = includes.iterator(); iter.hasNext(); ) { Include nextInclude = iter.next(); if (nextInclude.isRecurse() == false) { iter.remove(); @@ -1712,7 +1765,7 @@ public class SearchBuilder implements ISearchBuilder { nextRoundMatches = pidsToInclude; } while (includes.size() > 0 && nextRoundMatches.size() > 0 && addedSomeThisRound); - ourLog.info("Loaded {} {} in {} rounds and {} ms", new Object[] { allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart() }); + ourLog.info("Loaded {} {} in {} rounds and {} ms", new Object[]{allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart()}); return allAdded; } @@ -1788,49 +1841,49 @@ public class SearchBuilder implements ISearchBuilder { RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); if (nextParamDef != null) { switch (nextParamDef.getParamType()) { - case DATE: - for (List nextAnd : theAndOrParams) { - addPredicateDate(theResourceName, theParamName, nextAnd); - } - break; - case QUANTITY: - for (List nextAnd : theAndOrParams) { - addPredicateQuantity(theResourceName, theParamName, nextAnd); - } - break; - case REFERENCE: - for (List nextAnd : theAndOrParams) { - addPredicateReference(theResourceName, theParamName, nextAnd); - } - break; - case STRING: - for (List nextAnd : theAndOrParams) { - addPredicateString(theResourceName, theParamName, nextAnd); - } - break; - case TOKEN: - for (List nextAnd : theAndOrParams) { - addPredicateToken(theResourceName, theParamName, nextAnd); - } - break; - case NUMBER: - for (List nextAnd : theAndOrParams) { - addPredicateNumber(theResourceName, theParamName, nextAnd); - } - break; - case COMPOSITE: - for (List nextAnd : theAndOrParams) { - addPredicateComposite(theResourceName, nextParamDef, nextAnd); - } - break; - case URI: - for (List nextAnd : theAndOrParams) { - addPredicateUri(theResourceName, theParamName, nextAnd); - } - break; - case HAS: - // should not happen - break; + case DATE: + for (List nextAnd : theAndOrParams) { + addPredicateDate(theResourceName, theParamName, nextAnd); + } + break; + case QUANTITY: + for (List nextAnd : theAndOrParams) { + addPredicateQuantity(theResourceName, theParamName, nextAnd); + } + break; + case REFERENCE: + for (List nextAnd : theAndOrParams) { + addPredicateReference(theResourceName, theParamName, nextAnd); + } + break; + case STRING: + for (List nextAnd : theAndOrParams) { + addPredicateString(theResourceName, theParamName, nextAnd); + } + break; + case TOKEN: + for (List nextAnd : theAndOrParams) { + addPredicateToken(theResourceName, theParamName, nextAnd); + } + break; + case NUMBER: + for (List nextAnd : theAndOrParams) { + addPredicateNumber(theResourceName, theParamName, nextAnd); + } + break; + case COMPOSITE: + for (List nextAnd : theAndOrParams) { + addPredicateComposite(theResourceName, nextParamDef, nextAnd); + } + break; + case URI: + for (List nextAnd : theAndOrParams) { + addPredicateUri(theResourceName, theParamName, nextAnd); + } + break; + case HAS: + // should not happen + break; } } else { if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { @@ -1851,35 +1904,35 @@ public class SearchBuilder implements ISearchBuilder { private IQueryParameterType toParameterType(RuntimeSearchParam theParam) { IQueryParameterType qp; switch (theParam.getParamType()) { - case DATE: - qp = new DateParam(); - break; - case NUMBER: - qp = new NumberParam(); - break; - case QUANTITY: - qp = new QuantityParam(); - break; - case STRING: - qp = new StringParam(); - break; - case TOKEN: - qp = new TokenParam(); - break; - case COMPOSITE: - List compositeOf = theParam.getCompositeOf(); - if (compositeOf.size() != 2) { - throw new InternalErrorException("Parameter " + theParam.getName() + " has " + compositeOf.size() + " composite parts. Don't know how handlt this."); - } - IQueryParameterType leftParam = toParameterType(compositeOf.get(0)); - IQueryParameterType rightParam = toParameterType(compositeOf.get(1)); - qp = new CompositeParam(leftParam, rightParam); - break; - case REFERENCE: - qp = new ReferenceParam(); - break; - default: - throw new InternalErrorException("Don't know how to convert param type: " + theParam.getParamType()); + case DATE: + qp = new DateParam(); + break; + case NUMBER: + qp = new NumberParam(); + break; + case QUANTITY: + qp = new QuantityParam(); + break; + case STRING: + qp = new StringParam(); + break; + case TOKEN: + qp = new TokenParam(); + break; + case COMPOSITE: + List compositeOf = theParam.getCompositeOf(); + if (compositeOf.size() != 2) { + throw new InternalErrorException("Parameter " + theParam.getName() + " has " + compositeOf.size() + " composite parts. Don't know how handlt this."); + } + IQueryParameterType leftParam = toParameterType(compositeOf.get(0)); + IQueryParameterType rightParam = toParameterType(compositeOf.get(1)); + qp = new CompositeParam(leftParam, rightParam); + break; + case REFERENCE: + qp = new ReferenceParam(); + break; + default: + throw new InternalErrorException("Don't know how to convert param type: " + theParam.getParamType()); } return qp; } @@ -1918,11 +1971,11 @@ public class SearchBuilder implements ISearchBuilder { if (theLastUpdated != null) { if (theLastUpdated.getLowerBoundAsInstant() != null) { ourLog.info("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant())); - Predicate predicateLower = builder.greaterThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getLowerBoundAsInstant()); + Predicate predicateLower = builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant()); lastUpdatedPredicates.add(predicateLower); } if (theLastUpdated.getUpperBoundAsInstant() != null) { - Predicate predicateUpper = builder.lessThanOrEqualTo(from. get("myUpdated"), theLastUpdated.getUpperBoundAsInstant()); + Predicate predicateUpper = builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant()); lastUpdatedPredicates.add(predicateUpper); } } @@ -1934,7 +1987,7 @@ public class SearchBuilder implements ISearchBuilder { } private static Predicate createResourceLinkPathPredicate(IDao theCallingDao, FhirContext theContext, String theParamName, From theFrom, - String theResourceType) { + String theResourceType) { RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType); RuntimeSearchParam param = theCallingDao.getSearchParamByName(resourceDef, theParamName); List path = param.getPathsSplit(); @@ -1958,10 +2011,37 @@ public class SearchBuilder implements ISearchBuilder { return resultList; } + @VisibleForTesting + public static HandlerTypeEnum getLastHandlerMechanismForUnitTest() { + ourLog.info("Retrieving last handler mechanism: {}", ourLastHandlerMechanismForUnitTest); + return ourLastHandlerMechanismForUnitTest; + } + + @VisibleForTesting + public static void resetLastHandlerMechanismForUnitTest() { + ourLog.info("Clearing last handler mechanism (was {})", ourLastHandlerMechanismForUnitTest); + ourLastHandlerMechanismForUnitTest = null; + } + static Predicate[] toArray(List thePredicates) { return thePredicates.toArray(new Predicate[thePredicates.size()]); } + public enum HandlerTypeEnum { + UNIQUE_INDEX, STANDARD_QUERY + } + + private enum JoinEnum { + DATE, + NUMBER, + QUANTITY, + REFERENCE, + STRING, + TOKEN, + URI + + } + public class IncludesIterator extends BaseIterator implements Iterator { private Iterator myCurrentIterator; @@ -2020,51 +2100,12 @@ public class SearchBuilder implements ISearchBuilder { } - private enum JoinEnum { - DATE, - NUMBER, - QUANTITY, - REFERENCE, - STRING, - TOKEN, - URI - - } - - private static class JoinKey { - private final JoinEnum myJoinType; - private final String myParamName; - - public JoinKey(String theParamName, JoinEnum theJoinType) { - super(); - myParamName = theParamName; - myJoinType = theJoinType; - } - - @Override - public boolean equals(Object theObj) { - JoinKey obj = (JoinKey) theObj; - return new EqualsBuilder() - .append(myParamName, obj.myParamName) - .append(myJoinType, obj.myJoinType) - .isEquals(); - } - - @Override - public int hashCode() { - return new HashCodeBuilder() - .append(myParamName) - .append(myJoinType) - .toHashCode(); - } - } - private final class QueryIterator extends BaseIterator implements Iterator { + private final Set myPidSet = new HashSet(); private boolean myFirst = true; private IncludesIterator myIncludesIterator; private Long myNext; - private final Set myPidSet = new HashSet(); private Iterator myPreResultsIterator; private Iterator myResultsIterator; private SortSpec mySort; @@ -2215,4 +2256,68 @@ public class SearchBuilder implements ISearchBuilder { } + private class UniqueIndexIterator implements Iterator { + private final Set myUniqueQueryStrings; + private Iterator myWrap = null; + + public UniqueIndexIterator(Set theUniqueQueryStrings) { + myUniqueQueryStrings = theUniqueQueryStrings; + } + + private void ensureHaveQuery() { + if (myWrap == null) { + ourLog.info("Searching for unique index matches over {} candidate query strings"); + StopWatch sw = new StopWatch(); + Collection resourcePids = myCallingDao.getResourceIndexedCompositeStringUniqueDao().findResourcePidsByQueryStrings(myUniqueQueryStrings); + ourLog.info("Found {} unique index matches in {}ms", resourcePids.size(), sw.getMillis()); + myWrap = resourcePids.iterator(); + } + } + + @Override + public boolean hasNext() { + ensureHaveQuery(); + return myWrap.hasNext(); + } + + @Override + public Long next() { + ensureHaveQuery(); + return myWrap.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private static class JoinKey { + private final JoinEnum myJoinType; + private final String myParamName; + + public JoinKey(String theParamName, JoinEnum theJoinType) { + super(); + myParamName = theParamName; + myJoinType = theJoinType; + } + + @Override + public boolean equals(Object theObj) { + JoinKey obj = (JoinKey) theObj; + return new EqualsBuilder() + .append(myParamName, obj.myParamName) + .append(myJoinType, obj.myJoinType) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(myParamName) + .append(myJoinType) + .toHashCode(); + } + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java index 42f673acaea..4b354cad14b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java @@ -116,13 +116,13 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen if (nextValue.isEmpty()) { continue; } - nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), nextValue.getValue(), nextValue.getValue()); + nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), nextValue.getValue(), nextValue.getValue(), nextValue.getValueAsString()); } else if (nextObject instanceof PeriodDt) { PeriodDt nextValue = (PeriodDt) nextObject; if (nextValue.isEmpty()) { continue; } - nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), nextValue.getStart(), nextValue.getEnd()); + nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), nextValue.getStart(), nextValue.getEnd(), nextValue.getStartElement().getValueAsString()); } else { if (!multiType) { throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamRegistryDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamRegistryDstu2.java index b4a94a8e5a3..8eb845e8475 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamRegistryDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamRegistryDstu2.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.dao; */ public class SearchParamRegistryDstu2 extends BaseSearchParamRegistry { - // nothing yet - + @Override + protected void refreshCacheIfNecessary() { + // nothing yet + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java index 2f7771ec0bc..cac076a0d02 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java @@ -201,7 +201,7 @@ public class SearchParameterMap extends LinkedHashMap getRevIncludes() { if (myRevIncludes == null) { - myRevIncludes = new HashSet(); + myRevIncludes = new HashSet<>(); } return myRevIncludes; } @@ -210,6 +210,22 @@ public class SearchParameterMap extends LinkedHashMap> nextParamName : values()) { + for (List nextAnd : nextParamName) { + for (IQueryParameterType nextOr : nextAnd) { + if (isNotBlank(nextOr.getQueryParameterQualifier())) { + return false; + } + } + } + } + return true; + } + /** * If set, tells the server to load these results synchronously, and not to load * more than X results diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java new file mode 100644 index 00000000000..b83877d494f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java @@ -0,0 +1,19 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.Optional; + +public interface IResourceIndexedCompositeStringUniqueDao extends JpaRepository { + + @Query("SELECT r FROM ResourceIndexedCompositeStringUnique r WHERE r.myIndexString = :str") + ResourceIndexedCompositeStringUnique findByQueryString(@Param("str") String theQueryString); + + @Query("SELECT r.myResourceId FROM ResourceIndexedCompositeStringUnique r WHERE r.myIndexString IN :str") + Collection findResourcePidsByQueryStrings(@Param("str") Collection theQueryString); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java index 85e00fcabfa..7a5388e979e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java @@ -1,6 +1,26 @@ package ca.uhn.fhir.jpa.dao.dstu3; +import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter; +import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Enumerations; +import org.hl7.fhir.dstu3.model.Meta; +import org.hl7.fhir.dstu3.model.SearchParameter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; /* * #%L @@ -11,9 +31,9 @@ import static org.apache.commons.lang3.StringUtils.isBlank; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,27 +42,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank; * #L% */ -import org.apache.commons.lang3.time.DateUtils; -import org.hl7.fhir.dstu3.model.Bundle; -import org.hl7.fhir.dstu3.model.Meta; -import org.hl7.fhir.dstu3.model.SearchParameter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor; -import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter; -import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.util.ElementUtil; - public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3 implements IFhirResourceDaoSearchParameter { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoSearchParameterDstu3.class); @@ -56,19 +55,21 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3() { - @Override - public Integer doInTransaction(TransactionStatus theStatus) { - return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType); - } - }); + TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + int updatedCount = txTemplate.execute(new TransactionCallback() { + @Override + public Integer doInTransaction(TransactionStatus theStatus) { + return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType); + } + }); - ourLog.info("Marked {} resources for reindexing", updatedCount); + ourLog.info("Marked {} resources for reindexing", updatedCount); + } } mySearchParamRegistry.forceRefresh(); @@ -126,43 +127,47 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3 dates = new TreeSet(); + String firstValue = null; + TreeSet dates = new TreeSet<>(); for (DateTimeType nextEvent : nextValue.getEvent()) { if (nextEvent.getValue() != null) { dates.add(nextEvent.getValue()); + if (firstValue == null) { + firstValue = nextEvent.getValueAsString(); + } } } if (dates.isEmpty()) { continue; } - nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), dates.first(), dates.last()); + nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), dates.first(), dates.last(), firstValue); } else if (nextObject instanceof StringType) { // CarePlan.activitydate can be a string continue; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamRegistryDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamRegistryDstu3.java index df0c8ee55d6..d2c6e5cd38a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamRegistryDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamRegistryDstu3.java @@ -26,11 +26,15 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.*; import java.util.Map.Entry; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.util.JpaConstants; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.SearchParameter; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.springframework.beans.factory.annotation.Autowired; import ca.uhn.fhir.context.RuntimeSearchParam; @@ -42,6 +46,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamRegistryDstu3.class); + public static final int MAX_MANAGED_PARAM_COUNT = 10000; private volatile Map> myActiveSearchParams; @@ -62,33 +67,33 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { @Override public Map> getActiveSearchParams() { - refreshCacheIfNeccesary(); + refreshCacheIfNecessary(); return myActiveSearchParams; } @Override public Map getActiveSearchParams(String theResourceName) { - refreshCacheIfNeccesary(); + refreshCacheIfNecessary(); return myActiveSearchParams.get(theResourceName); } private Map getSearchParamMap(Map> searchParams, String theResourceName) { Map retVal = searchParams.get(theResourceName); if (retVal == null) { - retVal = new HashMap(); + retVal = new HashMap<>(); searchParams.put(theResourceName, retVal); } return retVal; } - private void refreshCacheIfNeccesary() { + protected void refreshCacheIfNecessary() { long refreshInterval = 60 * DateUtils.MILLIS_PER_MINUTE; if (System.currentTimeMillis() - refreshInterval > myLastRefresh) { synchronized (this) { if (System.currentTimeMillis() - refreshInterval > myLastRefresh) { StopWatch sw = new StopWatch(); - Map> searchParams = new HashMap>(); + Map> searchParams = new HashMap<>(); for (Entry> nextBuiltInEntry : getBuiltInSearchParams().entrySet()) { for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) { String nextResourceName = nextBuiltInEntry.getKey(); @@ -97,40 +102,41 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { } SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronous(true); + params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT); IBundleProvider allSearchParamsBp = mySpDao.search(params); int size = allSearchParamsBp.size(); // Just in case.. - if (size > 10000) { - ourLog.warn("Unable to support >10000 search params!"); - size = 10000; + if (size > MAX_MANAGED_PARAM_COUNT) { + ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!"); + size = MAX_MANAGED_PARAM_COUNT; } List allSearchParams = allSearchParamsBp.getResources(0, size); for (IBaseResource nextResource : allSearchParams) { SearchParameter nextSp = (SearchParameter) nextResource; - RuntimeSearchParam runtimeSp = toRuntimeSp(nextSp); + JpaRuntimeSearchParam runtimeSp = toRuntimeSp(nextSp); if (runtimeSp == null) { continue; } - int dotIdx = runtimeSp.getPath().indexOf('.'); - if (dotIdx == -1) { - ourLog.warn("Can not determine resource type of {}", runtimeSp.getPath()); - continue; - } - String resourceType = runtimeSp.getPath().substring(0, dotIdx); + for (org.hl7.fhir.dstu3.model.CodeType nextBaseName : nextSp.getBase()) { + String resourceType = nextBaseName.getValue(); + if (isBlank(resourceType)) { + continue; + } + + Map searchParamMap = getSearchParamMap(searchParams, resourceType); + String name = runtimeSp.getName(); + if (myDaoConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) { + searchParamMap.put(name, runtimeSp); + } - Map searchParamMap = getSearchParamMap(searchParams, resourceType); - String name = runtimeSp.getName(); - if (myDaoConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) { - searchParamMap.put(name, runtimeSp); } } - Map> activeSearchParams = new HashMap>(); + Map> activeSearchParams = new HashMap<>(); for (Entry> nextEntry : searchParams.entrySet()) { for (RuntimeSearchParam nextSp : nextEntry.getValue().values()) { String nextName = nextSp.getName(); @@ -155,6 +161,8 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { myActiveSearchParams = activeSearchParams; + super.populateActiveSearchParams(activeSearchParams); + myLastRefresh = System.currentTimeMillis(); ourLog.info("Refreshed search parameter cache in {}ms", sw.getMillis()); } @@ -162,7 +170,7 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { } } - private RuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) { + private JpaRuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) { String name = theNextSp.getCode(); String description = theNextSp.getDescription(); String path = theNextSp.getExpression(); @@ -215,12 +223,31 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { Set targets = toStrings(theNextSp.getTarget()); if (isBlank(name) || isBlank(path) || paramType == null) { - return null; + if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { + return null; + } } IIdType id = theNextSp.getIdElement(); String uri = ""; - RuntimeSearchParam retVal = new RuntimeSearchParam(id, uri, name, description, path, paramType, null, providesMembershipInCompartments, targets, status); + boolean unique = false; + + List uniqueExts = theNextSp.getExtensionsByUrl(JpaConstants.EXT_SP_UNIQUE); + if (uniqueExts.size() > 0) { + IPrimitiveType uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); + if (uniqueExtsValuePrimitive != null) { + if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { + unique = true; + } + } + } + + List components = new ArrayList<>(); + for (SearchParameter.SearchParameterComponentComponent next : theNextSp.getComponent()) { + components.add(new JpaRuntimeSearchParam.Component(next.getExpression(), next.getDefinition())); + } + + JpaRuntimeSearchParam retVal = new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, theNextSp.getBase()); return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java index 58d1b1156fb..d76ea7a5140 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao.r4; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; /* * #%L @@ -24,6 +25,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.SearchParameter; import org.springframework.beans.factory.annotation.Autowired; @@ -56,19 +58,21 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4() { - @Override - public Integer doInTransaction(TransactionStatus theStatus) { - return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType); - } - }); + TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + int updatedCount = txTemplate.execute(new TransactionCallback() { + @Override + public Integer doInTransaction(TransactionStatus theStatus) { + return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType); + } + }); - ourLog.info("Marked {} resources for reindexing", updatedCount); + ourLog.info("Marked {} resources for reindexing", updatedCount); + } } mySearchParamRegistry.forceRefresh(); @@ -125,44 +129,52 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4 dates = new TreeSet(); + TreeSet dates = new TreeSet<>(); + String firstValue = null; for (DateTimeType nextEvent : nextValue.getEvent()) { if (nextEvent.getValue() != null) { dates.add(nextEvent.getValue()); + if (firstValue == null) { + firstValue = nextEvent.getValueAsString(); + } } } if (dates.isEmpty()) { continue; } - nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), dates.first(), dates.last()); + nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), dates.first(), dates.last(), firstValue); } else if (nextObject instanceof StringType) { // CarePlan.activitydate can be a string continue; @@ -450,7 +454,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements */ @Override public Set extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); String useSystem = null; if (theResource instanceof CodeSystem) { @@ -477,16 +481,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements List systems = new ArrayList(); List codes = new ArrayList(); - // String needContactPointSystem = null; - // if (nextPath.contains(".where(system='phone')")) { - // nextPath = nextPath.replace(".where(system='phone')", ""); - // needContactPointSystem = "phone"; - // } - // if (nextPath.contains(".where(system='email')")) { - // nextPath = nextPath.replace(".where(system='email')", ""); - // needContactPointSystem = "email"; - // } - for (Object nextObject : extractValues(nextPath, theResource)) { if (nextObject == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamRegistryR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamRegistryR4.java index 978aaf243cc..e82e575423a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamRegistryR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamRegistryR4.java @@ -20,14 +20,19 @@ package ca.uhn.fhir.jpa.dao.r4; * #L% */ +import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.*; import java.util.Map.Entry; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.util.JpaConstants; import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -42,6 +47,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; public class SearchParamRegistryR4 extends BaseSearchParamRegistry { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamRegistryR4.class); + public static final int MAX_MANAGED_PARAM_COUNT = 10000; private volatile Map> myActiveSearchParams; @@ -62,33 +68,33 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry { @Override public Map> getActiveSearchParams() { - refreshCacheIfNeccesary(); + refreshCacheIfNecessary(); return myActiveSearchParams; } @Override public Map getActiveSearchParams(String theResourceName) { - refreshCacheIfNeccesary(); + refreshCacheIfNecessary(); return myActiveSearchParams.get(theResourceName); } private Map getSearchParamMap(Map> searchParams, String theResourceName) { Map retVal = searchParams.get(theResourceName); if (retVal == null) { - retVal = new HashMap(); + retVal = new HashMap<>(); searchParams.put(theResourceName, retVal); } return retVal; } - private void refreshCacheIfNeccesary() { + protected void refreshCacheIfNecessary() { long refreshInterval = 60 * DateUtils.MILLIS_PER_MINUTE; if (System.currentTimeMillis() - refreshInterval > myLastRefresh) { synchronized (this) { if (System.currentTimeMillis() - refreshInterval > myLastRefresh) { StopWatch sw = new StopWatch(); - Map> searchParams = new HashMap>(); + Map> searchParams = new HashMap<>(); for (Entry> nextBuiltInEntry : getBuiltInSearchParams().entrySet()) { for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) { String nextResourceName = nextBuiltInEntry.getKey(); @@ -97,15 +103,15 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry { } SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronous(true); + params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT); IBundleProvider allSearchParamsBp = mySpDao.search(params); int size = allSearchParamsBp.size(); // Just in case.. - if (size > 10000) { - ourLog.warn("Unable to support >10000 search params!"); - size = 10000; + if (size >= MAX_MANAGED_PARAM_COUNT) { + ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!"); + size = MAX_MANAGED_PARAM_COUNT; } List allSearchParams = allSearchParamsBp.getResources(0, size); @@ -116,21 +122,22 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry { continue; } - int dotIdx = runtimeSp.getPath().indexOf('.'); - if (dotIdx == -1) { - ourLog.warn("Can not determine resource type of {}", runtimeSp.getPath()); - continue; - } - String resourceType = runtimeSp.getPath().substring(0, dotIdx); + for (CodeType nextBaseName : nextSp.getBase()) { + String resourceType = nextBaseName.getValue(); + if (isBlank(resourceType)) { + continue; + } + + Map searchParamMap = getSearchParamMap(searchParams, resourceType); + String name = runtimeSp.getName(); + if (myDaoConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) { + searchParamMap.put(name, runtimeSp); + } - Map searchParamMap = getSearchParamMap(searchParams, resourceType); - String name = runtimeSp.getName(); - if (myDaoConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) { - searchParamMap.put(name, runtimeSp); } } - Map> activeSearchParams = new HashMap>(); + Map> activeSearchParams = new HashMap<>(); for (Entry> nextEntry : searchParams.entrySet()) { for (RuntimeSearchParam nextSp : nextEntry.getValue().values()) { String nextName = nextSp.getName(); @@ -155,6 +162,8 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry { myActiveSearchParams = activeSearchParams; + super.populateActiveSearchParams(activeSearchParams); + myLastRefresh = System.currentTimeMillis(); ourLog.info("Refreshed search parameter cache in {}ms", sw.getMillis()); } @@ -215,17 +224,36 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry { Set targets = toStrings(theNextSp.getTarget()); if (isBlank(name) || isBlank(path) || paramType == null) { - return null; + if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { + return null; + } } IIdType id = theNextSp.getIdElement(); String uri = ""; - RuntimeSearchParam retVal = new RuntimeSearchParam(id, uri, name, description, path, paramType, null, providesMembershipInCompartments, targets, status); + boolean unique = false; + + List uniqueExts = theNextSp.getExtensionsByUrl(JpaConstants.EXT_SP_UNIQUE); + if (uniqueExts.size() > 0) { + IPrimitiveType uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); + if (uniqueExtsValuePrimitive != null) { + if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { + unique = true; + } + } + } + + List components = new ArrayList<>(); + for (org.hl7.fhir.r4.model.SearchParameter.SearchParameterComponentComponent next : theNextSp.getComponent()) { + components.add(new JpaRuntimeSearchParam.Component(next.getExpression(), next.getDefinition())); + } + + RuntimeSearchParam retVal = new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, theNextSp.getBase()); return retVal; } private Set toStrings(List theTarget) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); for (CodeType next : theTarget) { if (isNotBlank(next.getValue())) { retVal.add(next.getValue()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java index 5858e720514..3bb9a0fa442 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java @@ -20,14 +20,14 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.io.Serializable; -import java.util.Date; - -import javax.persistence.*; - +import ca.uhn.fhir.model.api.IQueryParameterType; import org.hibernate.search.annotations.ContainedIn; import org.hibernate.search.annotations.Field; +import javax.persistence.*; +import java.io.Serializable; +import java.util.Date; + @MappedSuperclass public abstract class BaseResourceIndexedSearchParam implements Serializable { @@ -67,10 +67,19 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { return myParamName; } + public void setParamName(String theName) { + myParamName = theName; + } + public ResourceTable getResource() { return myResource; } + public void setResource(ResourceTable theResource) { + myResource = theResource; + myResourceType = theResource.getResourceType(); + } + public Long getResourcePid() { return myResourcePid; } @@ -83,6 +92,10 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { return myUpdated; } + public void setUpdated(Date theUpdated) { + myUpdated = theUpdated; + } + public boolean isMissing() { return Boolean.TRUE.equals(myMissing); } @@ -91,17 +104,5 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { myMissing = theMissing; } - public void setParamName(String theName) { - myParamName = theName; - } - - public void setResource(ResourceTable theResource) { - myResource = theResource; - myResourceType = theResource.getResourceType(); - } - - public void setUpdated(Date theUpdated) { - myUpdated = theUpdated; - } - + public abstract IQueryParameterType toQueryParameterType(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedCompositeStringUnique.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedCompositeStringUnique.java new file mode 100644 index 00000000000..76bf821848e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedCompositeStringUnique.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.jpa.entity; + +import org.apache.commons.lang3.builder.*; +import org.hl7.fhir.r4.model.Resource; + +import javax.persistence.*; + +@Entity() +@Table(name = "HFJ_IDX_CMP_STRING_UNIQ", indexes = { + @Index(name = ResourceIndexedCompositeStringUnique.IDX_IDXCMPSTRUNIQ_STRING, columnList = "IDX_STRING", unique = true) +}) +public class ResourceIndexedCompositeStringUnique implements Comparable { + + public static final int MAX_STRING_LENGTH = 800; + public static final String IDX_IDXCMPSTRUNIQ_STRING = "IDX_IDXCMPSTRUNIQ_STRING"; + + @SequenceGenerator(name = "SEQ_IDXCMPSTRUNIQ_ID", sequenceName = "SEQ_IDXCMPSTRUNIQ_ID") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_IDXCMPSTRUNIQ_ID") + @Id + @Column(name = "PID") + private Long myId; + + @ManyToOne + @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID") + private ResourceTable myResource; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("resourceId", myResourceId) + .append("indexString", myIndexString) + .toString(); + } + + @Column(name="RES_ID", insertable = false, updatable = false) + private Long myResourceId; + + @Column(name = "IDX_STRING", nullable = false, length = MAX_STRING_LENGTH) + private String myIndexString; + + /** + * Constructor + */ + public ResourceIndexedCompositeStringUnique() { + super(); + } + + /** + * Constructor + */ + public ResourceIndexedCompositeStringUnique(ResourceTable theResource, String theIndexString) { + setResource(theResource); + setIndexString(theIndexString); + } + + @Override + public int compareTo(ResourceIndexedCompositeStringUnique theO) { + CompareToBuilder b = new CompareToBuilder(); + b.append(myResource, theO.getResource()); + b.append(myIndexString, theO.getIndexString()); + return b.toComparison(); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + + if (theO == null || getClass() != theO.getClass()) return false; + + ResourceIndexedCompositeStringUnique that = (ResourceIndexedCompositeStringUnique) theO; + + return new EqualsBuilder() + .append(getResource(), that.getResource()) + .append(myIndexString, that.myIndexString) + .isEquals(); + } + + public String getIndexString() { + return myIndexString; + } + + public void setIndexString(String theIndexString) { + myIndexString = theIndexString; + } + + public ResourceTable getResource() { + return myResource; + } + + public void setResource(ResourceTable theResource) { + myResource = theResource; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(getResource()) + .append(myIndexString) + .toHashCode(); + } +} 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 f322d52f4e0..f804749897c 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 @@ -30,6 +30,8 @@ import javax.persistence.Index; import javax.persistence.SequenceGenerator; import javax.persistence.Table; +import ca.uhn.fhir.model.api.IQueryParameterType; +import com.sun.prism.image.Coords; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -99,6 +101,11 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP return myId; } + @Override + public IQueryParameterType toQueryParameterType() { + return null; + } + public double getLatitude() { return myLatitude; } 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 481fa2794d4..cbfa191ff95 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 @@ -20,54 +20,48 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.util.Date; - -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.rest.param.DateParam; import org.apache.commons.lang3.builder.EqualsBuilder; 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.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.InstantType; -import ca.uhn.fhir.model.primitive.InstantDt; +import javax.persistence.*; +import java.util.Date; @Embeddable @Entity -@Table(name = "HFJ_SPIDX_DATE", indexes= { +@Table(name = "HFJ_SPIDX_DATE", indexes = { @Index(name = "IDX_SP_DATE", columnList = "RES_TYPE,SP_NAME,SP_VALUE_LOW,SP_VALUE_HIGH"), - @Index(name = "IDX_SP_DATE_UPDATED", columnList = "SP_UPDATED"), - @Index(name = "IDX_SP_DATE_RESID", columnList = "RES_ID") + @Index(name = "IDX_SP_DATE_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_DATE_RESID", columnList = "RES_ID") }) public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchParam { private static final long serialVersionUID = 1L; - @Id - @SequenceGenerator(name = "SEQ_SPIDX_DATE", sequenceName = "SEQ_SPIDX_DATE") - @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_DATE") - @Column(name = "SP_ID") - private Long myId; + @Transient + private transient String myOriginalValue; @Column(name = "SP_VALUE_HIGH", nullable = true) @Temporal(TemporalType.TIMESTAMP) @Field public Date myValueHigh; - @Column(name = "SP_VALUE_LOW", nullable = true) @Temporal(TemporalType.TIMESTAMP) @Field public Date myValueLow; + @Id + @SequenceGenerator(name = "SEQ_SPIDX_DATE", sequenceName = "SEQ_SPIDX_DATE") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_DATE") + @Column(name = "SP_ID") + private Long myId; /** * Constructor @@ -78,10 +72,11 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar /** * Constructor */ - public ResourceIndexedSearchParamDate(String theName, Date theLow, Date theHigh) { + public ResourceIndexedSearchParamDate(String theName, Date theLow, Date theHigh, String theOriginalValue) { setParamName(theName); setValueLow(theLow); setValueHigh(theHigh); + myOriginalValue = theOriginalValue; } @Override @@ -113,10 +108,18 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar return myValueHigh; } + public void setValueHigh(Date theValueHigh) { + myValueHigh = theValueHigh; + } + public Date getValueLow() { return myValueLow; } + public void setValueLow(Date theValueLow) { + myValueLow = theValueLow; + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); @@ -127,12 +130,13 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar return b.toHashCode(); } - public void setValueHigh(Date theValueHigh) { - myValueHigh = theValueHigh; - } - - public void setValueLow(Date theValueLow) { - myValueLow = theValueLow; + @Override + public IQueryParameterType toQueryParameterType() { + DateTimeType value = new DateTimeType(myOriginalValue); + if (value.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) { + value.setTimeZoneZulu(true); + } + return new DateParam(value.getValueAsString()); } @Override 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 6a208943ba1..9168077dbdb 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 @@ -20,18 +20,9 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.math.BigDecimal; - -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; - +import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.NumberParam; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -40,32 +31,31 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.NumericField; -import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; +import javax.persistence.*; +import java.math.BigDecimal; //@formatter:off @Embeddable @Entity -@Table(name = "HFJ_SPIDX_NUMBER", indexes= { +@Table(name = "HFJ_SPIDX_NUMBER", indexes = { @Index(name = "IDX_SP_NUMBER", columnList = "RES_TYPE,SP_NAME,SP_VALUE"), - @Index(name = "IDX_SP_NUMBER_UPDATED", columnList = "SP_UPDATED"), - @Index(name = "IDX_SP_NUMBER_RESID", columnList = "RES_ID") + @Index(name = "IDX_SP_NUMBER_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_NUMBER_RESID", columnList = "RES_ID") }) //@formatter:on public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchParam { private static final long serialVersionUID = 1L; - - @Id - @SequenceGenerator(name = "SEQ_SPIDX_NUMBER", sequenceName = "SEQ_SPIDX_NUMBER") - @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_NUMBER") - @Column(name = "SP_ID") - private Long myId; - @Column(name = "SP_VALUE", nullable = true) @Field @NumericField @FieldBridge(impl = BigDecimalNumericFieldBridge.class) public BigDecimal myValue; + @Id + @SequenceGenerator(name = "SEQ_SPIDX_NUMBER", sequenceName = "SEQ_SPIDX_NUMBER") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_NUMBER") + @Column(name = "SP_ID") + private Long myId; public ResourceIndexedSearchParamNumber() { } @@ -103,6 +93,10 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP return myValue; } + public void setValue(BigDecimal theValue) { + myValue = theValue; + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); @@ -112,8 +106,9 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP return b.toHashCode(); } - public void setValue(BigDecimal theValue) { - myValue = theValue; + @Override + public IQueryParameterType toQueryParameterType() { + return new NumberParam(myValue.toPlainString()); } @Override 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 01e89853541..aafc52dbc26 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 @@ -20,18 +20,9 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.math.BigDecimal; - -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; - +import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.QuantityParam; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -40,15 +31,16 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.NumericField; -import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; +import javax.persistence.*; +import java.math.BigDecimal; //@formatter:off @Embeddable @Entity @Table(name = "HFJ_SPIDX_QUANTITY", indexes = { @Index(name = "IDX_SP_QUANTITY", columnList = "RES_TYPE,SP_NAME,SP_SYSTEM,SP_UNITS,SP_VALUE"), - @Index(name = "IDX_SP_QUANTITY_UPDATED", columnList = "SP_UPDATED"), - @Index(name = "IDX_SP_QUANTITY_RESID", columnList = "RES_ID") + @Index(name = "IDX_SP_QUANTITY_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_QUANTITY_RESID", columnList = "RES_ID") }) //@formatter:on public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearchParam { @@ -56,26 +48,22 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc private static final int MAX_LENGTH = 200; private static final long serialVersionUID = 1L; - - @Id - @SequenceGenerator(name = "SEQ_SPIDX_QUANTITY", sequenceName = "SEQ_SPIDX_QUANTITY") - @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_QUANTITY") - @Column(name = "SP_ID") - private Long myId; - @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) @Field public String mySystem; - @Column(name = "SP_UNITS", nullable = true, length = MAX_LENGTH) @Field public String myUnits; - @Column(name = "SP_VALUE", nullable = true) @Field @NumericField @FieldBridge(impl = BigDecimalNumericFieldBridge.class) public BigDecimal myValue; + @Id + @SequenceGenerator(name = "SEQ_SPIDX_QUANTITY", sequenceName = "SEQ_SPIDX_QUANTITY") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_QUANTITY") + @Column(name = "SP_ID") + private Long myId; public ResourceIndexedSearchParamQuantity() { // nothing @@ -118,14 +106,26 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc return mySystem; } + public void setSystem(String theSystem) { + mySystem = theSystem; + } + public String getUnits() { return myUnits; } + public void setUnits(String theUnits) { + myUnits = theUnits; + } + public BigDecimal getValue() { return myValue; } + public void setValue(BigDecimal theValue) { + myValue = theValue; + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); @@ -137,16 +137,9 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc return b.toHashCode(); } - public void setSystem(String theSystem) { - mySystem = theSystem; - } - - public void setUnits(String theUnits) { - myUnits = theUnits; - } - - public void setValue(BigDecimal theValue) { - myValue = theValue; + @Override + public IQueryParameterType toQueryParameterType() { + return new QuantityParam(null, getValue(), getSystem(), getUnits()); } @Override 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 6c735bd91ff..e7e345bf26c 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 @@ -20,39 +20,25 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.Entity; -import javax.persistence.ForeignKey; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; - +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.StringParam; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; 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.Analyze; -import org.hibernate.search.annotations.Analyzer; -import org.hibernate.search.annotations.ContainedIn; -import org.hibernate.search.annotations.Field; -import org.hibernate.search.annotations.Fields; -import org.hibernate.search.annotations.Indexed; -import org.hibernate.search.annotations.Store; +import org.hibernate.search.annotations.*; + +import javax.persistence.*; +import javax.persistence.Index; //@formatter:off @Embeddable @Entity -@Table(name = "HFJ_SPIDX_STRING", indexes = { - @Index(name = "IDX_SP_STRING", columnList = "RES_TYPE,SP_NAME,SP_VALUE_NORMALIZED"), - @Index(name = "IDX_SP_STRING_UPDATED", columnList = "SP_UPDATED"), - @Index(name = "IDX_SP_STRING_RESID", columnList = "RES_ID") +@Table(name = "HFJ_SPIDX_STRING", indexes = { + @Index(name = "IDX_SP_STRING", columnList = "RES_TYPE,SP_NAME,SP_VALUE_NORMALIZED"), + @Index(name = "IDX_SP_STRING_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_STRING_RESID", columnList = "RES_ID") }) @Indexed() //@AnalyzerDefs({ @@ -109,13 +95,13 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP private static final long serialVersionUID = 1L; @Id - @SequenceGenerator(name="SEQ_SPIDX_STRING", sequenceName="SEQ_SPIDX_STRING") + @SequenceGenerator(name = "SEQ_SPIDX_STRING", sequenceName = "SEQ_SPIDX_STRING") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_STRING") @Column(name = "SP_ID") private Long myId; @ManyToOne(optional = false) - @JoinColumn(name = "RES_ID", referencedColumnName="RES_ID", insertable=false, updatable=false, foreignKey=@ForeignKey(name="FK_SPIDXSTR_RESOURCE")) + @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_SPIDXSTR_RESOURCE")) @ContainedIn private ResourceTable myResourceTable; @@ -169,10 +155,24 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return myValueExact; } + public void setValueExact(String theValueExact) { + if (StringUtils.defaultString(theValueExact).length() > MAX_LENGTH) { + throw new IllegalArgumentException("Value is too long: " + theValueExact.length()); + } + myValueExact = theValueExact; + } + public String getValueNormalized() { return myValueNormalized; } + public void setValueNormalized(String theValueNormalized) { + if (StringUtils.defaultString(theValueNormalized).length() > MAX_LENGTH) { + throw new IllegalArgumentException("Value is too long: " + theValueNormalized.length()); + } + myValueNormalized = theValueNormalized; + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); @@ -182,18 +182,9 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return b.toHashCode(); } - public void setValueExact(String theValueExact) { - if (StringUtils.defaultString(theValueExact).length() > MAX_LENGTH) { - throw new IllegalArgumentException("Value is too long: " + theValueExact.length()); - } - myValueExact = theValueExact; - } - - public void setValueNormalized(String theValueNormalized) { - if (StringUtils.defaultString(theValueNormalized).length() > MAX_LENGTH) { - throw new IllegalArgumentException("Value is too long: " + theValueNormalized.length()); - } - myValueNormalized = theValueNormalized; + @Override + public IQueryParameterType toQueryParameterType() { + return new StringParam(getValueExact()); } @Override 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 f61d0618c1b..9ac5db3c141 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 @@ -20,16 +20,8 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; - +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.TokenParam; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; @@ -37,14 +29,16 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; +import javax.persistence.*; + //@formatter:off @Embeddable @Entity @Table(name = "HFJ_SPIDX_TOKEN", indexes = { @Index(name = "IDX_SP_TOKEN", columnList = "RES_TYPE,SP_NAME,SP_SYSTEM,SP_VALUE"), @Index(name = "IDX_SP_TOKEN_UNQUAL", columnList = "RES_TYPE,SP_NAME,SP_VALUE"), - @Index(name = "IDX_SP_TOKEN_UPDATED", columnList = "SP_UPDATED"), - @Index(name = "IDX_SP_TOKEN_RESID", columnList = "RES_ID") + @Index(name = "IDX_SP_TOKEN_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_TOKEN_RESID", columnList = "RES_ID") }) //@formatter:on public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam { @@ -52,21 +46,18 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa public static final int MAX_LENGTH = 200; private static final long serialVersionUID = 1L; - + @Field() + @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) + public String mySystem; + @Field() + @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH) + public String myValue; @Id @SequenceGenerator(name = "SEQ_SPIDX_TOKEN", sequenceName = "SEQ_SPIDX_TOKEN") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN") @Column(name = "SP_ID") private Long myId; - @Field() - @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) - public String mySystem; - - @Field() - @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH) - public String myValue; - public ResourceIndexedSearchParamToken() { } @@ -105,10 +96,18 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa return mySystem; } + public void setSystem(String theSystem) { + mySystem = StringUtils.defaultIfBlank(theSystem, null); + } + public String getValue() { return myValue; } + public void setValue(String theValue) { + myValue = StringUtils.defaultIfBlank(theValue, null); + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); @@ -119,12 +118,9 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa return b.toHashCode(); } - public void setSystem(String theSystem) { - mySystem = StringUtils.defaultIfBlank(theSystem, null); - } - - public void setValue(String theValue) { - myValue = StringUtils.defaultIfBlank(theValue, null); + @Override + public IQueryParameterType toQueryParameterType() { + return new TokenParam(getSystem(), getValue()); } @Override 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 d4d9cf155f0..e6ae403ff8e 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 @@ -20,30 +20,24 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; - +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.UriParam; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.hibernate.search.annotations.Field; +import javax.persistence.*; + //@formatter:off @Embeddable @Entity -@Table(name = "HFJ_SPIDX_URI", indexes = { - @Index(name = "IDX_SP_URI", columnList = "RES_TYPE,SP_NAME,SP_URI"), - @Index(name = "IDX_SP_URI_RESTYPE_NAME", columnList = "RES_TYPE,SP_NAME"), - @Index(name = "IDX_SP_URI_UPDATED", columnList = "SP_UPDATED"), - @Index(name = "IDX_SP_URI_COORDS", columnList = "RES_ID") +@Table(name = "HFJ_SPIDX_URI", indexes = { + @Index(name = "IDX_SP_URI", columnList = "RES_TYPE,SP_NAME,SP_URI"), + @Index(name = "IDX_SP_URI_RESTYPE_NAME", columnList = "RES_TYPE,SP_NAME"), + @Index(name = "IDX_SP_URI_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_URI_COORDS", columnList = "RES_ID") }) //@formatter:on public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchParam { @@ -54,16 +48,14 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara public static final int MAX_LENGTH = 255; private static final long serialVersionUID = 1L; - - @Id - @SequenceGenerator(name="SEQ_SPIDX_URI", sequenceName="SEQ_SPIDX_URI") - @GeneratedValue(strategy = GenerationType.AUTO, generator="SEQ_SPIDX_URI") - @Column(name = "SP_ID") - private Long myId; - @Column(name = "SP_URI", nullable = true, length = MAX_LENGTH) @Field() public String myUri; + @Id + @SequenceGenerator(name = "SEQ_SPIDX_URI", sequenceName = "SEQ_SPIDX_URI") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_URI") + @Column(name = "SP_ID") + private Long myId; public ResourceIndexedSearchParamUri() { } @@ -101,6 +93,10 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara return myUri; } + public void setUri(String theUri) { + myUri = StringUtils.defaultIfBlank(theUri, null); + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); @@ -110,8 +106,9 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara return b.toHashCode(); } - public void setUri(String theUri) { - myUri = StringUtils.defaultIfBlank(theUri, null); + @Override + public IQueryParameterType toQueryParameterType() { + return new UriParam(getUri()); } @Override 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 65bd1d06717..8b7c7a4452d 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 @@ -19,27 +19,11 @@ package ca.uhn.fhir.jpa.entity; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.defaultString; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.OneToMany; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; -import javax.persistence.Transient; +import ca.uhn.fhir.jpa.search.IndexNonDeletedInterceptor; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.lucene.analysis.core.LowerCaseFilterFactory; @@ -52,58 +36,53 @@ import org.apache.lucene.analysis.phonetic.PhoneticFilterFactory; import org.apache.lucene.analysis.snowball.SnowballPorterFilterFactory; import org.apache.lucene.analysis.standard.StandardFilterFactory; import org.apache.lucene.analysis.standard.StandardTokenizerFactory; -import org.hibernate.search.annotations.Analyze; -import org.hibernate.search.annotations.Analyzer; -import org.hibernate.search.annotations.AnalyzerDef; -import org.hibernate.search.annotations.AnalyzerDefs; -import org.hibernate.search.annotations.Field; -import org.hibernate.search.annotations.Fields; -import org.hibernate.search.annotations.Indexed; -import org.hibernate.search.annotations.IndexedEmbedded; +import org.hibernate.search.annotations.*; import org.hibernate.search.annotations.Parameter; -import org.hibernate.search.annotations.Store; -import org.hibernate.search.annotations.TokenFilterDef; -import org.hibernate.search.annotations.TokenizerDef; -import ca.uhn.fhir.jpa.search.IndexNonDeletedInterceptor; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import javax.persistence.*; +import javax.persistence.Index; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.defaultString; //@formatter:off -@Indexed(interceptor=IndexNonDeletedInterceptor.class) +@Indexed(interceptor = IndexNonDeletedInterceptor.class) @Entity -@Table(name = "HFJ_RESOURCE", uniqueConstraints = {}, indexes= { - @Index(name = "IDX_RES_DATE", columnList="RES_UPDATED"), - @Index(name = "IDX_RES_LANG", columnList="RES_TYPE,RES_LANGUAGE"), - @Index(name = "IDX_RES_PROFILE", columnList="RES_PROFILE"), - @Index(name = "IDX_RES_TYPE", columnList="RES_TYPE"), - @Index(name = "IDX_INDEXSTATUS", columnList="SP_INDEX_STATUS") +@Table(name = "HFJ_RESOURCE", uniqueConstraints = {}, indexes = { + @Index(name = "IDX_RES_DATE", columnList = "RES_UPDATED"), + @Index(name = "IDX_RES_LANG", columnList = "RES_TYPE,RES_LANGUAGE"), + @Index(name = "IDX_RES_PROFILE", columnList = "RES_PROFILE"), + @Index(name = "IDX_RES_TYPE", columnList = "RES_TYPE"), + @Index(name = "IDX_INDEXSTATUS", columnList = "SP_INDEX_STATUS") }) @AnalyzerDefs({ @AnalyzerDef(name = "autocompleteEdgeAnalyzer", - tokenizer = @TokenizerDef(factory = PatternTokenizerFactory.class, params= { - @Parameter(name="pattern", value="(.*)"), - @Parameter(name="group", value="1") + tokenizer = @TokenizerDef(factory = PatternTokenizerFactory.class, params = { + @Parameter(name = "pattern", value = "(.*)"), + @Parameter(name = "group", value = "1") }), filters = { @TokenFilterDef(factory = LowerCaseFilterFactory.class), @TokenFilterDef(factory = StopFilterFactory.class), @TokenFilterDef(factory = EdgeNGramFilterFactory.class, params = { @Parameter(name = "minGramSize", value = "3"), - @Parameter(name = "maxGramSize", value = "50") - }), + @Parameter(name = "maxGramSize", value = "50") + }), }), @AnalyzerDef(name = "autocompletePhoneticAnalyzer", - tokenizer = @TokenizerDef(factory=StandardTokenizerFactory.class), + tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class), filters = { - @TokenFilterDef(factory=StandardFilterFactory.class), - @TokenFilterDef(factory=StopFilterFactory.class), - @TokenFilterDef(factory=PhoneticFilterFactory.class, params = { - @Parameter(name="encoder", value="DoubleMetaphone") + @TokenFilterDef(factory = StandardFilterFactory.class), + @TokenFilterDef(factory = StopFilterFactory.class), + @TokenFilterDef(factory = PhoneticFilterFactory.class, params = { + @Parameter(name = "encoder", value = "DoubleMetaphone") }), - @TokenFilterDef(factory=SnowballPorterFilterFactory.class, params = { - @Parameter(name="language", value="English") + @TokenFilterDef(factory = SnowballPorterFilterFactory.class, params = { + @Parameter(name = "language", value = "English") }) }), @AnalyzerDef(name = "autocompleteNGramAnalyzer", @@ -113,7 +92,7 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; @TokenFilterDef(factory = LowerCaseFilterFactory.class), @TokenFilterDef(factory = NGramFilterFactory.class, params = { @Parameter(name = "minGramSize", value = "3"), - @Parameter(name = "maxGramSize", value = "20") + @Parameter(name = "maxGramSize", value = "20") }), }), @AnalyzerDef(name = "standardAnalyzer", @@ -125,21 +104,18 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class), filters = { }) - } +} ) //@formatter:on public class ResourceTable extends BaseHasResource implements Serializable { + static final int RESTYPE_LEN = 30; private static final int MAX_LANGUAGE_LENGTH = 20; private static final int MAX_PROFILE_LENGTH = 200; - - static final int RESTYPE_LEN = 30; - private static final long serialVersionUID = 1L; /** * Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB */ - //@formatter:off @Transient() @Fields({ @Field(name = "myContentText", index = org.hibernate.search.annotations.Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = "standardAnalyzer")), @@ -147,10 +123,9 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Field(name = "myContentTextNGram", index = org.hibernate.search.annotations.Index.YES, store = Store.NO, analyze = Analyze.YES, analyzer = @Analyzer(definition = "autocompleteNGramAnalyzer")), @Field(name = "myContentTextPhonetic", index = org.hibernate.search.annotations.Index.YES, store = Store.NO, analyze = Analyze.YES, analyzer = @Analyzer(definition = "autocompletePhoneticAnalyzer")) }) - //@formatter:on private String myContentText; - @Column(name = "HASH_SHA256", length=64, nullable=true) + @Column(name = "HASH_SHA256", length = 64, nullable = true) private String myHashSha256; @Column(name = "SP_HAS_LINKS") @@ -227,24 +202,22 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Column(name = "RES_PROFILE", length = MAX_PROFILE_LENGTH, nullable = true) private String myProfile; - + @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + private Collection myParamsCompositeStringUnique; + @Column(name = "SP_CMPSTR_UNIQ_PRESENT") + private boolean myParamsCompositeStringUniquePresent; @OneToMany(mappedBy = "mySourceResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) @IndexedEmbedded() 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) private Collection mySearchParamPresents; - @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) private Set myTags; - @Transient private transient boolean myUnchangedInCurrentOperation; - @Column(name = "RES_VER") private long myVersion; @@ -264,11 +237,19 @@ public class ResourceTable extends BaseHasResource implements Serializable { return myHashSha256; } + public void setHashSha256(String theHashSha256) { + myHashSha256 = theHashSha256; + } + @Override public Long getId() { return myId; } + public void setId(Long theId) { + myId = theId; + } + @Override public IdDt getIdDt() { if (getForcedId() == null) { @@ -283,157 +264,14 @@ public class ResourceTable extends BaseHasResource implements Serializable { return myIndexStatus; } - public String getLanguage() { - return myLanguage; - } - - public Collection getParamsCoords() { - if (myParamsCoords == null) { - myParamsCoords = new ArrayList(); - } - return myParamsCoords; - } - - public Collection getParamsDate() { - if (myParamsDate == null) { - myParamsDate = new ArrayList(); - } - return myParamsDate; - } - - public Collection getParamsNumber() { - if (myParamsNumber == null) { - myParamsNumber = new ArrayList(); - } - return myParamsNumber; - } - - public Collection getParamsQuantity() { - if (myParamsQuantity == null) { - myParamsQuantity = new ArrayList(); - } - return myParamsQuantity; - } - - public Collection getParamsString() { - if (myParamsString == null) { - myParamsString = new ArrayList(); - } - return myParamsString; - } - - public Collection getParamsToken() { - if (myParamsToken == null) { - myParamsToken = new ArrayList(); - } - return myParamsToken; - } - - public Collection getParamsUri() { - if (myParamsUri == null) { - myParamsUri = new ArrayList(); - } - return myParamsUri; - } - - public String getProfile() { - return myProfile; - } - - public Collection getResourceLinks() { - if (myResourceLinks == null) { - myResourceLinks = new ArrayList(); - } - return myResourceLinks; - } - - @Override - public String getResourceType() { - return myResourceType; - } - - @Override - public Collection getTags() { - if (myTags == null) { - myTags = new HashSet(); - } - return myTags; - } - - @Override - public long getVersion() { - return myVersion; - } - - public boolean hasTag(System theSystem, String theTerm) { - for (ResourceTag next : getTags()) { - if (next.getTag().getSystem().equals(theSystem) && next.getTag().getCode().equals(theTerm)) { - return true; - } - } - return false; - } - - public boolean isHasLinks() { - return myHasLinks; - } - - public boolean isParamsCoordsPopulated() { - return myParamsCoordsPopulated; - } - - public boolean isParamsDatePopulated() { - return myParamsDatePopulated; - } - - public boolean isParamsNumberPopulated() { - return myParamsNumberPopulated; - } - - public boolean isParamsQuantityPopulated() { - return myParamsQuantityPopulated; - } - - public boolean isParamsStringPopulated() { - return myParamsStringPopulated; - } - - public boolean isParamsTokenPopulated() { - return myParamsTokenPopulated; - } - - public boolean isParamsUriPopulated() { - return myParamsUriPopulated; - } - - /** - * Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation - * and was not re-saved in the database - */ - public boolean isUnchangedInCurrentOperation() { - return myUnchangedInCurrentOperation; - } - - public void setContentTextParsedIntoWords(String theContentText) { - myContentText = theContentText; - } - - public void setHashSha256(String theHashSha256) { - myHashSha256 = theHashSha256; - } - - public void setHasLinks(boolean theHasLinks) { - myHasLinks = theHasLinks; - } - - public void setId(Long theId) { - myId = theId; - } - public void setIndexStatus(Long theIndexStatus) { myIndexStatus = theIndexStatus; } + public String getLanguage() { + return myLanguage; + } + public void setLanguage(String theLanguage) { if (defaultString(theLanguage).length() > MAX_LANGUAGE_LENGTH) { throw new UnprocessableEntityException("Language exceeds maximum length of " + MAX_LANGUAGE_LENGTH + " chars: " + theLanguage); @@ -441,8 +279,22 @@ public class ResourceTable extends BaseHasResource implements Serializable { myLanguage = theLanguage; } - public void setNarrativeTextParsedIntoWords(String theNarrativeText) { - myNarrativeText = theNarrativeText; + public Collection getParamsCompositeStringUnique() { + if (myParamsCompositeStringUnique == null) { + myParamsCompositeStringUnique = new ArrayList<>(); + } + return myParamsCompositeStringUnique; + } + + public void setParamsCompositeStringUnique(Collection theParamsCompositeStringUnique) { + myParamsCompositeStringUnique = theParamsCompositeStringUnique; + } + + public Collection getParamsCoords() { + if (myParamsCoords == null) { + myParamsCoords = new ArrayList<>(); + } + return myParamsCoords; } public void setParamsCoords(Collection theParamsCoords) { @@ -453,8 +305,11 @@ public class ResourceTable extends BaseHasResource implements Serializable { getParamsCoords().addAll(theParamsCoords); } - public void setParamsCoordsPopulated(boolean theParamsCoordsPopulated) { - myParamsCoordsPopulated = theParamsCoordsPopulated; + public Collection getParamsDate() { + if (myParamsDate == null) { + myParamsDate = new ArrayList<>(); + } + return myParamsDate; } public void setParamsDate(Collection theParamsDate) { @@ -465,8 +320,11 @@ public class ResourceTable extends BaseHasResource implements Serializable { getParamsDate().addAll(theParamsDate); } - public void setParamsDatePopulated(boolean theParamsDatePopulated) { - myParamsDatePopulated = theParamsDatePopulated; + public Collection getParamsNumber() { + if (myParamsNumber == null) { + myParamsNumber = new ArrayList<>(); + } + return myParamsNumber; } public void setParamsNumber(Collection theNumberParams) { @@ -477,8 +335,11 @@ public class ResourceTable extends BaseHasResource implements Serializable { getParamsNumber().addAll(theNumberParams); } - public void setParamsNumberPopulated(boolean theParamsNumberPopulated) { - myParamsNumberPopulated = theParamsNumberPopulated; + public Collection getParamsQuantity() { + if (myParamsQuantity == null) { + myParamsQuantity = new ArrayList<>(); + } + return myParamsQuantity; } public void setParamsQuantity(Collection theQuantityParams) { @@ -489,8 +350,11 @@ public class ResourceTable extends BaseHasResource implements Serializable { getParamsQuantity().addAll(theQuantityParams); } - public void setParamsQuantityPopulated(boolean theParamsQuantityPopulated) { - myParamsQuantityPopulated = theParamsQuantityPopulated; + public Collection getParamsString() { + if (myParamsString == null) { + myParamsString = new ArrayList<>(); + } + return myParamsString; } public void setParamsString(Collection theParamsString) { @@ -501,8 +365,11 @@ public class ResourceTable extends BaseHasResource implements Serializable { getParamsString().addAll(theParamsString); } - public void setParamsStringPopulated(boolean theParamsStringPopulated) { - myParamsStringPopulated = theParamsStringPopulated; + public Collection getParamsToken() { + if (myParamsToken == null) { + myParamsToken = new ArrayList<>(); + } + return myParamsToken; } public void setParamsToken(Collection theParamsToken) { @@ -513,8 +380,11 @@ public class ResourceTable extends BaseHasResource implements Serializable { getParamsToken().addAll(theParamsToken); } - public void setParamsTokenPopulated(boolean theParamsTokenPopulated) { - myParamsTokenPopulated = theParamsTokenPopulated; + public Collection getParamsUri() { + if (myParamsUri == null) { + myParamsUri = new ArrayList<>(); + } + return myParamsUri; } public void setParamsUri(Collection theParamsUri) { @@ -525,8 +395,8 @@ public class ResourceTable extends BaseHasResource implements Serializable { getParamsUri().addAll(theParamsUri); } - public void setParamsUriPopulated(boolean theParamsUriPopulated) { - myParamsUriPopulated = theParamsUriPopulated; + public String getProfile() { + return myProfile; } public void setProfile(String theProfile) { @@ -536,6 +406,13 @@ public class ResourceTable extends BaseHasResource implements Serializable { myProfile = theProfile; } + public Collection getResourceLinks() { + if (myResourceLinks == null) { + myResourceLinks = new ArrayList<>(); + } + return myResourceLinks; + } + public void setResourceLinks(Collection theLinks) { if (!isHasLinks() && theLinks.isEmpty()) { return; @@ -544,10 +421,112 @@ public class ResourceTable extends BaseHasResource implements Serializable { getResourceLinks().addAll(theLinks); } + @Override + public String getResourceType() { + return myResourceType; + } + public void setResourceType(String theResourceType) { myResourceType = theResourceType; } + @Override + public Collection getTags() { + if (myTags == null) { + myTags = new HashSet<>(); + } + return myTags; + } + + @Override + public long getVersion() { + return myVersion; + } + + public void setVersion(long theVersion) { + myVersion = theVersion; + } + + public boolean isHasLinks() { + return myHasLinks; + } + + public void setHasLinks(boolean theHasLinks) { + myHasLinks = theHasLinks; + } + + public boolean isParamsCompositeStringUniquePresent() { + return myParamsCompositeStringUniquePresent; + } + + public void setParamsCompositeStringUniquePresent(boolean theParamsCompositeStringUniquePresent) { + myParamsCompositeStringUniquePresent = theParamsCompositeStringUniquePresent; + } + + public boolean isParamsCoordsPopulated() { + return myParamsCoordsPopulated; + } + + public void setParamsCoordsPopulated(boolean theParamsCoordsPopulated) { + myParamsCoordsPopulated = theParamsCoordsPopulated; + } + + public boolean isParamsDatePopulated() { + return myParamsDatePopulated; + } + + public void setParamsDatePopulated(boolean theParamsDatePopulated) { + myParamsDatePopulated = theParamsDatePopulated; + } + + public boolean isParamsNumberPopulated() { + return myParamsNumberPopulated; + } + + public void setParamsNumberPopulated(boolean theParamsNumberPopulated) { + myParamsNumberPopulated = theParamsNumberPopulated; + } + + public boolean isParamsQuantityPopulated() { + return myParamsQuantityPopulated; + } + + public void setParamsQuantityPopulated(boolean theParamsQuantityPopulated) { + myParamsQuantityPopulated = theParamsQuantityPopulated; + } + + public boolean isParamsStringPopulated() { + return myParamsStringPopulated; + } + + public void setParamsStringPopulated(boolean theParamsStringPopulated) { + myParamsStringPopulated = theParamsStringPopulated; + } + + public boolean isParamsTokenPopulated() { + return myParamsTokenPopulated; + } + + public void setParamsTokenPopulated(boolean theParamsTokenPopulated) { + myParamsTokenPopulated = theParamsTokenPopulated; + } + + public boolean isParamsUriPopulated() { + return myParamsUriPopulated; + } + + public void setParamsUriPopulated(boolean theParamsUriPopulated) { + myParamsUriPopulated = theParamsUriPopulated; + } + + /** + * Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation + * and was not re-saved in the database + */ + public boolean isUnchangedInCurrentOperation() { + return myUnchangedInCurrentOperation; + } + /** * Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation * and was not re-saved in the database @@ -556,8 +535,12 @@ public class ResourceTable extends BaseHasResource implements Serializable { myUnchangedInCurrentOperation = theUnchangedInCurrentOperation; } - public void setVersion(long theVersion) { - myVersion = theVersion; + public void setContentTextParsedIntoWords(String theContentText) { + myContentText = theContentText; + } + + public void setNarrativeTextParsedIntoWords(String theNarrativeText) { + myNarrativeText = theNarrativeText; } public ResourceHistoryTable toHistory(ResourceHistoryTable theResourceHistoryTable) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/JpaRuntimeSearchParam.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/JpaRuntimeSearchParam.java new file mode 100644 index 00000000000..3c204f88f1b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/JpaRuntimeSearchParam.java @@ -0,0 +1,73 @@ +package ca.uhn.fhir.jpa.search; + +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import java.util.*; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class JpaRuntimeSearchParam extends RuntimeSearchParam { + + private final boolean myUnique; + private final List myComponents; + + /** + * Constructor + */ + public JpaRuntimeSearchParam(IIdType theId, String theUri, String theName, String theDescription, String thePath, RestSearchParameterTypeEnum theParamType, Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus, boolean theUnique, List theComponents, Collection> theBase) { + super(theId, theUri, theName, theDescription, thePath, theParamType, createCompositeList(theParamType), theProvidesMembershipInCompartments, theTargets, theStatus, toStrings(theBase)); + myUnique = theUnique; + myComponents = Collections.unmodifiableList(theComponents); + } + + private static Collection toStrings(Collection> theBase) { + HashSet retVal = new HashSet<>(); + for (IPrimitiveType next : theBase) { + if (isNotBlank(next.getValueAsString())) { + retVal.add(next.getValueAsString()); + } + } + return retVal; + } + + public List getComponents() { + return myComponents; + } + + public boolean isUnique() { + return myUnique; + } + + private static ArrayList createCompositeList(RestSearchParameterTypeEnum theParamType) { + if (theParamType == RestSearchParameterTypeEnum.COMPOSITE) { + return new ArrayList<>(); + } else { + return null; + } + } + + public static class Component { + private final String myExpression; + private final IBaseReference myReference; + + public Component(String theExpression, IBaseReference theReference) { + myExpression = theExpression; + myReference = theReference; + + } + + public String getExpression() { + return myExpression; + } + + public IBaseReference getReference() { + return myReference; + } + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java new file mode 100644 index 00000000000..036c185d5f3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.jpa.util; + +public class JpaConstants { + + public static final String EXT_SP_UNIQUE = "http://hapifhir.io/fhir/StructureDefinition/sp-unique"; + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index d296e849aee..e211d55f9ac 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -48,10 +48,10 @@ public class TestR4Config extends BaseJavaConfigR4 { } catch (Exception e) { ourLog.error("Exceeded maximum wait for connection", e); logGetConnectionStackTrace(); - if ("true".equals(System.getProperty("ci"))) { - fail("Exceeded maximum wait for connection: " + e.toString()); - } - System.exit(1); +// if ("true".equals(System.getProperty("ci"))) { + fail("Exceeded maximum wait for connection: " + e.toString()); +// } +// System.exit(1); retVal = null; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 84228f34703..f5555663eb9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -236,6 +236,7 @@ public abstract class BaseJpaTest { entityManager.createQuery("DELETE from " + ResourceIndexedSearchParamToken.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ResourceIndexedSearchParamUri.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ResourceIndexedSearchParamCoords.class.getSimpleName() + " d").executeUpdate(); + entityManager.createQuery("DELETE from " + ResourceIndexedCompositeStringUnique.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + ResourceLink.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + SearchResult.class.getSimpleName() + " d").executeUpdate(); entityManager.createQuery("DELETE from " + SearchInclude.class.getSimpleName() + " d").executeUpdate(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index 067aaa6cb8a..41837e312b6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -57,8 +57,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { private static JpaValidationSupportChainDstu3 ourJpaValidationSupportChainDstu3; private static IFhirResourceDaoValueSet ourValueSetDao; - // @Autowired - // protected HapiWorkerContext myHapiWorkerContext; + @Autowired + protected IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; @Autowired @Qualifier("myAllergyIntoleranceDaoDstu3") protected IFhirResourceDao myAllergyIntoleranceDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java new file mode 100644 index 00000000000..7845f330ae3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java @@ -0,0 +1,295 @@ +package ca.uhn.fhir.jpa.dao.dstu3; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.orm.jpa.JpaSystemException; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.*; + +@SuppressWarnings({"unchecked", "deprecation"}) +public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3UniqueSearchParamTest.class); + + @After + public void after() { + myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + } + + @Before + public void before() { + myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + } + + private void createUniqueBirthdateAndGenderSps() { + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/patient-gender"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setCode("gender"); + sp.setExpression("Patient.gender"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-birthdate"); + sp.setType(Enumerations.SearchParamType.DATE); + sp.setCode("birthdate"); + sp.setExpression("Patient.birthDate"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-gender-birthdate"); + sp.setType(Enumerations.SearchParamType.COMPOSITE); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-gender")); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-birthdate")); + sp.addExtension() + .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setValue(new BooleanType(true)); + mySearchParameterDao.update(sp); + + mySearchParamRegsitry.forceRefresh(); + } + + private void createUniqueNameAndManagingOrganizationSps() { + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/patient-name"); + sp.setType(Enumerations.SearchParamType.STRING); + sp.setCode("name"); + sp.setExpression("Patient.name"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-organization"); + sp.setType(Enumerations.SearchParamType.REFERENCE); + sp.setCode("organization"); + sp.setExpression("Patient.managingOrganization"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-name-organization"); + sp.setType(Enumerations.SearchParamType.COMPOSITE); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-name")); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-organization")); + sp.addExtension() + .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setValue(new BooleanType(true)); + mySearchParameterDao.update(sp); + + mySearchParamRegsitry.forceRefresh(); + } + + @Test + public void testDetectUniqueSearchParams() { + createUniqueBirthdateAndGenderSps(); + List params = mySearchParamRegsitry.getActiveUniqueSearchParams("Patient"); + + assertEquals(1, params.size()); + assertEquals(params.get(0).isUnique(), true); + assertEquals(2, params.get(0).getCompositeOf().size()); + // Should be alphabetical order + assertEquals("birthdate", params.get(0).getCompositeOf().get(0).getName()); + assertEquals("gender", params.get(0).getCompositeOf().get(1).getName()); + } + + + @Test + public void testDuplicateUniqueValuesAreRejected() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + try { + myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + fail(); + } catch (JpaSystemException e) { + // good + } + + Patient pt2 = new Patient(); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + IIdType id2 = myPatientDao.create(pt2).getId().toUnqualifiedVersionless(); + + pt2 = new Patient(); + pt2.setId(id2); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + pt2.setBirthDateElement(new DateType("2011-01-01")); + try { + myPatientDao.update(pt2); + fail(); + } catch (JpaSystemException e) { + // good + } + + } + + @Test + public void testUniqueValuesAreIndexed_DateAndToken() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); + assertEquals(1, uniques.size()); + assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale", uniques.get(0).getIndexString()); + } + + @Test + public void testSearchSynchronousUsingUniqueComposite() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + pt2.setBirthDateElement(new DateType("2011-01-02")); + IIdType id2 = myPatientDao.create(pt2).getId().toUnqualifiedVersionless(); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(100); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + params.add("birthdate", new DateParam("2011-01-01")); + IBundleProvider results = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + assertEquals(SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest()); + } + + @Test + public void testSearchUsingUniqueComposite() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + pt2.setBirthDateElement(new DateType("2011-01-02")); + IIdType id2 = myPatientDao.create(pt2).getId().toUnqualifiedVersionless(); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + SearchParameterMap params = new SearchParameterMap(); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + params.add("birthdate", new DateParam("2011-01-01")); + IBundleProvider results = myPatientDao.search(params); + String searchId = results.getUuid(); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + assertEquals(SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + // Other order + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + params = new SearchParameterMap(); + params.add("birthdate", new DateParam("2011-01-01")); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + results = myPatientDao.search(params); + assertEquals(searchId, results.getUuid()); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + // Null because we just reuse the last search + assertEquals(null, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + params = new SearchParameterMap(); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + params.add("birthdate", new DateParam("2011-01-03")); + results = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(results), empty()); + assertEquals(SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + params = new SearchParameterMap(); + params.add("birthdate", new DateParam("2011-01-03")); + results = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(results), empty()); + assertEquals(SearchBuilder.HandlerTypeEnum.STANDARD_QUERY, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + } + + @Test + public void testUniqueValuesAreIndexed_StringAndReference() { + createUniqueNameAndManagingOrganizationSps(); + + Organization org = new Organization(); + org.setId("Organization/ORG"); + org.setName("ORG"); + myOrganizationDao.update(org); + + Patient pt1 = new Patient(); + pt1.addName() + .setFamily("FAMILY1") + .addGiven("GIVEN1") + .addGiven("GIVEN2") + .addGiven("GIVEN2"); // GIVEN2 happens twice + pt1.setManagingOrganization(new Reference("Organization/ORG")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); + Collections.sort(uniques); + + assertEquals(3, uniques.size()); + assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?name=FAMILY1&organization=Organization%2FORG", uniques.get(0).getIndexString()); + + assertEquals("Patient/" + id1.getIdPart(), uniques.get(1).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?name=GIVEN1&organization=Organization%2FORG", uniques.get(1).getIndexString()); + + assertEquals("Patient/" + id1.getIdPart(), uniques.get(2).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?name=GIVEN2&organization=Organization%2FORG", uniques.get(2).getIndexString()); + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java index 3e46a73cac5..62c32627826 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java @@ -3,9 +3,11 @@ package ca.uhn.fhir.jpa.dao.dstu3; import static org.junit.Assert.assertEquals; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport; import org.hl7.fhir.dstu3.model.Observation; @@ -53,6 +55,16 @@ public class SearchParamExtractorDstu3Test { return sps; } + @Override + public List getActiveUniqueSearchParams(String theResourceName) { + throw new UnsupportedOperationException(); + } + + @Override + public List getActiveUniqueSearchParams(String theResourceName, Set theParamNames) { + throw new UnsupportedOperationException(); + } + @Override public void forceRefresh() { // nothing diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 07fa7bc6134..e2b34d7eef5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -47,17 +47,15 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; -//@formatter:off @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= {TestR4Config.class}) -//@formatter:on public abstract class BaseJpaR4Test extends BaseJpaTest { private static JpaValidationSupportChainR4 ourJpaValidationSupportChainR4; private static IFhirResourceDaoValueSet ourValueSetDao; - // @Autowired - // protected HapiWorkerContext myHapiWorkerContext; + @Autowired + protected IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; @Autowired @Qualifier("myAllergyIntoleranceDaoR4") protected IFhirResourceDao myAllergyIntoleranceDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java new file mode 100644 index 00000000000..99e4eb030d8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -0,0 +1,296 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.orm.jpa.JpaSystemException; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.*; + +@SuppressWarnings({"unchecked", "deprecation"}) +public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4UniqueSearchParamTest.class); + + @After + public void after() { + myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + } + + @Before + public void before() { + myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + } + + private void createUniqueBirthdateAndGenderSps() { + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/patient-gender"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setCode("gender"); + sp.setExpression("Patient.gender"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-birthdate"); + sp.setType(Enumerations.SearchParamType.DATE); + sp.setCode("birthdate"); + sp.setExpression("Patient.birthDate"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-gender-birthdate"); + sp.setType(Enumerations.SearchParamType.COMPOSITE); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-gender")); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-birthdate")); + sp.addExtension() + .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setValue(new BooleanType(true)); + mySearchParameterDao.update(sp); + + mySearchParamRegsitry.forceRefresh(); + } + + private void createUniqueNameAndManagingOrganizationSps() { + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/patient-name"); + sp.setType(Enumerations.SearchParamType.STRING); + sp.setCode("name"); + sp.setExpression("Patient.name"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-organization"); + sp.setType(Enumerations.SearchParamType.REFERENCE); + sp.setCode("organization"); + sp.setExpression("Patient.managingOrganization"); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-name-organization"); + sp.setType(Enumerations.SearchParamType.COMPOSITE); + sp.setStatus(PublicationStatus.ACTIVE); + sp.addBase("Patient"); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-name")); + sp.addComponent() + .setExpression("Patient") + .setDefinition(new Reference("SearchParameter/patient-organization")); + sp.addExtension() + .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setValue(new BooleanType(true)); + mySearchParameterDao.update(sp); + + mySearchParamRegsitry.forceRefresh(); + } + + @Test + public void testDetectUniqueSearchParams() { + createUniqueBirthdateAndGenderSps(); + List params = mySearchParamRegsitry.getActiveUniqueSearchParams("Patient"); + + assertEquals(1, params.size()); + assertEquals(params.get(0).isUnique(), true); + assertEquals(2, params.get(0).getCompositeOf().size()); + // Should be alphabetical order + assertEquals("birthdate", params.get(0).getCompositeOf().get(0).getName()); + assertEquals("gender", params.get(0).getCompositeOf().get(1).getName()); + } + + + @Test + public void testDuplicateUniqueValuesAreRejected() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + try { + myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + fail(); + } catch (JpaSystemException e) { + // good + } + + Patient pt2 = new Patient(); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + IIdType id2 = myPatientDao.create(pt2).getId().toUnqualifiedVersionless(); + + pt2 = new Patient(); + pt2.setId(id2); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + pt2.setBirthDateElement(new DateType("2011-01-01")); + try { + myPatientDao.update(pt2); + fail(); + } catch (JpaSystemException e) { + // good + } + + } + + @Test + public void testUniqueValuesAreIndexed_DateAndToken() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); + assertEquals(1, uniques.size()); + assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale", uniques.get(0).getIndexString()); + } + + @Test + public void testSearchSynchronousUsingUniqueComposite() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + pt2.setBirthDateElement(new DateType("2011-01-02")); + IIdType id2 = myPatientDao.create(pt2).getId().toUnqualifiedVersionless(); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(100); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + params.add("birthdate", new DateParam("2011-01-01")); + IBundleProvider results = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + assertEquals(SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest()); + } + + @Test + public void testSearchUsingUniqueComposite() { + createUniqueBirthdateAndGenderSps(); + + Patient pt1 = new Patient(); + pt1.setGender(Enumerations.AdministrativeGender.MALE); + pt1.setBirthDateElement(new DateType("2011-01-01")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + Patient pt2 = new Patient(); + pt2.setGender(Enumerations.AdministrativeGender.MALE); + pt2.setBirthDateElement(new DateType("2011-01-02")); + IIdType id2 = myPatientDao.create(pt2).getId().toUnqualifiedVersionless(); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + SearchParameterMap params = new SearchParameterMap(); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + params.add("birthdate", new DateParam("2011-01-01")); + IBundleProvider results = myPatientDao.search(params); + String searchId = results.getUuid(); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + assertEquals(SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + // Other order + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + params = new SearchParameterMap(); + params.add("birthdate", new DateParam("2011-01-01")); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + results = myPatientDao.search(params); + assertEquals(searchId, results.getUuid()); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + // Null because we just reuse the last search + assertEquals(null, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + params = new SearchParameterMap(); + params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); + params.add("birthdate", new DateParam("2011-01-03")); + results = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(results), empty()); + assertEquals(SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + SearchBuilder.resetLastHandlerMechanismForUnitTest(); + params = new SearchParameterMap(); + params.add("birthdate", new DateParam("2011-01-03")); + results = myPatientDao.search(params); + assertThat(toUnqualifiedVersionlessIdValues(results), empty()); + assertEquals(SearchBuilder.HandlerTypeEnum.STANDARD_QUERY, SearchBuilder.getLastHandlerMechanismForUnitTest()); + + } + + @Test + public void testUniqueValuesAreIndexed_StringAndReference() { + createUniqueNameAndManagingOrganizationSps(); + + Organization org = new Organization(); + org.setId("Organization/ORG"); + org.setName("ORG"); + myOrganizationDao.update(org); + + Patient pt1 = new Patient(); + pt1.addName() + .setFamily("FAMILY1") + .addGiven("GIVEN1") + .addGiven("GIVEN2") + .addGiven("GIVEN2"); // GIVEN2 happens twice + pt1.setManagingOrganization(new Reference("Organization/ORG")); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); + Collections.sort(uniques); + + assertEquals(3, uniques.size()); + assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?name=FAMILY1&organization=Organization%2FORG", uniques.get(0).getIndexString()); + + assertEquals("Patient/" + id1.getIdPart(), uniques.get(1).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?name=GIVEN1&organization=Organization%2FORG", uniques.get(1).getIndexString()); + + assertEquals("Patient/" + id1.getIdPart(), uniques.get(2).getResource().getIdDt().toUnqualifiedVersionless().getValue()); + assertEquals("Patient?name=GIVEN2&organization=Organization%2FORG", uniques.get(2).getIndexString()); + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index 8ff334b3e69..add3f47f8e5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import java.util.*; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.Observation; @@ -45,6 +46,16 @@ public class SearchParamExtractorR4Test { return sps; } + @Override + public List getActiveUniqueSearchParams(String theResourceName) { + throw new UnsupportedOperationException(); + } + + @Override + public List getActiveUniqueSearchParams(String theResourceName, Set theParamNames) { + throw new UnsupportedOperationException(); + } + @Override public void forceRefresh() { // nothing diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java index f9776ffbd8d..0ba634e43ca 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.java @@ -49,7 +49,7 @@ public interface IServerOperationInterceptor extends IServerInterceptor { * User code may call this method to indicate to an interceptor that * a resource is being updated * - * @deprecated Deprecated in HAPI FHIR 2.6 in favour of {@link #resourceUpdated(RequestDetails, IBaseResource, IBaseResource)} + * @deprecated Deprecated in HAPI FHIR 3.0.0 in favour of {@link #resourceUpdated(RequestDetails, IBaseResource, IBaseResource)} */ @Deprecated void resourceUpdated(RequestDetails theRequest, IBaseResource theResource); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d61e72af780..1cd491b125e 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -349,6 +349,12 @@ HAPI FHIR 2.5, but this was not documented. This variable has now been documented as a part of the available features. + + A new experimental feature has been added to the JPA server which allows + you to define certain search parameter combinations as being resource keys, + so that a database constraint will prevent more than one resource from + having a matching pair +