Java tutorial
package ca.uhn.fhir.jpa.dao; import static org.apache.commons.lang3.StringUtils.defaultString; /* * #%L * HAPI FHIR JPA Server * %% * Copyright (C) 2014 - 2016 University Health Network * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.math.BigDecimal; import java.math.MathContext; import java.util.*; import java.util.Map.Entry; import javax.persistence.EntityManager; import javax.persistence.Tuple; import javax.persistence.TypedQuery; import javax.persistence.criteria.*; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; 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 org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import com.google.common.collect.Lists; import com.google.common.collect.Sets; 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.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.term.VersionIndependentConcept; 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.dstu.resource.BaseResource; 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.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.method.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; 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; public class SearchBuilder { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class); private BaseHapiFhirDao<?> myCallingDao; private FhirContext myContext; private EntityManager myEntityManager; private IForcedIdDao myForcedIdDao; private SearchParameterMap myParams; private Collection<Long> myPids; private PlatformTransactionManager myPlatformTransactionManager; private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; private String myResourceName; private Class<? extends IBaseResource> myResourceType; private IFulltextSearchSvc mySearchDao; private Search mySearchEntity; private ISearchResultDao mySearchResultDao; private IHapiTerminologySvc myTerminologySvc; public SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager, PlatformTransactionManager thePlatformTransactionManager, IFulltextSearchSvc theSearchDao, ISearchResultDao theSearchResultDao, BaseHapiFhirDao<?> theDao, IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao, IHapiTerminologySvc theTerminologySvc) { myContext = theFhirContext; myEntityManager = theEntityManager; myPlatformTransactionManager = thePlatformTransactionManager; mySearchDao = theSearchDao; mySearchResultDao = theSearchResultDao; myCallingDao = theDao; myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao; myForcedIdDao = theForcedIdDao; myTerminologySvc = theTerminologySvc; } private void addPredicateComposite(RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd) { // TODO: fail if missing is set for a composite query CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); IQueryParameterType or = theNextAnd.get(0); if (!(or instanceof CompositeParam<?, ?>)) { throw new InvalidRequestException("Invalid type for composite param (must be " + CompositeParam.class.getSimpleName() + ": " + or.getClass()); } CompositeParam<?, ?> cp = (CompositeParam<?, ?>) or; List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); RuntimeSearchParam left = theParamDef.getCompositeOf().get(0); IQueryParameterType leftValue = cp.getLeftValue(); predicates.add(createCompositeParamPart(builder, from, left, leftValue)); RuntimeSearchParam right = theParamDef.getCompositeOf().get(1); IQueryParameterType rightValue = cp.getRightValue(); predicates.add(createCompositeParamPart(builder, from, right, rightValue)); createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(q.getResultList()); } private void addPredicateDate(String theParamName, List<? extends IQueryParameterType> theList) { if (Boolean.TRUE.equals(theList.get(0).getMissing())) { addPredicateParamMissing("myParamsDate", theParamName, ResourceIndexedSearchParamDate.class); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceIndexedSearchParamDate> from = cq.from(ResourceIndexedSearchParamDate.class); cq.select(from.get("myResourcePid").as(Long.class)); List<Predicate> codePredicates = new ArrayList<Predicate>(); for (IQueryParameterType nextOr : theList) { if (addPredicateMissingFalseIfPresent(builder, theParamName, from, codePredicates, nextOr)) { continue; } IQueryParameterType params = nextOr; Predicate p = createPredicateDate(builder, from, params); codePredicates.add(p); } Predicate masterCodePredicate = builder.or(toArray(codePredicates)); List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.equal(from.get("myParamName"), theParamName)); createPredicateResourceId(builder, cq, predicates, from.get("myResourcePid").as(Long.class)); createPredicateLastUpdatedForIndexedSearchParam(builder, from, predicates); predicates.add(masterCodePredicate); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(q.getResultList()); } private void addPredicateHas(List<List<? extends IQueryParameterType>> theHasParameters, DateRangeParam theLastUpdated) { for (List<? extends IQueryParameterType> nextOrList : theHasParameters) { StringBuilder valueBuilder = new StringBuilder(); String targetResourceType = null; String owningParameter = null; String parameterName = null; for (IQueryParameterType nextParam : nextOrList) { HasParam next = (HasParam) nextParam; if (valueBuilder.length() > 0) { valueBuilder.append(','); } valueBuilder.append(UrlUtil.escape(next.getValueAsQueryToken(myContext))); targetResourceType = next.getTargetResourceType(); owningParameter = next.getOwningFieldName(); parameterName = next.getParameterName(); } if (valueBuilder.length() == 0) { continue; } String matchUrl = targetResourceType + '?' + UrlUtil.escape(parameterName) + '=' + valueBuilder.toString(); RuntimeResourceDefinition targetResourceDefinition; try { targetResourceDefinition = myContext.getResourceDefinition(targetResourceType); } catch (DataFormatException e) { throw new InvalidRequestException("Invalid resource type: " + targetResourceType); } RuntimeSearchParam owningParameterDef = targetResourceDefinition .getSearchParam(parameterName.replaceAll("\\..*", "")); if (owningParameterDef == null) { throw new InvalidRequestException( "Unknown parameter name: " + targetResourceType + ':' + parameterName); } owningParameterDef = targetResourceDefinition.getSearchParam(owningParameter); if (owningParameterDef == null) { throw new InvalidRequestException( "Unknown parameter name: " + targetResourceType + ':' + owningParameter); } Class<? extends IBaseResource> resourceType = targetResourceDefinition.getImplementingClass(); Set<Long> match = myCallingDao.processMatchUrl(matchUrl, resourceType); if (match.isEmpty()) { doSetPids(new ArrayList<Long>()); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceLink> from = cq.from(ResourceLink.class); cq.select(from.get("myTargetResourcePid").as(Long.class)); List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("mySourceResourceType"), targetResourceType)); predicates.add(from.get("mySourceResourcePid").in(match)); predicates.add(createResourceLinkPathPredicate(myContext, owningParameter, from, resourceType)); predicates.add(builder.equal(from.get("myTargetResourceType"), myResourceName)); createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); createPredicateLastUpdatedForResourceLink(builder, from, predicates); cq.where(toArray(predicates)); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(q.getResultList()); if (doHaveNoResults()) { return; } return; } } private void addPredicateId(Set<Long> thePids) { if (thePids == null || thePids.isEmpty()) { return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(from.get("myId").in(thePids)); createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); createPredicateLastUpdatedForResourceTable(builder, from, predicates); cq.where(toArray(predicates)); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(q.getResultList()); } private void addPredicateLanguage(List<List<? extends IQueryParameterType>> theList) { for (List<? extends IQueryParameterType> nextList : theList) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); Set<String> values = new HashSet<String>(); for (IQueryParameterType next : nextList) { if (next instanceof StringParam) { String nextValue = ((StringParam) next).getValue(); if (isBlank(nextValue)) { continue; } values.add(nextValue); } else { throw new InternalErrorException( "Lanugage parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName()); } } if (values.isEmpty()) { continue; } List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(from.get("myLanguage").as(String.class).in(values)); createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); createPredicateLastUpdatedForResourceTable(builder, from, predicates); predicates.add(builder.isNull(from.get("myDeleted"))); cq.where(toArray(predicates)); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(q.getResultList()); if (doHaveNoResults()) { return; } } return; } private boolean addPredicateMissingFalseIfPresent(CriteriaBuilder theBuilder, String theParamName, Root<? extends BaseResourceIndexedSearchParam> from, List<Predicate> codePredicates, IQueryParameterType nextOr) { boolean missingFalse = false; if (nextOr.getMissing() != null) { if (nextOr.getMissing().booleanValue() == true) { throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "multipleParamsWithSameNameOneIsMissingTrue", theParamName)); } Predicate singleCode = from.get("myId").isNotNull(); Predicate name = theBuilder.equal(from.get("myParamName"), theParamName); codePredicates.add(theBuilder.and(name, singleCode)); missingFalse = true; } return missingFalse; } private boolean addPredicateMissingFalseIfPresentForResourceLink(CriteriaBuilder theBuilder, String theParamName, Root<? extends ResourceLink> from, List<Predicate> codePredicates, IQueryParameterType nextOr) { boolean missingFalse = false; if (nextOr.getMissing() != null) { if (nextOr.getMissing().booleanValue() == true) { throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "multipleParamsWithSameNameOneIsMissingTrue", theParamName)); } Predicate singleCode = from.get("mySourceResource").isNotNull(); Predicate name = createResourceLinkPathPredicate(theParamName, from); codePredicates.add(theBuilder.and(name, singleCode)); missingFalse = true; } return missingFalse; } private void addPredicateNumber(String theParamName, List<? extends IQueryParameterType> theList) { if (Boolean.TRUE.equals(theList.get(0).getMissing())) { addPredicateParamMissing("myParamsNumber", theParamName, ResourceIndexedSearchParamNumber.class); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceIndexedSearchParamNumber> from = cq.from(ResourceIndexedSearchParamNumber.class); cq.select(from.get("myResourcePid").as(Long.class)); List<Predicate> codePredicates = new ArrayList<Predicate>(); for (IQueryParameterType nextOr : theList) { IQueryParameterType params = nextOr; if (addPredicateMissingFalseIfPresent(builder, theParamName, from, codePredicates, nextOr)) { continue; } if (params instanceof NumberParam) { NumberParam param = (NumberParam) params; BigDecimal value = param.getValue(); if (value == null) { continue; } final Expression<BigDecimal> fromObj = from.get("myValue"); ParamPrefixEnum prefix = ObjectUtils.defaultIfNull(param.getPrefix(), ParamPrefixEnum.EQUAL); String invalidMessageName = "invalidNumberPrefix"; String valueAsString = param.getValue().toPlainString(); Predicate num = createPredicateNumeric(builder, params, prefix, value, fromObj, invalidMessageName, valueAsString); codePredicates.add(num); } else { throw new IllegalArgumentException("Invalid token type: " + params.getClass()); } } List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.equal(from.get("myParamName"), theParamName)); predicates.add(builder.or(toArray(codePredicates))); createPredicateResourceId(builder, cq, predicates, from.get("myResourcePid").as(Long.class)); createPredicateLastUpdatedForIndexedSearchParam(builder, from, predicates); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(q.getResultList()); } private void addPredicateParamMissing(String joinName, String theParamName, Class<? extends BaseResourceIndexedSearchParam> theParamTable) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); Subquery<Long> subQ = cq.subquery(Long.class); Root<? extends BaseResourceIndexedSearchParam> subQfrom = subQ.from(theParamTable); subQ.select(subQfrom.get("myResourcePid").as(Long.class)); Predicate subQname = builder.equal(subQfrom.get("myParamName"), theParamName); Predicate subQtype = builder.equal(subQfrom.get("myResourceType"), myResourceName); subQ.where(builder.and(subQtype, subQname)); List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.not(builder.in(from.get("myId")).value(subQ))); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.isNull(from.get("myDeleted"))); createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); cq.where(builder.and(toArray(predicates))); ourLog.info("Adding :missing qualifier for parameter '{}'", theParamName); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(q.getResultList()); } private void addPredicateParamMissingResourceLink(String joinName, String theParamName) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); Subquery<Long> subQ = cq.subquery(Long.class); Root<ResourceLink> subQfrom = subQ.from(ResourceLink.class); subQ.select(subQfrom.get("mySourceResourcePid").as(Long.class)); // subQ.where(builder.equal(subQfrom.get("myParamName"), theParamName)); Predicate path = createResourceLinkPathPredicate(theParamName, subQfrom); subQ.where(path); List<Predicate> predicates = new ArrayList<Predicate>(); createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); predicates.add(builder.not(builder.in(from.get("myId")).value(subQ))); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); List<Long> resultList = q.getResultList(); doSetPids(new HashSet<Long>(resultList)); } private void addPredicateQuantity(String theParamName, List<? extends IQueryParameterType> theList) { if (Boolean.TRUE.equals(theList.get(0).getMissing())) { addPredicateParamMissing("myParamsQuantity", theParamName, ResourceIndexedSearchParamQuantity.class); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceIndexedSearchParamQuantity> from = cq.from(ResourceIndexedSearchParamQuantity.class); cq.select(from.get("myResourcePid").as(Long.class)); List<Predicate> codePredicates = new ArrayList<Predicate>(); for (IQueryParameterType nextOr : theList) { if (addPredicateMissingFalseIfPresent(builder, theParamName, from, codePredicates, nextOr)) { continue; } Predicate singleCode = createPredicateQuantity(builder, from, nextOr); codePredicates.add(singleCode); } List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.equal(from.get("myParamName"), theParamName)); predicates.add(builder.or(toArray(codePredicates))); createPredicateResourceId(builder, cq, predicates, from.get("myResourcePid").as(Long.class)); createPredicateLastUpdatedForIndexedSearchParam(builder, from, predicates); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(new HashSet<Long>(q.getResultList())); } private void addPredicateReference(String theParamName, List<? extends IQueryParameterType> theList) { assert theParamName.contains(".") == false; if (Boolean.TRUE.equals(theList.get(0).getMissing())) { addPredicateParamMissingResourceLink("myResourceLinks", theParamName); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceLink> from = cq.from(ResourceLink.class); cq.select(from.get("mySourceResourcePid").as(Long.class)); List<Predicate> codePredicates = new ArrayList<Predicate>(); for (IQueryParameterType nextOr : theList) { IQueryParameterType params = nextOr; if (addPredicateMissingFalseIfPresentForResourceLink(builder, theParamName, from, codePredicates, nextOr)) { continue; } if (params instanceof ReferenceParam) { ReferenceParam ref = (ReferenceParam) params; if (isBlank(ref.getChain())) { IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null); if (dt.hasBaseUrl()) { if (myCallingDao.getConfig().getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) { dt = dt.toUnqualified(); } else { ourLog.debug("Searching for resource link with target URL: {}", dt.getValue()); Predicate eq = builder.equal(from.get("myTargetResourceUrl"), dt.getValue()); codePredicates.add(eq); continue; } } List<Long> targetPid; try { targetPid = myCallingDao.translateForcedIdToPids(dt); } catch (ResourceNotFoundException e) { doSetPids(new ArrayList<Long>()); return; } for (Long next : targetPid) { ourLog.debug("Searching for resource link with target PID: {}", next); Predicate eq = builder.equal(from.get("myTargetResourcePid"), next); codePredicates.add(eq); } } else { List<Class<? extends IBaseResource>> resourceTypes; String resourceId; if (!ref.getValue().matches("[a-zA-Z]+\\/.*")) { String paramPath = myContext.getResourceDefinition(myResourceType) .getSearchParam(theParamName).getPath(); if (paramPath.endsWith(".as(Reference)")) { paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference"; } BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, paramPath); if (def instanceof RuntimeChildChoiceDefinition) { RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def; resourceTypes = choiceDef.getResourceTypes(); } else if (def instanceof RuntimeChildResourceDefinition) { RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def; resourceTypes = resDef.getResourceTypes(); } else { throw new ConfigurationException("Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass()); } resourceId = ref.getValue(); } else { RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType()); resourceTypes = new ArrayList<Class<? extends IBaseResource>>(1); resourceTypes.add(resDef.getImplementingClass()); resourceId = ref.getIdPart(); } boolean foundChainMatch = false; String chain = ref.getChain(); String remainingChain = null; int chainDotIndex = chain.indexOf('.'); if (chainDotIndex != -1) { remainingChain = chain.substring(chainDotIndex + 1); chain = chain.substring(0, chainDotIndex); } for (Class<? extends IBaseResource> nextType : resourceTypes) { RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType); IFhirResourceDao<?> dao = myCallingDao.getDao(nextType); if (dao == null) { ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName()); continue; } int qualifierIndex = chain.indexOf(':'); String qualifier = null; if (qualifierIndex != -1) { qualifier = chain.substring(qualifierIndex); chain = chain.substring(0, qualifierIndex); } boolean isMeta = BaseHapiFhirDao.RESOURCE_META_PARAMS.containsKey(chain); RuntimeSearchParam param = null; if (!isMeta) { param = typeDef.getSearchParam(chain); if (param == null) { ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param); continue; } } 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 }); continue; } chainValue = new ReferenceParam(); chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); ((ReferenceParam) chainValue).setChain(remainingChain); } else if (isMeta) { IQueryParameterType type = BaseHapiFhirDao.newInstanceType(chain); type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); chainValue = type; } else { chainValue = toParameterType(param, qualifier, resourceId); } foundChainMatch = true; Set<Long> pids = dao.searchForIds(chain, chainValue); if (pids.isEmpty()) { continue; } Predicate eq = from.get("myTargetResourcePid").in(pids); codePredicates.add(eq); } if (!foundChainMatch) { throw new InvalidRequestException( myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + ref.getChain())); } } } else { throw new IllegalArgumentException( "Invalid token type (expecting ReferenceParam): " + params.getClass()); } } List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(createResourceLinkPathPredicate(theParamName, from)); predicates.add(builder.or(toArray(codePredicates))); createPredicateResourceId(builder, cq, predicates, from.get("mySourceResourcePid").as(Long.class)); createPredicateLastUpdatedForResourceLink(builder, from, predicates); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(new HashSet<Long>(q.getResultList())); } private void addPredicateString(String theParamName, List<? extends IQueryParameterType> theList) { if (Boolean.TRUE.equals(theList.get(0).getMissing())) { addPredicateParamMissing("myParamsString", theParamName, ResourceIndexedSearchParamString.class); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceIndexedSearchParamString> from = cq.from(ResourceIndexedSearchParamString.class); cq.select(from.get("myResourcePid").as(Long.class)); List<Predicate> codePredicates = new ArrayList<Predicate>(); for (IQueryParameterType nextOr : theList) { IQueryParameterType theParameter = nextOr; if (addPredicateMissingFalseIfPresent(builder, theParamName, from, codePredicates, nextOr)) { continue; } Predicate singleCode = createPredicateString(theParameter, theParamName, builder, from); codePredicates.add(singleCode); } List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.equal(from.get("myParamName"), theParamName)); predicates.add(builder.or(toArray(codePredicates))); createPredicateResourceId(builder, cq, predicates, from.get("myResourcePid").as(Long.class)); createPredicateLastUpdatedForIndexedSearchParam(builder, from, predicates); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(new HashSet<Long>(q.getResultList())); } private void addPredicateTag(List<List<? extends IQueryParameterType>> theList, String theParamName, DateRangeParam theLastUpdated) { TagTypeEnum tagType; if (Constants.PARAM_TAG.equals(theParamName)) { tagType = TagTypeEnum.TAG; } else if (Constants.PARAM_PROFILE.equals(theParamName)) { tagType = TagTypeEnum.PROFILE; } else if (Constants.PARAM_SECURITY.equals(theParamName)) { tagType = TagTypeEnum.SECURITY_LABEL; } else { throw new IllegalArgumentException("Param name: " + theParamName); // shouldn't happen } /* * CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = * builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); * cq.select(from.get("myId").as(Long.class)); * * Subquery<Long> subQ = cq.subquery(Long.class); Root<? extends BaseResourceIndexedSearchParam> subQfrom = * subQ.from(theParamTable); subQ.select(subQfrom.get("myResourcePid").as(Long.class)); * Predicate subQname = builder.equal(subQfrom.get("myParamName"), theParamName); Predicate subQtype = * builder.equal(subQfrom.get("myResourceType"), myResourceName); * subQ.where(builder.and(subQtype, subQname)); * * List<Predicate> predicates = new ArrayList<Predicate>(); * predicates.add(builder.not(builder.in(from.get("myId")).value(subQ))); * predicates.add(builder.equal(from.get("myResourceType"), * myResourceName)); predicates.add(builder.isNull(from.get("myDeleted"))); createPredicateResourceId(builder, cq, * predicates, from.get("myId").as(Long.class)); */ List<Pair<String, String>> notTags = Lists.newArrayList(); for (List<? extends IQueryParameterType> nextAndParams : theList) { for (IQueryParameterType nextOrParams : nextAndParams) { if (nextOrParams instanceof TokenParam) { TokenParam param = (TokenParam) nextOrParams; if (param.getModifier() == TokenParamModifier.NOT) { if (isNotBlank(param.getSystem()) || isNotBlank(param.getValue())) { notTags.add(Pair.of(param.getSystem(), param.getValue())); } } } } } /* * We have a parameter of ResourceType?_tag:not=foo This means match resources that don't have the given tag(s) */ if (notTags.isEmpty() == false) { // CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); // CriteriaQuery<Long> cq = builder.createQuery(Long.class); // Root<ResourceTable> from = cq.from(ResourceTable.class); // cq.select(from.get("myId").as(Long.class)); // // Subquery<Long> subQ = cq.subquery(Long.class); // Root<ResourceTag> subQfrom = subQ.from(ResourceTag.class); // subQ.select(subQfrom.get("myResourceId").as(Long.class)); // Predicate subQname = builder.equal(subQfrom.get("myParamName"), theParamName); // Predicate subQtype = builder.equal(subQfrom.get("myResourceType"), myResourceName); // subQ.where(builder.and(subQtype, subQname)); // // List<Predicate> predicates = new ArrayList<Predicate>(); // predicates.add(builder.not(builder.in(from.get("myId")).value(subQ))); // predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); // predicates.add(builder.isNull(from.get("myDeleted"))); // createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); } for (List<? extends IQueryParameterType> nextAndParams : theList) { boolean haveTags = false; for (IQueryParameterType nextParamUncasted : nextAndParams) { if (nextParamUncasted instanceof TokenParam) { TokenParam nextParam = (TokenParam) nextParamUncasted; if (isNotBlank(nextParam.getValue())) { haveTags = true; } else if (isNotBlank(nextParam.getSystem())) { throw new InvalidRequestException("Invalid " + theParamName + " parameter (must supply a value/code and not just a system): " + nextParam.getValueAsQueryToken(myContext)); } } else { UriParam nextParam = (UriParam) nextParamUncasted; if (isNotBlank(nextParam.getValue())) { haveTags = true; } } } if (!haveTags) { continue; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); boolean paramInverted = false; List<Pair<String, String>> tokens = Lists.newArrayList(); for (IQueryParameterType nextOrParams : nextAndParams) { String code; String system; if (nextOrParams instanceof TokenParam) { TokenParam nextParam = (TokenParam) nextOrParams; code = nextParam.getValue(); system = nextParam.getSystem(); if (nextParam.getModifier() == TokenParamModifier.NOT) { paramInverted = true; } } else { UriParam nextParam = (UriParam) nextOrParams; code = nextParam.getValue(); system = null; } if (isNotBlank(code)) { tokens.add(Pair.of(system, code)); } } if (tokens.isEmpty()) { continue; } if (paramInverted) { ourLog.debug("Searching for _tag:not"); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> newFrom = cq.from(ResourceTable.class); Subquery<Long> subQ = cq.subquery(Long.class); Root<ResourceTag> subQfrom = subQ.from(ResourceTag.class); subQ.select(subQfrom.get("myResourceId").as(Long.class)); cq.select(newFrom.get("myId").as(Long.class)); List<Predicate> andPredicates = new ArrayList<Predicate>(); andPredicates = new ArrayList<Predicate>(); andPredicates.add(builder.equal(newFrom.get("myResourceType"), myResourceName)); andPredicates.add(builder.not(builder.in(newFrom.get("myId")).value(subQ))); Subquery<Long> defJoin = subQ.subquery(Long.class); Root<TagDefinition> defJoinFrom = defJoin.from(TagDefinition.class); defJoin.select(defJoinFrom.get("myId").as(Long.class)); subQ.where(subQfrom.get("myTagId").as(Long.class).in(defJoin)); List<Predicate> orPredicates = createPredicateTagList(defJoinFrom, builder, tagType, tokens); defJoin.where(toArray(orPredicates)); cq.where(toArray(andPredicates)); TypedQuery<Long> q = myEntityManager.createQuery(cq); Set<Long> pids = new HashSet<Long>(q.getResultList()); doSetPids(pids); continue; } CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTag> from = cq.from(ResourceTag.class); List<Predicate> andPredicates = new ArrayList<Predicate>(); andPredicates.add(builder.equal(from.get("myResourceType"), myResourceName)); From<ResourceTag, TagDefinition> defJoin = from.join("myTag"); Join<?, ResourceTable> defJoin2 = from.join("myResource"); Predicate notDeletedPredicatePrediate = builder.isNull(defJoin2.get("myDeleted")); andPredicates.add(notDeletedPredicatePrediate); List<Predicate> orPredicates = createPredicateTagList(defJoin, builder, tagType, tokens); andPredicates.add(builder.or(toArray(orPredicates))); if (theLastUpdated != null) { andPredicates.addAll(createLastUpdatedPredicates(theLastUpdated, builder, defJoin2)); } createPredicateResourceId(builder, cq, andPredicates, from.get("myResourceId").as(Long.class)); Predicate masterCodePredicate = builder.and(toArray(andPredicates)); cq.select(from.get("myResourceId").as(Long.class)); cq.where(masterCodePredicate); TypedQuery<Long> q = myEntityManager.createQuery(cq); Set<Long> pids = new HashSet<Long>(q.getResultList()); doSetPids(pids); } } private void addPredicateToken(String theParamName, List<? extends IQueryParameterType> theList) { if (Boolean.TRUE.equals(theList.get(0).getMissing())) { addPredicateParamMissing("myParamsToken", theParamName, ResourceIndexedSearchParamToken.class); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceIndexedSearchParamToken> from = cq.from(ResourceIndexedSearchParamToken.class); cq.select(from.get("myResourcePid").as(Long.class)); List<Predicate> codePredicates = new ArrayList<Predicate>(); for (IQueryParameterType nextOr : theList) { if (addPredicateMissingFalseIfPresent(builder, theParamName, from, codePredicates, nextOr)) { continue; } if (nextOr instanceof TokenParam) { TokenParam id = (TokenParam) nextOr; if (id.isText()) { addPredicateString(theParamName, theList); continue; } } Predicate singleCode = createPredicateToken(nextOr, theParamName, builder, from); if (singleCode == null) { doSetPids(new ArrayList<Long>()); return; } codePredicates.add(singleCode); } if (codePredicates.isEmpty()) { return; } List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.equal(from.get("myParamName"), theParamName)); predicates.add(builder.or(toArray(codePredicates))); createPredicateResourceId(builder, cq, predicates, from.get("myResourcePid").as(Long.class)); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(new HashSet<Long>(q.getResultList())); } private void addPredicateUri(String theParamName, List<? extends IQueryParameterType> theList) { if (Boolean.TRUE.equals(theList.get(0).getMissing())) { addPredicateParamMissing("myParamsUri", theParamName, ResourceIndexedSearchParamUri.class); return; } CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceIndexedSearchParamUri> from = cq.from(ResourceIndexedSearchParamUri.class); cq.select(from.get("myResourcePid").as(Long.class)); List<Predicate> codePredicates = new ArrayList<Predicate>(); for (IQueryParameterType nextOr : theList) { IQueryParameterType params = nextOr; if (addPredicateMissingFalseIfPresent(builder, theParamName, from, codePredicates, nextOr)) { continue; } if (params instanceof UriParam) { UriParam param = (UriParam) params; String value = param.getValue(); if (value == null) { continue; } Path<Object> fromObj = from.get("myUri"); Predicate predicate; if (param.getQualifier() == UriParamQualifierEnum.ABOVE) { /* * :above is an inefficient query- It means that the user is supplying a more specific URL (say * http://example.com/foo/bar/baz) and that we should match on any URLs that are less * specific but otherwise the same. For example http://example.com and http://example.com/foo would both * match. * * We do this by querying the DB for all candidate URIs and then manually checking each one. This isn't * very efficient, but this is also probably not a very common type of query to do. * * If we ever need to make this more efficient, lucene could certainly be used as an optimization. */ ourLog.info("Searching for candidate URI:above parameters for Resource[{}] param[{}]", myResourceName, theParamName); Collection<String> candidates = myResourceIndexedSearchParamUriDao .findAllByResourceTypeAndParamName(myResourceName, theParamName); List<String> toFind = new ArrayList<String>(); for (String next : candidates) { if (value.length() >= next.length()) { if (value.substring(0, next.length()).equals(next)) { toFind.add(next); } } } if (toFind.isEmpty()) { continue; } predicate = fromObj.as(String.class).in(toFind); } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { predicate = builder.like(fromObj.as(String.class), createLeftMatchLikeExpression(value)); } else { predicate = builder.equal(fromObj.as(String.class), value); } codePredicates.add(predicate); } else { throw new IllegalArgumentException("Invalid URI type: " + params.getClass()); } } if (codePredicates.isEmpty()) { doSetPids(new HashSet<Long>()); return; } List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.equal(from.get("myParamName"), theParamName)); predicates.add(builder.or(toArray(codePredicates))); createPredicateResourceId(builder, cq, predicates, from.get("myResourcePid").as(Long.class)); cq.where(builder.and(toArray(predicates))); TypedQuery<Long> q = myEntityManager.createQuery(cq); doSetPids(new HashSet<Long>(q.getResultList())); } private Predicate createCompositeParamPart(CriteriaBuilder builder, Root<ResourceTable> from, RuntimeSearchParam left, IQueryParameterType leftValue) { Predicate retVal = null; switch (left.getParamType()) { case STRING: { From<ResourceIndexedSearchParamString, ResourceIndexedSearchParamString> stringJoin = from .join("myParamsString", JoinType.INNER); retVal = createPredicateString(leftValue, left.getName(), builder, stringJoin); break; } case TOKEN: { From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> tokenJoin = from .join("myParamsToken", JoinType.INNER); retVal = createPredicateToken(leftValue, left.getName(), builder, tokenJoin); break; } case DATE: { From<ResourceIndexedSearchParamDate, ResourceIndexedSearchParamDate> dateJoin = from .join("myParamsDate", JoinType.INNER); retVal = createPredicateDate(builder, dateJoin, leftValue); break; } case QUANTITY: { From<ResourceIndexedSearchParamQuantity, ResourceIndexedSearchParamQuantity> dateJoin = from .join("myParamsQuantity", JoinType.INNER); retVal = createPredicateQuantity(builder, dateJoin, leftValue); break; } } if (retVal == null) { throw new InvalidRequestException( "Don't know how to handle composite parameter with type of " + left.getParamType()); } return retVal; } private Predicate createPredicateDate(CriteriaBuilder theBuilder, From<ResourceIndexedSearchParamDate, ResourceIndexedSearchParamDate> theFrom, IQueryParameterType theParam) { Predicate p; if (theParam instanceof DateParam) { DateParam date = (DateParam) theParam; if (!date.isEmpty()) { DateRangeParam range = new DateRangeParam(date); p = createPredicateDateFromRange(theBuilder, theFrom, range); } else { // TODO: handle missing date param? p = null; } } else if (theParam instanceof DateRangeParam) { DateRangeParam range = (DateRangeParam) theParam; p = createPredicateDateFromRange(theBuilder, theFrom, range); } else { throw new IllegalArgumentException("Invalid token type: " + theParam.getClass()); } return p; } private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, From<ResourceIndexedSearchParamDate, ResourceIndexedSearchParamDate> theFrom, DateRangeParam theRange) { Date lowerBound = theRange.getLowerBoundAsInstant(); Date upperBound = theRange.getUpperBoundAsInstant(); Predicate lb = null; if (lowerBound != null) { Predicate gt = theBuilder.greaterThanOrEqualTo(theFrom.<Date>get("myValueLow"), lowerBound); Predicate lt = theBuilder.greaterThanOrEqualTo(theFrom.<Date>get("myValueHigh"), lowerBound); if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) { lb = gt; } else { lb = theBuilder.or(gt, lt); } } Predicate ub = null; if (upperBound != null) { Predicate gt = theBuilder.lessThanOrEqualTo(theFrom.<Date>get("myValueLow"), upperBound); Predicate lt = theBuilder.lessThanOrEqualTo(theFrom.<Date>get("myValueHigh"), upperBound); if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { ub = lt; } else { ub = theBuilder.or(gt, lt); } } if (lb != null && ub != null) { return (theBuilder.and(lb, ub)); } else if (lb != null) { return (lb); } else { return (ub); } } private void createPredicateLastUpdatedForIndexedSearchParam(CriteriaBuilder builder, Root<? extends BaseResourceIndexedSearchParam> from, List<Predicate> predicates) { DateRangeParam lastUpdated = myParams.getLastUpdatedAndRemove(); if (lastUpdated != null) { From<BaseResourceIndexedSearchParam, ResourceTable> defJoin = from.join("myResource", JoinType.INNER); List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(lastUpdated, builder, defJoin); predicates.addAll(lastUpdatedPredicates); } } private void createPredicateLastUpdatedForResourceLink(CriteriaBuilder builder, Root<ResourceLink> from, List<Predicate> predicates) { DateRangeParam lastUpdated = myParams.getLastUpdatedAndRemove(); if (lastUpdated != null) { From<BaseResourceIndexedSearchParam, ResourceTable> defJoin = from.join("mySourceResource", JoinType.INNER); List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(lastUpdated, builder, defJoin); predicates.addAll(lastUpdatedPredicates); } } private void createPredicateLastUpdatedForResourceTable(CriteriaBuilder builder, Root<ResourceTable> from, List<Predicate> predicates) { predicates.addAll(createLastUpdatedPredicates(myParams.getLastUpdatedAndRemove(), builder, from)); } private Predicate createPredicateNumeric(CriteriaBuilder builder, IQueryParameterType params, ParamPrefixEnum cmpValue, BigDecimal valueValue, final Expression<BigDecimal> path, String invalidMessageName, String theValueString) { Predicate num; switch (cmpValue) { case GREATERTHAN: num = builder.gt(path, valueValue); break; case GREATERTHAN_OR_EQUALS: num = builder.ge(path, valueValue); break; case LESSTHAN: num = builder.lt(path, valueValue); break; case LESSTHAN_OR_EQUALS: num = builder.le(path, valueValue); break; case APPROXIMATE: case EQUAL: case NOT_EQUAL: BigDecimal mul = calculateFuzzAmount(cmpValue, valueValue); BigDecimal low = valueValue.subtract(mul, MathContext.DECIMAL64); BigDecimal high = valueValue.add(mul, MathContext.DECIMAL64); Predicate lowPred; Predicate highPred; if (cmpValue != ParamPrefixEnum.NOT_EQUAL) { lowPred = builder.ge(path.as(BigDecimal.class), low); highPred = builder.le(path.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(path.as(BigDecimal.class), low); highPred = builder.gt(path.as(BigDecimal.class), high); num = builder.or(lowPred, highPred); } break; default: String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, invalidMessageName, cmpValue.getValue(), params.getValueAsQueryToken(myContext)); throw new InvalidRequestException(msg); } return num; } private Predicate createPredicateQuantity(CriteriaBuilder theBuilder, From<ResourceIndexedSearchParamQuantity, ResourceIndexedSearchParamQuantity> theFrom, IQueryParameterType theParam) { String systemValue; String unitsValue; ParamPrefixEnum cmpValue; BigDecimal valueValue; String valueString; if (theParam instanceof BaseQuantityDt) { BaseQuantityDt param = (BaseQuantityDt) theParam; systemValue = param.getSystemElement().getValueAsString(); unitsValue = param.getUnitsElement().getValueAsString(); cmpValue = ParamPrefixEnum.forDstu1Value(param.getComparatorElement().getValueAsString()); valueValue = param.getValueElement().getValue(); valueString = param.getValueElement().getValueAsString(); } else if (theParam instanceof QuantityParam) { QuantityParam param = (QuantityParam) theParam; systemValue = param.getSystem(); unitsValue = param.getUnits(); cmpValue = param.getPrefix(); valueValue = param.getValue(); valueString = param.getValueAsString(); } else { throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); } Predicate system = null; if (!isBlank(systemValue)) { system = theBuilder.equal(theFrom.get("mySystem"), systemValue); } Predicate code = null; if (!isBlank(unitsValue)) { code = theBuilder.equal(theFrom.get("myUnits"), unitsValue); } cmpValue = ObjectUtils.defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); final Expression<BigDecimal> path = theFrom.get("myValue"); String invalidMessageName = "invalidQuantityPrefix"; Predicate num = createPredicateNumeric(theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName, valueString); Predicate singleCode; if (system == null && code == null) { singleCode = num; } else if (system == null) { singleCode = theBuilder.and(code, num); } else if (code == null) { singleCode = theBuilder.and(system, num); } else { singleCode = theBuilder.and(system, code, num); } return singleCode; } private void createPredicateResourceId(CriteriaBuilder builder, CriteriaQuery<?> cq, List<Predicate> thePredicates, Expression<Long> theExpression) { if (myParams.isPersistResults()) { if (mySearchEntity.getTotalCount() > -1) { Subquery<Long> subQ = cq.subquery(Long.class); Root<SearchResult> subQfrom = subQ.from(SearchResult.class); subQ.select(subQfrom.get("myResourcePid").as(Long.class)); Predicate subQname = builder.equal(subQfrom.get("mySearch"), mySearchEntity); subQ.where(subQname); thePredicates.add(theExpression.in(subQ)); } } else { if (myPids != null) { thePredicates.add(theExpression.in(myPids)); } } } private Predicate createPredicateString(IQueryParameterType theParameter, String theParamName, CriteriaBuilder theBuilder, From<ResourceIndexedSearchParamString, ResourceIndexedSearchParamString> theFrom) { String rawSearchTerm; if (theParameter instanceof TokenParam) { TokenParam id = (TokenParam) theParameter; if (!id.isText()) { throw new IllegalStateException("Trying to process a text search on a non-text token parameter"); } rawSearchTerm = id.getValue(); } else if (theParameter instanceof StringParam) { StringParam id = (StringParam) theParameter; rawSearchTerm = id.getValue(); } else if (theParameter instanceof IPrimitiveDatatype<?>) { IPrimitiveDatatype<?> id = (IPrimitiveDatatype<?>) theParameter; rawSearchTerm = id.getValueAsString(); } else { throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass()); } 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); } String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm); likeExpression = createLeftMatchLikeExpression(likeExpression); Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); if (theParameter instanceof StringParam && ((StringParam) theParameter).isExact()) { Predicate exactCode = theBuilder.equal(theFrom.get("myValueExact"), rawSearchTerm); singleCode = theBuilder.and(singleCode, exactCode); } return singleCode; } private List<Predicate> createPredicateTagList(Path<TagDefinition> theDefJoin, CriteriaBuilder theBuilder, TagTypeEnum theTagType, List<Pair<String, String>> theTokens) { Predicate typePrediate = theBuilder.equal(theDefJoin.get("myTagType"), theTagType); List<Predicate> orPredicates = Lists.newArrayList(); for (Pair<String, String> next : theTokens) { Predicate codePrediate = theBuilder.equal(theDefJoin.get("myCode"), next.getRight()); if (isNotBlank(next.getLeft())) { Predicate systemPrediate = theBuilder.equal(theDefJoin.get("mySystem"), next.getLeft()); orPredicates.add(theBuilder.and(typePrediate, systemPrediate, codePrediate)); } else { orPredicates.add(theBuilder.and(typePrediate, codePrediate)); } } return orPredicates; } private Predicate createPredicateToken(IQueryParameterType theParameter, String theParamName, CriteriaBuilder theBuilder, From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> theFrom) { String code; String system; TokenParamModifier modifier = null; if (theParameter instanceof TokenParam) { TokenParam id = (TokenParam) theParameter; system = id.getSystem(); code = (id.getValue()); modifier = id.getModifier(); } else if (theParameter instanceof BaseIdentifierDt) { BaseIdentifierDt id = (BaseIdentifierDt) theParameter; system = id.getSystemElement().getValueAsString(); code = (id.getValueElement().getValue()); } else if (theParameter instanceof BaseCodingDt) { BaseCodingDt id = (BaseCodingDt) theParameter; system = id.getSystemElement().getValueAsString(); code = (id.getCodeElement().getValue()); } else { throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass()); } 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); } 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); } /* * Process token modifiers (:in, :below, :above) */ List<VersionIndependentConcept> codes = null; if (modifier == TokenParamModifier.IN) { codes = myTerminologySvc.expandValueSet(code); } else if (modifier == TokenParamModifier.ABOVE) { system = determineSystemIfMissing(theParamName, code, system); codes = myTerminologySvc.findCodesAbove(system, code); } else if (modifier == TokenParamModifier.BELOW) { system = determineSystemIfMissing(theParamName, code, system); codes = myTerminologySvc.findCodesBelow(system, code); } if (codes != null) { if (codes.isEmpty()) { return null; } List<Predicate> orPredicates = new ArrayList<Predicate>(); for (VersionIndependentConcept nextCode : codes) { Predicate systemPredicate = theBuilder.equal(theFrom.get("mySystem"), nextCode.getSystem()); Predicate codePredicate = theBuilder.equal(theFrom.get("myValue"), nextCode.getCode()); orPredicates.add(theBuilder.and(systemPredicate, codePredicate)); } return theBuilder.or(orPredicates.toArray(new Predicate[orPredicates.size()])); } /* * Ok, this is a normal query */ ArrayList<Predicate> singleCodePredicates = (new ArrayList<Predicate>()); if (StringUtils.isNotBlank(system)) { singleCodePredicates.add(theBuilder.equal(theFrom.get("mySystem"), system)); } else if (system == null) { // don't check the system } else { // If the system is "", we only match on null systems singleCodePredicates.add(theBuilder.isNull(theFrom.get("mySystem"))); } if (StringUtils.isNotBlank(code)) { singleCodePredicates.add(theBuilder.equal(theFrom.get("myValue"), code)); } else { /* * As of HAPI FHIR 1.5, if the client searched for a token with a system but no specified value this means to * match all tokens with the given value. * * I'm not sure I agree with this, but hey.. FHIR-I voted and this was the result :) */ // singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue"))); } Predicate singleCode = theBuilder.and(toArray(singleCodePredicates)); return singleCode; } private String determineSystemIfMissing(String theParamName, String code, String system) { if (system == null) { RuntimeSearchParam param = getSearchParam(theParamName); if (param != null) { Set<String> valueSetUris = Sets.newHashSet(); for (String nextPath : param.getPathsSplit()) { BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, nextPath); if (def instanceof BaseRuntimeDeclaredChildDefinition) { String valueSet = ((BaseRuntimeDeclaredChildDefinition) def).getBindingValueSet(); if (isNotBlank(valueSet)) { valueSetUris.add(valueSet); } } } if (valueSetUris.size() == 1) { List<VersionIndependentConcept> candidateCodes = myTerminologySvc .expandValueSet(valueSetUris.iterator().next()); for (VersionIndependentConcept nextCandidate : candidateCodes) { if (nextCandidate.getCode().equals(code)) { system = nextCandidate.getSystem(); break; } } } } } return system; } private Predicate createResourceLinkPathPredicate(String theParamName, Root<? extends ResourceLink> from) { return createResourceLinkPathPredicate(myContext, theParamName, from, myResourceType); } private static Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, Root<? extends ResourceLink> from, Class<? extends IBaseResource> resourceType) { RuntimeSearchParam param = theContext.getResourceDefinition(resourceType).getSearchParam(theParamName); List<String> path = param.getPathsSplit(); Predicate type = from.get("mySourcePath").in(path); return type; } private TypedQuery<Long> createSearchAllByTypeQuery(DateRangeParam theLastUpdated) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); List<Predicate> predicates = new ArrayList<Predicate>(); predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.isNull(from.get("myDeleted"))); if (theLastUpdated != null) { predicates.addAll(createLastUpdatedPredicates(theLastUpdated, builder, from)); } cq.where(toArray(predicates)); TypedQuery<Long> query = myEntityManager.createQuery(cq); return query; } private void createSort(CriteriaBuilder theBuilder, Root<ResourceTable> theFrom, SortSpec theSort, List<Order> theOrders, List<Predicate> thePredicates) { if (theSort == null || isBlank(theSort.getParamName())) { return; } if (BaseResource.SP_RES_ID.equals(theSort.getParamName())) { From<?, ?> forcedIdJoin = theFrom.join("myForcedId", JoinType.LEFT); if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { theOrders.add(theBuilder.asc(forcedIdJoin.get("myForcedId"))); theOrders.add(theBuilder.asc(theFrom.get("myId"))); } else { theOrders.add(theBuilder.desc(forcedIdJoin.get("myForcedId"))); theOrders.add(theBuilder.desc(theFrom.get("myId"))); } createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates); return; } if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) { if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { theOrders.add(theBuilder.asc(theFrom.get("myUpdated"))); } else { theOrders.add(theBuilder.desc(theFrom.get("myUpdated"))); } createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates); return; } RuntimeSearchParam param = getSearchParam(theSort.getParamName()); if (param == null) { throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'"); } String joinAttrName; String[] sortAttrName; switch (param.getParamType()) { case STRING: joinAttrName = "myParamsString"; sortAttrName = new String[] { "myValueExact" }; break; case DATE: joinAttrName = "myParamsDate"; sortAttrName = new String[] { "myValueLow" }; break; case REFERENCE: joinAttrName = "myResourceLinks"; sortAttrName = new String[] { "myTargetResourcePid" }; break; case TOKEN: joinAttrName = "myParamsToken"; sortAttrName = new String[] { "mySystem", "myValue" }; break; case NUMBER: joinAttrName = "myParamsNumber"; sortAttrName = new String[] { "myValue" }; break; case URI: joinAttrName = "myParamsUri"; sortAttrName = new String[] { "myUri" }; break; case QUANTITY: joinAttrName = "myParamsQuantity"; sortAttrName = new String[] { "myValue" }; break; default: throw new InvalidRequestException("This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName()); } From<?, ?> stringJoin = theFrom.join(joinAttrName, JoinType.INNER); if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { thePredicates.add(stringJoin.get("mySourcePath").as(String.class).in(param.getPathsSplit())); } else { thePredicates.add(theBuilder.equal(stringJoin.get("myParamName"), theSort.getParamName())); } // Predicate p = theBuilder.equal(stringJoin.get("myParamName"), theSort.getParamName()); // Predicate pn = theBuilder.isNull(stringJoin.get("myParamName")); // thePredicates.add(theBuilder.or(p, pn)); for (String next : sortAttrName) { if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { theOrders.add(theBuilder.asc(stringJoin.get(next))); } else { theOrders.add(theBuilder.desc(stringJoin.get(next))); } } createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates); } private RuntimeSearchParam getSearchParam(String theParamName) { RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceType); RuntimeSearchParam param = resourceDef.getSearchParam(theParamName); return param; } public Set<Long> doGetPids() { if (myParams.isPersistResults()) { HashSet<Long> retVal = new HashSet<Long>(); for (SearchResult next : mySearchResultDao.findWithSearchUuid(mySearchEntity)) { retVal.add(next.getResourcePid()); } return retVal; } else { return new HashSet<Long>(myPids); } } private boolean doHaveNoResults() { if (myParams.isPersistResults()) { return mySearchEntity.getTotalCount() == 0; } else { return myPids != null && myPids.isEmpty(); } } private void doInitializeSearch() { if (mySearchEntity == null) { reinitializeSearch(); } } private IBundleProvider doReturnProvider() { if (myParams.isPersistResults()) { return new PersistedJpaBundleProvider(mySearchEntity.getUuid(), myCallingDao); } else { if (myPids == null) { return new SimpleBundleProvider(); } else { return new BundleProviderInMemory(myPids); } } } private void doSetPids(Collection<Long> thePids) { if (myParams.isPersistResults()) { if (mySearchEntity.getTotalCount() != null) { reinitializeSearch(); } LinkedHashSet<SearchResult> results = new LinkedHashSet<SearchResult>(); int index = 0; for (Long next : thePids) { SearchResult nextResult = new SearchResult(mySearchEntity); nextResult.setResourcePid(next); nextResult.setOrder(index); results.add(nextResult); index++; } mySearchResultDao.save(results); mySearchEntity.setTotalCount(results.size()); mySearchEntity = myEntityManager.merge(mySearchEntity); myEntityManager.flush(); } else { myPids = thePids; } } private void filterResourceIdsByLastUpdated(final DateRangeParam theLastUpdated) { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from); createPredicateResourceId(builder, cq, lastUpdatedPredicates, from.get("myId").as(Long.class)); cq.where(SearchBuilder.toArray(lastUpdatedPredicates)); TypedQuery<Long> query = myEntityManager.createQuery(cq); List<Long> resultList = query.getResultList(); doSetPids(resultList); } private void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation) { EntityManager entityManager = myEntityManager; FhirContext context = myContext; BaseHapiFhirDao<?> dao = myCallingDao; loadResourcesByPid(theIncludePids, theResourceListToPopulate, theRevIncludedPids, theForHistoryOperation, entityManager, context, dao); } private void processSort(final SearchParameterMap theParams) { // Set<Long> loadPids = theLoadPids; if (theParams.getSort() != null && isNotBlank(theParams.getSort().getParamName())) { List<Order> orders = new ArrayList<Order>(); List<Predicate> predicates = new ArrayList<Predicate>(); CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Tuple> cq = builder.createTupleQuery(); Root<ResourceTable> from = cq.from(ResourceTable.class); createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); createSort(builder, from, theParams.getSort(), orders, predicates); if (orders.size() > 0) { // TODO: why do we need the existing list for this join to work? Collection<Long> originalPids = doGetPids(); LinkedHashSet<Long> loadPids = new LinkedHashSet<Long>(); cq.multiselect(from.get("myId").as(Long.class)); cq.where(toArray(predicates)); cq.orderBy(orders); TypedQuery<Tuple> query = myEntityManager.createQuery(cq); for (Tuple next : query.getResultList()) { loadPids.add(next.get(0, Long.class)); } ourLog.debug("Sort PID order is now: {}", loadPids); ArrayList<Long> pids = new ArrayList<Long>(loadPids); // Any ressources which weren't matched by the sort get added to the bottom for (Long next : originalPids) { if (loadPids.contains(next) == false) { pids.add(next); } } doSetPids(pids); } } } private void reinitializeSearch() { mySearchEntity = new Search(); mySearchEntity.setUuid(UUID.randomUUID().toString()); mySearchEntity.setCreated(new Date()); mySearchEntity.setTotalCount(-1); mySearchEntity.setPreferredPageSize(myParams.getCount()); mySearchEntity.setSearchType( myParams.getEverythingMode() != null ? SearchTypeEnum.EVERYTHING : SearchTypeEnum.SEARCH); mySearchEntity.setLastUpdated(myParams.getLastUpdated()); for (Include next : myParams.getIncludes()) { mySearchEntity.getIncludes() .add(new SearchInclude(mySearchEntity, next.getValue(), false, next.isRecurse())); } for (Include next : myParams.getRevIncludes()) { mySearchEntity.getIncludes() .add(new SearchInclude(mySearchEntity, next.getValue(), true, next.isRecurse())); } if (myParams.isPersistResults()) { myEntityManager.persist(mySearchEntity); for (SearchInclude next : mySearchEntity.getIncludes()) { myEntityManager.persist(next); } } } public IBundleProvider search(final SearchParameterMap theParams) { myParams = theParams; StopWatch w = new StopWatch(); doInitializeSearch(); DateRangeParam lu = theParams.getLastUpdated(); // Collection<Long> loadPids; if (theParams.getEverythingMode() != null) { Long pid = null; if (theParams.get(BaseResource.SP_RES_ID) != null) { StringParam idParm = (StringParam) theParams.get(BaseResource.SP_RES_ID).get(0).get(0); pid = BaseHapiFhirDao.translateForcedIdToPid(myResourceName, idParm.getValue(), myForcedIdDao); } if (theParams.containsKey(Constants.PARAM_CONTENT) || theParams.containsKey(Constants.PARAM_TEXT)) { List<Long> pids = mySearchDao.everything(myResourceName, theParams); if (pids.isEmpty()) { return doReturnProvider(); } doSetPids(pids); } else { CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); CriteriaQuery<Tuple> cq = builder.createTupleQuery(); Root<ResourceTable> from = cq.from(ResourceTable.class); List<Predicate> predicates = new ArrayList<Predicate>(); if (pid != null) { predicates.add(builder.equal(from.get("myId"), pid)); } predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); predicates.add(builder.isNull(from.get("myDeleted"))); cq.where(builder.and(SearchBuilder.toArray(predicates))); Join<Object, Object> join = from.join("myIncomingResourceLinks", JoinType.LEFT); cq.multiselect(from.get("myId").as(Long.class), join.get("mySourceResourcePid").as(Long.class)); TypedQuery<Tuple> query = myEntityManager.createQuery(cq); Set<Long> pids = new HashSet<Long>(); for (Tuple next : query.getResultList()) { pids.add(next.get(0, Long.class)); Long nextLong = next.get(1, Long.class); if (nextLong != null) { pids.add(nextLong); } } doSetPids(pids); } } else if (theParams.isEmpty()) { TypedQuery<Long> query = createSearchAllByTypeQuery(lu); doSetPids(query.getResultList()); } else { if (mySearchDao == null) { if (theParams.containsKey(Constants.PARAM_TEXT)) { throw new InvalidRequestException( "Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_TEXT); } else if (theParams.containsKey(Constants.PARAM_CONTENT)) { throw new InvalidRequestException( "Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_CONTENT); } } else { List<Long> searchResultPids = mySearchDao.search(myResourceName, theParams); if (searchResultPids != null) { if (searchResultPids.isEmpty()) { return doReturnProvider(); } doSetPids(searchResultPids); } } if (!theParams.isEmpty()) { searchForIdsWithAndOr(theParams, lu); } } // // Load _include and _revinclude before filter and sort in everything mode // if (theParams.getEverythingMode() != null) { // if (theParams.getRevIncludes() != null && theParams.getRevIncludes().isEmpty() == false) { // loadPids.addAll(loadReverseIncludes(loadPids, theParams.getRevIncludes(), true, // theParams.getEverythingMode())); // loadPids.addAll(loadReverseIncludes(loadPids, theParams.getIncludes(), false, theParams.getEverythingMode())); // } // } if (doHaveNoResults()) { return doReturnProvider(); } // Handle _lastUpdated if (lu != null) { filterResourceIdsByLastUpdated(lu); if (doHaveNoResults()) { return doReturnProvider(); } } // Handle sorting if any was provided processSort(theParams); ourLog.info(" {} on {} in {}ms", new Object[] { myResourceName, theParams, w.getMillisAndRestart() }); return doReturnProvider(); } private void searchForIdsWithAndOr(SearchParameterMap theParams, DateRangeParam theLastUpdated) { SearchParameterMap params = theParams; if (params == null) { params = new SearchParameterMap(); } myParams = theParams; doInitializeSearch(); RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceType); for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : params.entrySet()) { String nextParamName = nextParamEntry.getKey(); if (nextParamName.equals(BaseResource.SP_RES_ID)) { if (nextParamEntry.getValue().isEmpty()) { continue; } else { for (List<? extends IQueryParameterType> nextValue : nextParamEntry.getValue()) { Set<Long> joinPids = new HashSet<Long>(); if (nextValue == null || nextValue.size() == 0) { continue; } else { for (IQueryParameterType next : nextValue) { String value = next.getValueAsQueryToken(myContext); IIdType valueId = new IdDt(value); try { BaseHasResource entity = myCallingDao.readEntity(valueId); if (entity.getDeleted() != null) { continue; } joinPids.add(entity.getId()); } catch (ResourceNotFoundException e) { // This isn't an error, just means no result found } } if (joinPids.isEmpty()) { doSetPids(new HashSet<Long>()); return; } } addPredicateId(joinPids); if (doHaveNoResults()) { return; } } } } else if (nextParamName.equals(BaseResource.SP_RES_LANGUAGE)) { addPredicateLanguage(nextParamEntry.getValue()); } else if (nextParamName.equals(Constants.PARAM_HAS)) { addPredicateHas(nextParamEntry.getValue(), theLastUpdated); } else if (nextParamName.equals(Constants.PARAM_TAG) || nextParamName.equals(Constants.PARAM_PROFILE) || nextParamName.equals(Constants.PARAM_SECURITY)) { addPredicateTag(nextParamEntry.getValue(), nextParamName, theLastUpdated); } else { RuntimeSearchParam nextParamDef = resourceDef.getSearchParam(nextParamName); if (nextParamDef != null) { switch (nextParamDef.getParamType()) { case DATE: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateDate(nextParamName, nextAnd); if (doHaveNoResults()) { return; } } break; case QUANTITY: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateQuantity(nextParamName, nextAnd); if (doHaveNoResults()) { return; } } break; case REFERENCE: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateReference(nextParamName, nextAnd); if (doHaveNoResults()) { return; } } break; case STRING: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateString(nextParamName, nextAnd); if (doHaveNoResults()) { return; } } break; case TOKEN: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateToken(nextParamName, nextAnd); if (doHaveNoResults()) { return; } } break; case NUMBER: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateNumber(nextParamName, nextAnd); if (doHaveNoResults()) { return; } } break; case COMPOSITE: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateComposite(nextParamDef, nextAnd); if (doHaveNoResults()) { return; } } break; case URI: for (List<? extends IQueryParameterType> nextAnd : nextParamEntry.getValue()) { addPredicateUri(nextParamName, nextAnd); if (doHaveNoResults()) { return; } } break; } } } if (doHaveNoResults()) { return; } } } public void setType(Class<? extends IBaseResource> theResourceType, String theResourceName) { myResourceType = theResourceType; myResourceName = theResourceName; } 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<RuntimeSearchParam> 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<IQueryParameterType, IQueryParameterType>(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; } private IQueryParameterType toParameterType(RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) { IQueryParameterType qp = toParameterType(theParam); qp.setValueAsQueryToken(myContext, theParam.getName(), theQualifier, theValueAsQueryToken); return qp; } /** * Figures out the tolerance for a search. For example, if the user is searching for <code>4.00</code>, this method * returns <code>0.005</code> because we shold actually match values which are * <code>4 (+/-) 0.005</code> according to the FHIR specs. */ static BigDecimal calculateFuzzAmount(ParamPrefixEnum cmpValue, BigDecimal theValue) { if (cmpValue == ParamPrefixEnum.APPROXIMATE) { return theValue.multiply(new BigDecimal(0.1)); } else { String plainString = theValue.toPlainString(); int dotIdx = plainString.indexOf('.'); if (dotIdx == -1) { return new BigDecimal(0.5); } int precision = plainString.length() - (dotIdx); double mul = Math.pow(10, -precision); double val = mul * 5.0d; return new BigDecimal(val); } } private static List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) { List<Predicate> lastUpdatedPredicates = new ArrayList<Predicate>(); if (theLastUpdated != null) { if (theLastUpdated.getLowerBoundAsInstant() != null) { ourLog.info("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant())); Predicate predicateLower = builder.greaterThanOrEqualTo(from.<Date>get("myUpdated"), theLastUpdated.getLowerBoundAsInstant()); lastUpdatedPredicates.add(predicateLower); } if (theLastUpdated.getUpperBoundAsInstant() != null) { Predicate predicateUpper = builder.lessThanOrEqualTo(from.<Date>get("myUpdated"), theLastUpdated.getUpperBoundAsInstant()); lastUpdatedPredicates.add(predicateUpper); } } return lastUpdatedPredicates; } private static String createLeftMatchLikeExpression(String likeExpression) { return likeExpression.replace("%", "[%]") + "%"; } private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) { CriteriaBuilder builder = theEntityManager.getCriteriaBuilder(); CriteriaQuery<Long> cq = builder.createQuery(Long.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.select(from.get("myId").as(Long.class)); List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from); lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(thePids)); cq.where(SearchBuilder.toArray(lastUpdatedPredicates)); TypedQuery<Long> query = theEntityManager.createQuery(cq); List<Long> resultList = query.getResultList(); return resultList; } public static void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation, EntityManager entityManager, FhirContext context, IDao theDao) { if (theIncludePids.isEmpty()) { return; } Map<Long, Integer> position = new HashMap<Long, Integer>(); for (Long next : theIncludePids) { position.put(next, theResourceListToPopulate.size()); theResourceListToPopulate.add(null); } CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<ResourceTable> cq = builder.createQuery(ResourceTable.class); Root<ResourceTable> from = cq.from(ResourceTable.class); cq.where(from.get("myId").in(theIncludePids)); TypedQuery<ResourceTable> q = entityManager.createQuery(cq); for (ResourceTable next : q.getResultList()) { Class<? extends IBaseResource> resourceType = context.getResourceDefinition(next.getResourceType()) .getImplementingClass(); IBaseResource resource = (IBaseResource) theDao.toResource(resourceType, next, theForHistoryOperation); Integer index = position.get(next.getId()); if (index == null) { ourLog.warn("Got back unexpected resource PID {}", next.getId()); continue; } if (resource instanceof IResource) { if (theRevIncludedPids.contains(next.getId())) { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.INCLUDE); } else { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.MATCH); } } else { if (theRevIncludedPids.contains(next.getId())) { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.INCLUDE.getCode()); } else { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.MATCH.getCode()); } } theResourceListToPopulate.set(index, resource); } } /** * THIS SHOULD RETURN HASHSET and not jsut Set because we add to it later (so it can't be Collections.emptySet()) * * @param theLastUpdated */ public static HashSet<Long> loadReverseIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes, boolean theReverseMode, DateRangeParam theLastUpdated) { if (theMatches.size() == 0) { return new HashSet<Long>(); } if (theRevIncludes == null || theRevIncludes.isEmpty()) { return new HashSet<Long>(); } String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid"; Collection<Long> nextRoundMatches = theMatches; HashSet<Long> allAdded = new HashSet<Long>(); HashSet<Long> original = new HashSet<Long>(theMatches); ArrayList<Include> includes = new ArrayList<Include>(theRevIncludes); int roundCounts = 0; StopWatch w = new StopWatch(); boolean addedSomeThisRound; do { roundCounts++; HashSet<Long> pidsToInclude = new HashSet<Long>(); Set<Long> nextRoundOmit = new HashSet<Long>(); for (Iterator<Include> iter = includes.iterator(); iter.hasNext();) { Include nextInclude = iter.next(); if (nextInclude.isRecurse() == false) { iter.remove(); } boolean matchAll = "*".equals(nextInclude.getValue()); if (matchAll) { String sql; sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids)"; TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class); q.setParameter("target_pids", nextRoundMatches); List<ResourceLink> results = q.getResultList(); for (ResourceLink resourceLink : results) { if (theReverseMode) { // if (theEverythingModeEnum.isEncounter()) { // if (resourceLink.getSourcePath().equals("Encounter.subject") || // resourceLink.getSourcePath().equals("Encounter.patient")) { // nextRoundOmit.add(resourceLink.getSourceResourcePid()); // } // } pidsToInclude.add(resourceLink.getSourceResourcePid()); } else { pidsToInclude.add(resourceLink.getTargetResourcePid()); } } } else { List<String> paths; RuntimeSearchParam param = null; if (theContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) { paths = Collections.singletonList(nextInclude.getValue()); } else { String resType = nextInclude.getParamType(); if (isBlank(resType)) { continue; } RuntimeResourceDefinition def = theContext.getResourceDefinition(resType); if (def == null) { ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue()); continue; } String paramName = nextInclude.getParamName(); param = isNotBlank(paramName) ? def.getSearchParam(paramName) : null; if (param == null) { ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue()); continue; } paths = param.getPathsSplit(); } String targetResourceType = defaultString(nextInclude.getParamTargetType(), null); for (String nextPath : paths) { String sql; boolean haveTargetTypesDefinedByParam = param != null && param.getTargets() != null && param.getTargets().isEmpty() == false; if (targetResourceType != null) { sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type"; } else if (haveTargetTypesDefinedByParam) { sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)"; } else { sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)"; } TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class); q.setParameter("src_path", nextPath); q.setParameter("target_pids", nextRoundMatches); if (targetResourceType != null) { q.setParameter("target_resource_type", targetResourceType); } else if (haveTargetTypesDefinedByParam) { q.setParameter("target_resource_types", param.getTargets()); } List<ResourceLink> results = q.getResultList(); for (ResourceLink resourceLink : results) { if (theReverseMode) { Long pid = resourceLink.getSourceResourcePid(); pidsToInclude.add(pid); } else { Long pid = resourceLink.getTargetResourcePid(); pidsToInclude.add(pid); } } } } } if (theLastUpdated != null && (theLastUpdated.getLowerBoundAsInstant() != null || theLastUpdated.getUpperBoundAsInstant() != null)) { pidsToInclude = new HashSet<Long>( filterResourceIdsByLastUpdated(theEntityManager, theLastUpdated, pidsToInclude)); } for (Long next : pidsToInclude) { if (original.contains(next) == false && allAdded.contains(next) == false) { theMatches.add(next); } } pidsToInclude.removeAll(nextRoundOmit); addedSomeThisRound = allAdded.addAll(pidsToInclude); 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() }); return allAdded; } static Predicate[] toArray(List<Predicate> thePredicates) { return thePredicates.toArray(new Predicate[thePredicates.size()]); } private final class BundleProviderInMemory implements IBundleProvider { private final ArrayList<Long> myPids; private BundleProviderInMemory(Collection<Long> thePids) { final ArrayList<Long> pids; if (!(thePids instanceof List)) { pids = new ArrayList<Long>(thePids); } else { pids = (ArrayList<Long>) thePids; } myPids = pids; } @Override public InstantDt getPublished() { return new InstantDt(mySearchEntity.getCreated()); } @Override public List<IBaseResource> getResources(final int theFromIndex, final int theToIndex) { TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager); return template.execute(new TransactionCallback<List<IBaseResource>>() { @Override public List<IBaseResource> doInTransaction(TransactionStatus theStatus) { List<Long> pidsSubList = myPids.subList(theFromIndex, theToIndex); // Load includes pidsSubList = new ArrayList<Long>(pidsSubList); Set<Long> revIncludedPids = new HashSet<Long>(); if (myParams.getEverythingMode() == null) { revIncludedPids.addAll(loadReverseIncludes(myContext, myEntityManager, pidsSubList, myParams.getRevIncludes(), true, myParams.getLastUpdated())); } revIncludedPids.addAll(loadReverseIncludes(myContext, myEntityManager, pidsSubList, myParams.getIncludes(), false, myParams.getLastUpdated())); // Execute the query and make sure we return distinct results List<IBaseResource> resources = new ArrayList<IBaseResource>(); loadResourcesByPid(pidsSubList, resources, revIncludedPids, false); return resources; } }); } @Override public Integer preferredPageSize() { return myParams.getCount(); } @Override public int size() { return myPids.size(); } } }