View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   /*
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2018 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   * http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.context.*;
24  import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
25  import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
26  import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
27  import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
28  import ca.uhn.fhir.jpa.entity.*;
29  import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
30  import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
31  import ca.uhn.fhir.jpa.term.VersionIndependentConcept;
32  import ca.uhn.fhir.jpa.util.BaseIterator;
33  import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
34  import ca.uhn.fhir.model.api.*;
35  import ca.uhn.fhir.model.base.composite.BaseCodingDt;
36  import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
37  import ca.uhn.fhir.model.base.composite.BaseQuantityDt;
38  import ca.uhn.fhir.model.primitive.IdDt;
39  import ca.uhn.fhir.model.primitive.InstantDt;
40  import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
41  import ca.uhn.fhir.parser.DataFormatException;
42  import ca.uhn.fhir.rest.api.Constants;
43  import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
44  import ca.uhn.fhir.rest.api.SortOrderEnum;
45  import ca.uhn.fhir.rest.api.SortSpec;
46  import ca.uhn.fhir.rest.param.*;
47  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
48  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
49  import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
50  import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
51  import ca.uhn.fhir.util.StopWatch;
52  import ca.uhn.fhir.util.UrlUtil;
53  import com.google.common.annotations.VisibleForTesting;
54  import com.google.common.collect.Lists;
55  import com.google.common.collect.Maps;
56  import com.google.common.collect.Sets;
57  import org.apache.commons.lang3.ObjectUtils;
58  import org.apache.commons.lang3.Validate;
59  import org.apache.commons.lang3.builder.EqualsBuilder;
60  import org.apache.commons.lang3.builder.HashCodeBuilder;
61  import org.apache.commons.lang3.tuple.Pair;
62  import org.hibernate.ScrollMode;
63  import org.hibernate.ScrollableResults;
64  import org.hibernate.query.Query;
65  import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
66  import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPredicate;
67  import org.hl7.fhir.dstu3.model.BaseResource;
68  import org.hl7.fhir.instance.model.api.IAnyResource;
69  import org.hl7.fhir.instance.model.api.IBaseResource;
70  import org.hl7.fhir.instance.model.api.IIdType;
71  
72  import javax.annotation.Nonnull;
73  import javax.persistence.EntityManager;
74  import javax.persistence.TypedQuery;
75  import javax.persistence.criteria.*;
76  import java.math.BigDecimal;
77  import java.math.MathContext;
78  import java.util.*;
79  import java.util.Map.Entry;
80  
81  import static org.apache.commons.lang3.StringUtils.*;
82  
83  /**
84   * The SearchBuilder is responsible for actually forming the SQL query that handles
85   * searches for resources
86   */
87  public class SearchBuilder implements ISearchBuilder {
88  
89  	private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<Long>());
90  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class);
91  	private static Long NO_MORE = -1L;
92  	private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest;
93  	private static SearchParameterMap ourLastHandlerParamsForUnitTest;
94  	private static String ourLastHandlerThreadForUnitTest;
95  	private static boolean ourTrackHandlersForUnitTest;
96  	private List<Long> myAlsoIncludePids;
97  	private CriteriaBuilder myBuilder;
98  	private BaseHapiFhirDao<?> myCallingDao;
99  	private FhirContext myContext;
100 	private EntityManager myEntityManager;
101 	private IForcedIdDao myForcedIdDao;
102 	private IFulltextSearchSvc myFulltextSearchSvc;
103 	private Map<JoinKey, Join<?, ?>> myIndexJoins = Maps.newHashMap();
104 	private SearchParameterMap myParams;
105 	private ArrayList<Predicate> myPredicates;
106 	private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
107 	private String myResourceName;
108 	private AbstractQuery<Long> myResourceTableQuery;
109 	private Root<ResourceTable> myResourceTableRoot;
110 	private Class<? extends IBaseResource> myResourceType;
111 	private ISearchParamRegistry mySearchParamRegistry;
112 	private String mySearchUuid;
113 	private IHapiTerminologySvc myTerminologySvc;
114 	private int myFetchSize;
115 
116 	protected IResourceTagDao myResourceTagDao;
117 	protected IResourceSearchViewDao myResourceSearchViewDao;
118 
119 	/**
120 	 * Constructor
121 	 */
122 	public SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager,
123 			IFulltextSearchSvc theFulltextSearchSvc, BaseHapiFhirDao<?> theDao,
124 			IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao,
125 			IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry,
126 			IResourceTagDao theResourceTagDao, IResourceSearchViewDao theResourceViewDao) {
127 		myContext = theFhirContext;
128 		myEntityManager = theEntityManager;
129 		myFulltextSearchSvc = theFulltextSearchSvc;
130 		myCallingDao = theDao;
131 		myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao;
132 		myForcedIdDao = theForcedIdDao;
133 		myTerminologySvc = theTerminologySvc;
134 		mySearchParamRegistry = theSearchParamRegistry;
135 		myResourceTagDao = theResourceTagDao;
136 		myResourceSearchViewDao = theResourceViewDao;
137 	}
138 
139 	private void addPredicateComposite(String theResourceName, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd) {
140 		// TODO: fail if missing is set for a composite query
141 
142 		IQueryParameterType or = theNextAnd.get(0);
143 		if (!(or instanceof CompositeParam<?, ?>)) {
144 			throw new InvalidRequestException("Invalid type for composite param (must be " + CompositeParam.class.getSimpleName() + ": " + or.getClass());
145 		}
146 		CompositeParam<?, ?> cp = (CompositeParam<?, ?>) or;
147 
148 		RuntimeSearchParam left = theParamDef.getCompositeOf().get(0);
149 		IQueryParameterType leftValue = cp.getLeftValue();
150 		myPredicates.add(createCompositeParamPart(theResourceName, myResourceTableRoot, left, leftValue));
151 
152 		RuntimeSearchParam right = theParamDef.getCompositeOf().get(1);
153 		IQueryParameterType rightValue = cp.getRightValue();
154 		myPredicates.add(createCompositeParamPart(theResourceName, myResourceTableRoot, right, rightValue));
155 
156 	}
157 
158 	private void addPredicateDate(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
159 
160 		Join<ResourceTable, ResourceIndexedSearchParamDate> join = createOrReuseJoin(JoinEnum.DATE, theParamName);
161 
162 		if (theList.get(0).getMissing() != null) {
163 			Boolean missing = theList.get(0).getMissing();
164 			addPredicateParamMissing(theResourceName, theParamName, missing, join);
165 			return;
166 		}
167 
168 		List<Predicate> codePredicates = new ArrayList<>();
169 		for (IQueryParameterType nextOr : theList) {
170 			IQueryParameterType params = nextOr;
171 			Predicate p = createPredicateDate(params, theResourceName, theParamName, myBuilder, join);
172 			codePredicates.add(p);
173 		}
174 
175 		Predicate orPredicates = myBuilder.or(toArray(codePredicates));
176 		myPredicates.add(orPredicates);
177 
178 	}
179 
180 	private void addPredicateHas(List<List<? extends IQueryParameterType>> theHasParameters) {
181 
182 		for (List<? extends IQueryParameterType> nextOrList : theHasParameters) {
183 
184 			StringBuilder valueBuilder = new StringBuilder();
185 			String targetResourceType = null;
186 			String owningParameter = null;
187 			String parameterName = null;
188 			for (IQueryParameterType nextParam : nextOrList) {
189 				HasParam next = (HasParam) nextParam;
190 				if (valueBuilder.length() > 0) {
191 					valueBuilder.append(',');
192 				}
193 				valueBuilder.append(UrlUtil.escapeUrlParam(next.getValueAsQueryToken(myContext)));
194 				targetResourceType = next.getTargetResourceType();
195 				owningParameter = next.getOwningFieldName();
196 				parameterName = next.getParameterName();
197 			}
198 
199 			if (valueBuilder.length() == 0) {
200 				continue;
201 			}
202 
203 			String matchUrl = targetResourceType + '?' + UrlUtil.escapeUrlParam(parameterName) + '=' + valueBuilder.toString();
204 			RuntimeResourceDefinition targetResourceDefinition;
205 			try {
206 				targetResourceDefinition = myContext.getResourceDefinition(targetResourceType);
207 			} catch (DataFormatException e) {
208 				throw new InvalidRequestException("Invalid resource type: " + targetResourceType);
209 			}
210 
211 			String paramName = parameterName.replaceAll("\\..*", "");
212 			RuntimeSearchParam owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, paramName);
213 			if (owningParameterDef == null) {
214 				throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName);
215 			}
216 
217 			owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, owningParameter);
218 			if (owningParameterDef == null) {
219 				throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + owningParameter);
220 			}
221 
222 			Class<? extends IBaseResource> resourceType = targetResourceDefinition.getImplementingClass();
223 			Set<Long> match = myCallingDao.processMatchUrl(matchUrl, resourceType);
224 			if (match.isEmpty()) {
225 				// Pick a PID that can never match
226 				match = Collections.singleton(-1L);
227 			}
228 
229 			Join<ResourceTable, ResourceLink> join = myResourceTableRoot.join("myIncomingResourceLinks", JoinType.LEFT);
230 
231 			Predicate predicate = join.get("mySourceResourcePid").in(match);
232 			myPredicates.add(predicate);
233 		}
234 	}
235 
236 	private void addPredicateLanguage(List<List<? extends IQueryParameterType>> theList) {
237 		for (List<? extends IQueryParameterType> nextList : theList) {
238 
239 			Set<String> values = new HashSet<String>();
240 			for (IQueryParameterType next : nextList) {
241 				if (next instanceof StringParam) {
242 					String nextValue = ((StringParam) next).getValue();
243 					if (isBlank(nextValue)) {
244 						continue;
245 					}
246 					values.add(nextValue);
247 				} else {
248 					throw new InternalErrorException("Lanugage parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName());
249 				}
250 			}
251 
252 			if (values.isEmpty()) {
253 				continue;
254 			}
255 
256 			Predicate predicate = myResourceTableRoot.get("myLanguage").as(String.class).in(values);
257 			myPredicates.add(predicate);
258 		}
259 
260 		return;
261 	}
262 
263 	private void addPredicateNumber(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
264 
265 		Join<ResourceTable, ResourceIndexedSearchParamNumber> join = createOrReuseJoin(JoinEnum.NUMBER, theParamName);
266 
267 		if (theList.get(0).getMissing() != null) {
268 			addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
269 			return;
270 		}
271 
272 		List<Predicate> codePredicates = new ArrayList<>();
273 		for (IQueryParameterType nextOr : theList) {
274 			IQueryParameterType params = nextOr;
275 
276 			if (params instanceof NumberParam) {
277 				NumberParam param = (NumberParam) params;
278 
279 				BigDecimal value = param.getValue();
280 				if (value == null) {
281 					continue;
282 				}
283 
284 				final Expression<BigDecimal> fromObj = join.get("myValue");
285 				ParamPrefixEnum prefix = ObjectUtils.defaultIfNull(param.getPrefix(), ParamPrefixEnum.EQUAL);
286 				String invalidMessageName = "invalidNumberPrefix";
287 
288 				Predicate predicateNumeric = createPredicateNumeric(theResourceName, theParamName, join, myBuilder, params, prefix, value, fromObj, invalidMessageName);
289 				Predicate predicateOuter = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, predicateNumeric );
290 				codePredicates.add(predicateOuter);
291 
292 			} else {
293 				throw new IllegalArgumentException("Invalid token type: " + params.getClass());
294 			}
295 
296 		}
297 
298 		myPredicates.add(myBuilder.or(toArray(codePredicates)));
299 	}
300 
301 	private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) {
302 		Join<ResourceTable, SearchParamPresent> paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT);
303 
304 		Expression<Long> hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class);
305 		Long hash = SearchParamPresent.calculateHashPresence(theResourceName, theParamName, !theMissing);
306 		myPredicates.add(myBuilder.equal(hashPresence, hash));
307 	}
308 
309 	private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join<ResourceTable, ? extends BaseResourceIndexedSearchParam> theJoin) {
310 
311 		myPredicates.add(myBuilder.equal(theJoin.get("myResourceType"), theResourceName));
312 		myPredicates.add(myBuilder.equal(theJoin.get("myParamName"), theParamName));
313 		myPredicates.add(myBuilder.equal(theJoin.get("myMissing"), theMissing));
314 	}
315 
316 	private void addPredicateQuantity(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
317 		Join<ResourceTable, ResourceIndexedSearchParamQuantity> join = createOrReuseJoin(JoinEnum.QUANTITY, theParamName);
318 
319 		if (theList.get(0).getMissing() != null) {
320 			addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
321 			return;
322 		}
323 
324 		List<Predicate> codePredicates = new ArrayList<>();
325 		for (IQueryParameterType nextOr : theList) {
326 
327 			Predicate singleCode = createPredicateQuantity(nextOr, theResourceName, theParamName, myBuilder, join);
328 			codePredicates.add(singleCode);
329 		}
330 
331 		myPredicates.add(myBuilder.or(toArray(codePredicates)));
332 	}
333 
334 	/**
335 	 * Add reference predicate to the current search
336 	 */
337 	private void addPredicateReference(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
338 		assert theParamName.contains(".") == false;
339 
340 		if (theList.get(0).getMissing() != null) {
341 			addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing());
342 			return;
343 		}
344 
345 		Join<ResourceTable, ResourceLink> join = createOrReuseJoin(JoinEnum.REFERENCE, theParamName);
346 
347 		List<Predicate> codePredicates = new ArrayList<>();
348 
349 		for (IQueryParameterType nextOr : theList) {
350 
351 			if (nextOr instanceof ReferenceParam) {
352 				ReferenceParam ref = (ReferenceParam) nextOr;
353 
354 				if (isBlank(ref.getChain())) {
355 					IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
356 
357 					if (dt.hasBaseUrl()) {
358 						if (myCallingDao.getConfig().getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
359 							dt = dt.toUnqualified();
360 						} else {
361 							ourLog.debug("Searching for resource link with target URL: {}", dt.getValue());
362 							Predicate eq = myBuilder.equal(join.get("myTargetResourceUrl"), dt.getValue());
363 							codePredicates.add(eq);
364 							continue;
365 						}
366 					}
367 
368 					List<Long> targetPid;
369 					try {
370 						targetPid = myCallingDao.translateForcedIdToPids(dt);
371 					} catch (ResourceNotFoundException e) {
372 						// Use a PID that will never exist
373 						targetPid = Collections.singletonList(-1L);
374 					}
375 					for (Long next : targetPid) {
376 						ourLog.debug("Searching for resource link with target PID: {}", next);
377 
378 						Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
379 						Predicate pidPredicate = myBuilder.equal(join.get("myTargetResourcePid"), next);
380 						codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
381 					}
382 
383 				} else {
384 
385 					final List<Class<? extends IBaseResource>> resourceTypes;
386 					String resourceId;
387 					if (!ref.getValue().matches("[a-zA-Z]+\\/.*")) {
388 
389 						RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
390 						resourceTypes = new ArrayList<>();
391 
392 						Set<String> targetTypes = param.getTargets();
393 
394 						if (targetTypes != null && !targetTypes.isEmpty()) {
395 							for (String next : targetTypes) {
396 								resourceTypes.add(myContext.getResourceDefinition(next).getImplementingClass());
397 							}
398 						}
399 
400 						if (resourceTypes.isEmpty()) {
401 							RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName);
402 							RuntimeSearchParam searchParamByName = myCallingDao.getSearchParamByName(resourceDef, theParamName);
403 							if (searchParamByName == null) {
404 								throw new InternalErrorException("Could not find parameter " + theParamName);
405 							}
406 							String paramPath = searchParamByName.getPath();
407 							if (paramPath.endsWith(".as(Reference)")) {
408 								paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference";
409 							}
410 
411 							if (paramPath.contains(".extension(")) {
412 								int startIdx = paramPath.indexOf(".extension(");
413 								int endIdx = paramPath.indexOf(')', startIdx);
414 								if (startIdx != -1 && endIdx != -1) {
415 									paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1);
416 								}
417 							}
418 
419 							BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, paramPath);
420 							if (def instanceof RuntimeChildChoiceDefinition) {
421 								RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def;
422 								resourceTypes.addAll(choiceDef.getResourceTypes());
423 							} else if (def instanceof RuntimeChildResourceDefinition) {
424 								RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def;
425 								resourceTypes.addAll(resDef.getResourceTypes());
426 							} else {
427 								throw new ConfigurationException("Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass());
428 							}
429 						}
430 
431 						if (resourceTypes.isEmpty()) {
432 							for (BaseRuntimeElementDefinition<?> next : myContext.getElementDefinitions()) {
433 								if (next instanceof RuntimeResourceDefinition) {
434 									RuntimeResourceDefinition nextResDef = (RuntimeResourceDefinition) next;
435 									resourceTypes.add(nextResDef.getImplementingClass());
436 								}
437 							}
438 						}
439 
440 						resourceId = ref.getValue();
441 
442 					} else {
443 						RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType());
444 						resourceTypes = new ArrayList<>(1);
445 						resourceTypes.add(resDef.getImplementingClass());
446 						resourceId = ref.getIdPart();
447 					}
448 
449 					boolean foundChainMatch = false;
450 
451 					String chain = ref.getChain();
452 					String remainingChain = null;
453 					int chainDotIndex = chain.indexOf('.');
454 					if (chainDotIndex != -1) {
455 						remainingChain = chain.substring(chainDotIndex + 1);
456 						chain = chain.substring(0, chainDotIndex);
457 					}
458 
459 					for (Class<? extends IBaseResource> nextType : resourceTypes) {
460 						RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType);
461 						String subResourceName = typeDef.getName();
462 
463 						IFhirResourceDao<?> dao = myCallingDao.getDao(nextType);
464 						if (dao == null) {
465 							ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName());
466 							continue;
467 						}
468 
469 						int qualifierIndex = chain.indexOf(':');
470 						String qualifier = null;
471 						if (qualifierIndex != -1) {
472 							qualifier = chain.substring(qualifierIndex);
473 							chain = chain.substring(0, qualifierIndex);
474 						}
475 
476 						boolean isMeta = BaseHapiFhirDao.RESOURCE_META_PARAMS.containsKey(chain);
477 						RuntimeSearchParam param = null;
478 						if (!isMeta) {
479 							param = myCallingDao.getSearchParamByName(typeDef, chain);
480 							if (param == null) {
481 								ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param);
482 								continue;
483 							}
484 						}
485 
486 						IQueryParameterType chainValue;
487 						if (remainingChain != null) {
488 							if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
489 								ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", nextType.getSimpleName(), chain, remainingChain);
490 								continue;
491 							}
492 
493 							chainValue = new ReferenceParam();
494 							chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
495 							((ReferenceParam) chainValue).setChain(remainingChain);
496 						} else if (isMeta) {
497 							IQueryParameterType type = BaseHapiFhirDao.newInstanceType(chain);
498 							type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
499 							chainValue = type;
500 						} else {
501 							chainValue = toParameterType(param, qualifier, resourceId);
502 						}
503 
504 						foundChainMatch = true;
505 
506 						Subquery<Long> subQ = myResourceTableQuery.subquery(Long.class);
507 						Root<ResourceTable> subQfrom = subQ.from(ResourceTable.class);
508 						subQ.select(subQfrom.get("myId").as(Long.class));
509 
510 						List<List<? extends IQueryParameterType>> andOrParams = new ArrayList<>();
511 						andOrParams.add(Collections.singletonList(chainValue));
512 
513 						/*
514 						 * We're doing a chain call, so push the current query root
515 						 * and predicate list down and put new ones at the top of the
516 						 * stack and run a subuery
517 						 */
518 						Root<ResourceTable> stackRoot = myResourceTableRoot;
519 						ArrayList<Predicate> stackPredicates = myPredicates;
520 						Map<JoinKey, Join<?, ?>> stackIndexJoins = myIndexJoins;
521 						myResourceTableRoot = subQfrom;
522 						myPredicates = Lists.newArrayList();
523 						myIndexJoins = Maps.newHashMap();
524 
525 						// Create the subquery predicates
526 						myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), subResourceName));
527 						myPredicates.add(myBuilder.isNull(myResourceTableRoot.get("myDeleted")));
528 						searchForIdsWithAndOr(subResourceName, chain, andOrParams);
529 
530 						subQ.where(toArray(myPredicates));
531 
532 						/*
533 						 * Pop the old query root and predicate list back
534 						 */
535 						myResourceTableRoot = stackRoot;
536 						myPredicates = stackPredicates;
537 						myIndexJoins = stackIndexJoins;
538 
539 						Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
540 						Predicate pidPredicate = join.get("myTargetResourcePid").in(subQ);
541 						codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
542 
543 					}
544 
545 					if (!foundChainMatch) {
546 						throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + ref.getChain()));
547 					}
548 				}
549 
550 			} else {
551 				throw new IllegalArgumentException("Invalid token type (expecting ReferenceParam): " + nextOr.getClass());
552 			}
553 
554 		}
555 
556 		myPredicates.add(myBuilder.or(toArray(codePredicates)));
557 	}
558 
559 	private void addPredicateResourceId(List<List<? extends IQueryParameterType>> theValues) {
560 		for (List<? extends IQueryParameterType> nextValue : theValues) {
561 			Set<Long> orPids = new HashSet<>();
562 			for (IQueryParameterType next : nextValue) {
563 				String value = next.getValueAsQueryToken(myContext);
564 				if (value != null && value.startsWith("|")) {
565 					value = value.substring(1);
566 				}
567 
568 				IdDt valueAsId = new IdDt(value);
569 				if (isNotBlank(value)) {
570 					if (valueAsId.isIdPartValidLong()) {
571 						orPids.add(valueAsId.getIdPartAsLong());
572 					} else {
573 						try {
574 							BaseHasResource entity = myCallingDao.readEntity(valueAsId);
575 							if (entity.getDeleted() == null) {
576 								orPids.add(entity.getId());
577 							}
578 						} catch (ResourceNotFoundException e) {
579 							/*
580 							 * This isn't an error, just means no result found
581 							 * that matches the ID the client provided
582 							 */
583 						}
584 					}
585 				}
586 			}
587 
588 			if (orPids.size() > 0) {
589 				Predicate nextPredicate = myResourceTableRoot.get("myId").as(Long.class).in(orPids);
590 				myPredicates.add(nextPredicate);
591 			} else {
592 				// This will never match
593 				Predicate nextPredicate = myBuilder.equal(myResourceTableRoot.get("myId").as(Long.class), -1);
594 				myPredicates.add(nextPredicate);
595 			}
596 
597 		}
598 	}
599 
600 	private void addPredicateString(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
601 
602 		Join<ResourceTable, ResourceIndexedSearchParamString> join = createOrReuseJoin(JoinEnum.STRING, theParamName);
603 
604 		if (theList.get(0).getMissing() != null) {
605 			addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
606 			return;
607 		}
608 
609 		List<Predicate> codePredicates = new ArrayList<>();
610 		for (IQueryParameterType nextOr : theList) {
611 			Predicate singleCode = createPredicateString(nextOr, theResourceName, theParamName, myBuilder, join);
612 			codePredicates.add(singleCode);
613 		}
614 
615 		myPredicates.add(myBuilder.or(toArray(codePredicates)));
616 
617 	}
618 
619 	private void addPredicateTag(List<List<? extends IQueryParameterType>> theList, String theParamName) {
620 		TagTypeEnum tagType;
621 		if (Constants.PARAM_TAG.equals(theParamName)) {
622 			tagType = TagTypeEnum.TAG;
623 		} else if (Constants.PARAM_PROFILE.equals(theParamName)) {
624 			tagType = TagTypeEnum.PROFILE;
625 		} else if (Constants.PARAM_SECURITY.equals(theParamName)) {
626 			tagType = TagTypeEnum.SECURITY_LABEL;
627 		} else {
628 			throw new IllegalArgumentException("Param name: " + theParamName); // shouldn't happen
629 		}
630 
631 		List<Pair<String, String>> notTags = Lists.newArrayList();
632 		for (List<? extends IQueryParameterType> nextAndParams : theList) {
633 			for (IQueryParameterType nextOrParams : nextAndParams) {
634 				if (nextOrParams instanceof TokenParam) {
635 					TokenParam param = (TokenParam) nextOrParams;
636 					if (param.getModifier() == TokenParamModifier.NOT) {
637 						if (isNotBlank(param.getSystem()) || isNotBlank(param.getValue())) {
638 							notTags.add(Pair.of(param.getSystem(), param.getValue()));
639 						}
640 					}
641 				}
642 			}
643 		}
644 
645 		/*
646 		 * We have a parameter of ResourceType?_tag:not=foo This means match resources that don't have the given tag(s)
647 		 */
648 		if (notTags.isEmpty() == false) {
649 			// CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
650 			// CriteriaQuery<Long> cq = builder.createQuery(Long.class);
651 			// Root<ResourceTable> from = cq.from(ResourceTable.class);
652 			// cq.select(from.get("myId").as(Long.class));
653 			//
654 			// Subquery<Long> subQ = cq.subquery(Long.class);
655 			// Root<ResourceTag> subQfrom = subQ.from(ResourceTag.class);
656 			// subQ.select(subQfrom.get("myResourceId").as(Long.class));
657 			// Predicate subQname = builder.equal(subQfrom.get("myParamName"), theParamName);
658 			// Predicate subQtype = builder.equal(subQfrom.get("myResourceType"), myResourceName);
659 			// subQ.where(builder.and(subQtype, subQname));
660 			//
661 			// List<Predicate> predicates = new ArrayList<Predicate>();
662 			// predicates.add(builder.not(builder.in(from.get("myId")).value(subQ)));
663 			// predicates.add(builder.equal(from.get("myResourceType"), myResourceName));
664 			// predicates.add(builder.isNull(from.get("myDeleted")));
665 			// createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class));
666 		}
667 
668 		for (List<? extends IQueryParameterType> nextAndParams : theList) {
669 			boolean haveTags = false;
670 			for (IQueryParameterType nextParamUncasted : nextAndParams) {
671 				if (nextParamUncasted instanceof TokenParam) {
672 					TokenParam nextParam = (TokenParam) nextParamUncasted;
673 					if (isNotBlank(nextParam.getValue())) {
674 						haveTags = true;
675 					} else if (isNotBlank(nextParam.getSystem())) {
676 						throw new InvalidRequestException("Invalid " + theParamName + " parameter (must supply a value/code and not just a system): " + nextParam.getValueAsQueryToken(myContext));
677 					}
678 				} else {
679 					UriParam nextParam = (UriParam) nextParamUncasted;
680 					if (isNotBlank(nextParam.getValue())) {
681 						haveTags = true;
682 					}
683 				}
684 			}
685 			if (!haveTags) {
686 				continue;
687 			}
688 
689 			boolean paramInverted = false;
690 			List<Pair<String, String>> tokens = Lists.newArrayList();
691 			for (IQueryParameterType nextOrParams : nextAndParams) {
692 				String code;
693 				String system;
694 				if (nextOrParams instanceof TokenParam) {
695 					TokenParam nextParam = (TokenParam) nextOrParams;
696 					code = nextParam.getValue();
697 					system = nextParam.getSystem();
698 					if (nextParam.getModifier() == TokenParamModifier.NOT) {
699 						paramInverted = true;
700 					}
701 				} else {
702 					UriParam nextParam = (UriParam) nextOrParams;
703 					code = nextParam.getValue();
704 					system = null;
705 				}
706 
707 				if (isNotBlank(code)) {
708 					tokens.add(Pair.of(system, code));
709 				}
710 			}
711 
712 			if (tokens.isEmpty()) {
713 				continue;
714 			}
715 
716 			if (paramInverted) {
717 				ourLog.debug("Searching for _tag:not");
718 
719 				Subquery<Long> subQ = myResourceTableQuery.subquery(Long.class);
720 				Root<ResourceTag> subQfrom = subQ.from(ResourceTag.class);
721 				subQ.select(subQfrom.get("myResourceId").as(Long.class));
722 
723 				myPredicates.add(myBuilder.not(myBuilder.in(myResourceTableRoot.get("myId")).value(subQ)));
724 
725 				Subquery<Long> defJoin = subQ.subquery(Long.class);
726 				Root<TagDefinition> defJoinFrom = defJoin.from(TagDefinition.class);
727 				defJoin.select(defJoinFrom.get("myId").as(Long.class));
728 
729 				subQ.where(subQfrom.get("myTagId").as(Long.class).in(defJoin));
730 
731 				List<Predicate> orPredicates = createPredicateTagList(defJoinFrom, myBuilder, tagType, tokens);
732 				defJoin.where(toArray(orPredicates));
733 
734 				continue;
735 			}
736 
737 			Join<ResourceTable, ResourceTag> tagJoin = myResourceTableRoot.join("myTags", JoinType.LEFT);
738 			From<ResourceTag, TagDefinition> defJoin = tagJoin.join("myTag");
739 
740 			List<Predicate> orPredicates = createPredicateTagList(defJoin, myBuilder, tagType, tokens);
741 			myPredicates.add(myBuilder.or(toArray(orPredicates)));
742 
743 		}
744 
745 	}
746 
747 	private void addPredicateToken(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
748 
749 		Join<ResourceTable, ResourceIndexedSearchParamToken> join = createOrReuseJoin(JoinEnum.TOKEN, theParamName);
750 
751 		if (theList.get(0).getMissing() != null) {
752 			addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
753 			return;
754 		}
755 
756 		List<Predicate> codePredicates = new ArrayList<>();
757 		for (IQueryParameterType nextOr : theList) {
758 
759 			if (nextOr instanceof TokenParam) {
760 				TokenParam id = (TokenParam) nextOr;
761 				if (id.isText()) {
762 					addPredicateString(theResourceName, theParamName, theList);
763 					continue;
764 				}
765 			}
766 
767 			Predicate singleCode = createPredicateToken(nextOr, theResourceName, theParamName, myBuilder, join);
768 			codePredicates.add(singleCode);
769 		}
770 
771 		if (codePredicates.isEmpty()) {
772 			return;
773 		}
774 
775 		Predicate spPredicate = myBuilder.or(toArray(codePredicates));
776 		myPredicates.add(spPredicate);
777 	}
778 
779 	private void addPredicateUri(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) {
780 
781 		Join<ResourceTable, ResourceIndexedSearchParamUri> join = createOrReuseJoin(JoinEnum.URI, theParamName);
782 
783 		if (theList.get(0).getMissing() != null) {
784 			addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join);
785 			return;
786 		}
787 
788 		List<Predicate> codePredicates = new ArrayList<>();
789 		for (IQueryParameterType nextOr : theList) {
790 
791 			if (nextOr instanceof UriParam) {
792 				UriParam param = (UriParam) nextOr;
793 
794 				String value = param.getValue();
795 				if (value == null) {
796 					continue;
797 				}
798 
799 				if (param.getQualifier() == UriParamQualifierEnum.ABOVE) {
800 
801 					/*
802 					 * :above is an inefficient query- It means that the user is supplying a more specific URL (say
803 					 * http://example.com/foo/bar/baz) and that we should match on any URLs that are less
804 					 * specific but otherwise the same. For example http://example.com and http://example.com/foo would both
805 					 * match.
806 					 *
807 					 * We do this by querying the DB for all candidate URIs and then manually checking each one. This isn't
808 					 * very efficient, but this is also probably not a very common type of query to do.
809 					 *
810 					 * If we ever need to make this more efficient, lucene could certainly be used as an optimization.
811 					 */
812 					ourLog.info("Searching for candidate URI:above parameters for Resource[{}] param[{}]", myResourceName, theParamName);
813 					Collection<String> candidates = myResourceIndexedSearchParamUriDao.findAllByResourceTypeAndParamName(myResourceName, theParamName);
814 					List<String> toFind = new ArrayList<>();
815 					for (String next : candidates) {
816 						if (value.length() >= next.length()) {
817 							if (value.substring(0, next.length()).equals(next)) {
818 								toFind.add(next);
819 							}
820 						}
821 					}
822 
823 					if (toFind.isEmpty()) {
824 						continue;
825 					}
826 
827 					Predicate uriPredicate = join.get("myUri").as(String.class).in(toFind);
828 					Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate);
829 					codePredicates.add(hashAndUriPredicate);
830 
831 				} else if (param.getQualifier() == UriParamQualifierEnum.BELOW) {
832 
833 					Predicate uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value));
834 					Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate);
835 					codePredicates.add(hashAndUriPredicate);
836 
837 				} else {
838 
839 					long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value);
840 					Predicate hashPredicate = myBuilder.equal(join.get("myHashUri"), hashUri);
841 					codePredicates.add(hashPredicate);
842 
843 				}
844 
845 			} else {
846 				throw new IllegalArgumentException("Invalid URI type: " + nextOr.getClass());
847 			}
848 
849 		}
850 
851 		/*
852 		 * If we haven't found any of the requested URIs in the candidates, then we'll
853 		 * just add a predicate that can never match
854 		 */
855 		if (codePredicates.isEmpty()) {
856 			Predicate predicate = myBuilder.isNull(join.get("myMissing").as(String.class));
857 			myPredicates.add(predicate);
858 			return;
859 		}
860 
861 		Predicate orPredicate = myBuilder.or(toArray(codePredicates));
862 		myPredicates.add(orPredicate);
863 	}
864 
865 	private Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From<?, ? extends BaseResourceIndexedSearchParam> theFrom, Predicate thePredicate) {
866 		long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName);
867 		Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity);
868 		return myBuilder.and(hashIdentityPredicate, thePredicate);
869 	}
870 
871 	private Predicate createCompositeParamPart(String theResourceName, Root<ResourceTable> theRoot, RuntimeSearchParam theParam, IQueryParameterType leftValue) {
872 		Predicate retVal = null;
873 		switch (theParam.getParamType()) {
874 			case STRING: {
875 				From<ResourceIndexedSearchParamString, ResourceIndexedSearchParamString> stringJoin = theRoot.join("myParamsString", JoinType.INNER);
876 				retVal = createPredicateString(leftValue, theResourceName, theParam.getName(), myBuilder, stringJoin);
877 				break;
878 			}
879 			case TOKEN: {
880 				From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> tokenJoin = theRoot.join("myParamsToken", JoinType.INNER);
881 				retVal = createPredicateToken(leftValue, theResourceName, theParam.getName(), myBuilder, tokenJoin);
882 				break;
883 			}
884 			case DATE: {
885 				From<ResourceIndexedSearchParamDate, ResourceIndexedSearchParamDate> dateJoin = theRoot.join("myParamsDate", JoinType.INNER);
886 				retVal = createPredicateDate(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin);
887 				break;
888 			}
889 			case QUANTITY: {
890 				From<ResourceIndexedSearchParamQuantity, ResourceIndexedSearchParamQuantity> dateJoin = theRoot.join("myParamsQuantity", JoinType.INNER);
891 				retVal = createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin);
892 				break;
893 			}
894 			case COMPOSITE:
895 			case HAS:
896 			case NUMBER:
897 			case REFERENCE:
898 			case URI:
899 				break;
900 		}
901 
902 		if (retVal == null) {
903 			throw new InvalidRequestException("Don't know how to handle composite parameter with type of " + theParam.getParamType());
904 		}
905 
906 		return retVal;
907 	}
908 
909 	@SuppressWarnings("unchecked")
910 	private <T> Join<ResourceTable, T> createOrReuseJoin(JoinEnum theType, String theSearchParameterName) {
911 		Join<ResourceTable, ResourceIndexedSearchParamDate> join = null;
912 
913 		switch (theType) {
914 			case DATE:
915 				join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT);
916 				break;
917 			case NUMBER:
918 				join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT);
919 				break;
920 			case QUANTITY:
921 				join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT);
922 				break;
923 			case REFERENCE:
924 				join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT);
925 				break;
926 			case STRING:
927 				join = myResourceTableRoot.join("myParamsString", JoinType.LEFT);
928 				break;
929 			case URI:
930 				join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT);
931 				break;
932 			case TOKEN:
933 				join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT);
934 				break;
935 		}
936 
937 		JoinKey key = new JoinKey(theSearchParameterName, theType);
938 		if (!myIndexJoins.containsKey(key)) {
939 			myIndexJoins.put(key, join);
940 		}
941 
942 		return (Join<ResourceTable, T>) join;
943 	}
944 
945 	private Predicate createPredicateDate(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamDate> theFrom) {
946 		Predicate p;
947 		if (theParam instanceof DateParam) {
948 			DateParam date = (DateParam) theParam;
949 			if (!date.isEmpty()) {
950 				DateRangeParam range = new DateRangeParam(date);
951 				p = createPredicateDateFromRange(theBuilder, theFrom, range);
952 			} else {
953 				// TODO: handle missing date param?
954 				p = null;
955 			}
956 		} else if (theParam instanceof DateRangeParam) {
957 			DateRangeParam range = (DateRangeParam) theParam;
958 			p = createPredicateDateFromRange(theBuilder, theFrom, range);
959 		} else {
960 			throw new IllegalArgumentException("Invalid token type: " + theParam.getClass());
961 		}
962 
963 		return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, p);
964 	}
965 
966 	private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamDate> theFrom, DateRangeParam theRange) {
967 		Date lowerBound = theRange.getLowerBoundAsInstant();
968 		Date upperBound = theRange.getUpperBoundAsInstant();
969 
970 		Predicate lb = null;
971 		if (lowerBound != null) {
972 			Predicate gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound);
973 			Predicate lt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), lowerBound);
974 			if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) {
975 				lb = gt;
976 			} else {
977 				lb = theBuilder.or(gt, lt);
978 			}
979 		}
980 
981 		Predicate ub = null;
982 		if (upperBound != null) {
983 			Predicate gt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), upperBound);
984 			Predicate lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound);
985 			if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) {
986 				ub = lt;
987 			} else {
988 				ub = theBuilder.or(gt, lt);
989 			}
990 		}
991 
992 		if (lb != null && ub != null) {
993 			return (theBuilder.and(lb, ub));
994 		} else if (lb != null) {
995 			return (lb);
996 		} else {
997 			return (ub);
998 		}
999 	}
1000 
1001 	private Predicate createPredicateNumeric(String theResourceName, String theParamName, From<?, ? extends BaseResourceIndexedSearchParam> theFrom, CriteriaBuilder builder,
1002 														  IQueryParameterType theParam, ParamPrefixEnum thePrefix, BigDecimal theValue, final Expression<BigDecimal> thePath,
1003 														  String invalidMessageName) {
1004 		Predicate num;
1005 		switch (thePrefix) {
1006 			case GREATERTHAN:
1007 				num = builder.gt(thePath, theValue);
1008 				break;
1009 			case GREATERTHAN_OR_EQUALS:
1010 				num = builder.ge(thePath, theValue);
1011 				break;
1012 			case LESSTHAN:
1013 				num = builder.lt(thePath, theValue);
1014 				break;
1015 			case LESSTHAN_OR_EQUALS:
1016 				num = builder.le(thePath, theValue);
1017 				break;
1018 			case APPROXIMATE:
1019 			case EQUAL:
1020 			case NOT_EQUAL:
1021 				BigDecimal mul = calculateFuzzAmount(thePrefix, theValue);
1022 				BigDecimal low = theValue.subtract(mul, MathContext.DECIMAL64);
1023 				BigDecimal high = theValue.add(mul, MathContext.DECIMAL64);
1024 				Predicate lowPred;
1025 				Predicate highPred;
1026 				if (thePrefix != ParamPrefixEnum.NOT_EQUAL) {
1027 					lowPred = builder.ge(thePath.as(BigDecimal.class), low);
1028 					highPred = builder.le(thePath.as(BigDecimal.class), high);
1029 					num = builder.and(lowPred, highPred);
1030 					ourLog.trace("Searching for {} <= val <= {}", low, high);
1031 				} else {
1032 					// Prefix was "ne", so reverse it!
1033 					lowPred = builder.lt(thePath.as(BigDecimal.class), low);
1034 					highPred = builder.gt(thePath.as(BigDecimal.class), high);
1035 					num = builder.or(lowPred, highPred);
1036 				}
1037 				break;
1038 			case ENDS_BEFORE:
1039 			case STARTS_AFTER:
1040 			default:
1041 				String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, invalidMessageName, thePrefix.getValue(), theParam.getValueAsQueryToken(myContext));
1042 				throw new InvalidRequestException(msg);
1043 		}
1044 
1045 		if (theParamName == null) {
1046 			return num;
1047 		}
1048 		return num;
1049 	}
1050 
1051 	private Predicate createPredicateQuantity(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
1052 															From<?, ResourceIndexedSearchParamQuantity> theFrom) {
1053 		String systemValue;
1054 		String unitsValue;
1055 		ParamPrefixEnum cmpValue;
1056 		BigDecimal valueValue;
1057 
1058 		if (theParam instanceof BaseQuantityDt) {
1059 			BaseQuantityDt param = (BaseQuantityDt) theParam;
1060 			systemValue = param.getSystemElement().getValueAsString();
1061 			unitsValue = param.getUnitsElement().getValueAsString();
1062 			cmpValue = ParamPrefixEnum.forValue(param.getComparatorElement().getValueAsString());
1063 			valueValue = param.getValueElement().getValue();
1064 		} else if (theParam instanceof QuantityParam) {
1065 			QuantityParam param = (QuantityParam) theParam;
1066 			systemValue = param.getSystem();
1067 			unitsValue = param.getUnits();
1068 			cmpValue = param.getPrefix();
1069 			valueValue = param.getValue();
1070 		} else {
1071 			throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass());
1072 		}
1073 
1074 		Predicate hashPredicate;
1075 		if (!isBlank(systemValue) && !isBlank(unitsValue)) {
1076 			long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue);
1077 			hashPredicate = myBuilder.equal(theFrom.get("myHashIdentitySystemAndUnits"), hash);
1078 		} else if (!isBlank(unitsValue)) {
1079 			long hash = ResourceIndexedSearchParamQuantity.calculateHashUnits(theResourceName, theParamName, unitsValue);
1080 			hashPredicate = myBuilder.equal(theFrom.get("myHashIdentityAndUnits"), hash);
1081 		} else {
1082 			long hash = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName);
1083 			hashPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hash);
1084 		}
1085 
1086 		cmpValue = ObjectUtils.defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL);
1087 		final Expression<BigDecimal> path = theFrom.get("myValue");
1088 		String invalidMessageName = "invalidQuantityPrefix";
1089 
1090 		Predicate numericPredicate = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName);
1091 
1092 		return theBuilder.and(hashPredicate, numericPredicate);
1093 	}
1094 
1095 	private Predicate createPredicateString(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
1096 														 From<?, ResourceIndexedSearchParamString> theFrom) {
1097 		String rawSearchTerm;
1098 		DaoConfig daoConfig = myCallingDao.getConfig();
1099 		if (theParameter instanceof TokenParam) {
1100 			TokenParam id = (TokenParam) theParameter;
1101 			if (!id.isText()) {
1102 				throw new IllegalStateException("Trying to process a text search on a non-text token parameter");
1103 			}
1104 			rawSearchTerm = id.getValue();
1105 		} else if (theParameter instanceof StringParam) {
1106 			StringParam id = (StringParam) theParameter;
1107 			rawSearchTerm = id.getValue();
1108 			if (id.isContains()) {
1109 				if (!daoConfig.isAllowContainsSearches()) {
1110 					throw new MethodNotAllowedException(":contains modifier is disabled on this server");
1111 				}
1112 			}
1113 		} else if (theParameter instanceof IPrimitiveDatatype<?>) {
1114 			IPrimitiveDatatype<?> id = (IPrimitiveDatatype<?>) theParameter;
1115 			rawSearchTerm = id.getValueAsString();
1116 		} else {
1117 			throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass());
1118 		}
1119 
1120 		if (rawSearchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) {
1121 			throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed ("
1122 				+ ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm);
1123 		}
1124 
1125 		boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact();
1126 		if (exactMatch) {
1127 
1128 			// Exact match
1129 
1130 			Long hash = ResourceIndexedSearchParamString.calculateHashExact(theResourceName, theParamName, rawSearchTerm);
1131 			return theBuilder.equal(theFrom.get("myHashExact").as(Long.class), hash);
1132 
1133 		} else {
1134 
1135 			// Normalized Match
1136 
1137 			String normalizedString = BaseHapiFhirDao.normalizeString(rawSearchTerm);
1138 			String likeExpression;
1139 			if (theParameter instanceof StringParam &&
1140 				((StringParam) theParameter).isContains() &&
1141 				daoConfig.isAllowContainsSearches()) {
1142 				likeExpression = createLeftAndRightMatchLikeExpression(normalizedString);
1143 			} else {
1144 				likeExpression = createLeftMatchLikeExpression(normalizedString);
1145 			}
1146 
1147 			Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(daoConfig, theResourceName, theParamName, normalizedString);
1148 			Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash);
1149 			Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression);
1150 			return theBuilder.and(hashCode, singleCode);
1151 
1152 		}
1153 	}
1154 
1155 	private List<Predicate> createPredicateTagList(Path<TagDefinition> theDefJoin, CriteriaBuilder theBuilder, TagTypeEnum theTagType, List<Pair<String, String>> theTokens) {
1156 		Predicate typePrediate = theBuilder.equal(theDefJoin.get("myTagType"), theTagType);
1157 
1158 		List<Predicate> orPredicates = Lists.newArrayList();
1159 		for (Pair<String, String> next : theTokens) {
1160 			Predicate codePrediate = theBuilder.equal(theDefJoin.get("myCode"), next.getRight());
1161 			if (isNotBlank(next.getLeft())) {
1162 				Predicate systemPrediate = theBuilder.equal(theDefJoin.get("mySystem"), next.getLeft());
1163 				orPredicates.add(theBuilder.and(typePrediate, systemPrediate, codePrediate));
1164 			} else {
1165 				orPredicates.add(theBuilder.and(typePrediate, codePrediate));
1166 			}
1167 		}
1168 		return orPredicates;
1169 	}
1170 
1171 	private Predicate createPredicateToken(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
1172 														From<?, ResourceIndexedSearchParamToken> theFrom) {
1173 		String code;
1174 		String system;
1175 		TokenParamModifier modifier = null;
1176 		if (theParameter instanceof TokenParam) {
1177 			TokenParam id = (TokenParam) theParameter;
1178 			system = id.getSystem();
1179 			code = (id.getValue());
1180 			modifier = id.getModifier();
1181 		} else if (theParameter instanceof BaseIdentifierDt) {
1182 			BaseIdentifierDt id = (BaseIdentifierDt) theParameter;
1183 			system = id.getSystemElement().getValueAsString();
1184 			code = (id.getValueElement().getValue());
1185 		} else if (theParameter instanceof BaseCodingDt) {
1186 			BaseCodingDt id = (BaseCodingDt) theParameter;
1187 			system = id.getSystemElement().getValueAsString();
1188 			code = (id.getCodeElement().getValue());
1189 		} else {
1190 			throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass());
1191 		}
1192 
1193 		if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
1194 			throw new InvalidRequestException(
1195 				"Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
1196 		}
1197 
1198 		if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
1199 			throw new InvalidRequestException(
1200 				"Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
1201 		}
1202 
1203 		/*
1204 		 * Process token modifiers (:in, :below, :above)
1205 		 */
1206 
1207 		List<VersionIndependentConcept> codes;
1208 		if (modifier == TokenParamModifier.IN) {
1209 			codes = myTerminologySvc.expandValueSet(code);
1210 		} else if (modifier == TokenParamModifier.ABOVE) {
1211 			system = determineSystemIfMissing(theParamName, code, system);
1212 			codes = myTerminologySvc.findCodesAbove(system, code);
1213 		} else if (modifier == TokenParamModifier.BELOW) {
1214 			system = determineSystemIfMissing(theParamName, code, system);
1215 			codes = myTerminologySvc.findCodesBelow(system, code);
1216 		} else {
1217 			codes = Collections.singletonList(new VersionIndependentConcept(system, code));
1218 		}
1219 
1220 		if (codes.isEmpty()) {
1221 			// This will never match anything
1222 			return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false);
1223 		}
1224 
1225 		/*
1226 		 * Note: A null system value means "match any system", but
1227 		 * an empty-string system value means "match values that
1228 		 * explicitly have no system".
1229 		 */
1230 		boolean haveSystem = codes.get(0).getSystem() != null;
1231 		boolean haveCode = isNotBlank(codes.get(0).getCode());
1232 		Expression<Long> hashField;
1233 		if (!haveSystem && !haveCode) {
1234 			// If we have neither, this isn't actually an expression so
1235 			// just return 1=1
1236 			return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, true);
1237 		} else if (haveSystem && haveCode) {
1238 			hashField = theFrom.get("myHashSystemAndValue").as(Long.class);
1239 		} else if (haveSystem) {
1240 			hashField = theFrom.get("myHashSystem").as(Long.class);
1241 		} else {
1242 			hashField = theFrom.get("myHashValue").as(Long.class);
1243 		}
1244 
1245 		List<Long> values = new ArrayList<>(codes.size());
1246 		for (VersionIndependentConcept next : codes) {
1247 			if (haveSystem && haveCode) {
1248 				values.add(ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, next.getSystem(), next.getCode()));
1249 			} else if (haveSystem) {
1250 				values.add(ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, next.getSystem()));
1251 			} else {
1252 				values.add(ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, next.getCode()));
1253 			}
1254 		}
1255 
1256 		Predicate predicate = hashField.in(values);
1257 		if (modifier == TokenParamModifier.NOT) {
1258 			Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName));
1259 			Predicate disjunctionPredicate = theBuilder.not(predicate);
1260 			predicate = theBuilder.and(identityPredicate, disjunctionPredicate);
1261 		}
1262 		return predicate;
1263 	}
1264 
1265 	@Override
1266 	public Iterator<Long> createQuery(SearchParameterMap theParams, String theSearchUuid) {
1267 		myParams = theParams;
1268 		myBuilder = myEntityManager.getCriteriaBuilder();
1269 		mySearchUuid = theSearchUuid;
1270 
1271 		/*
1272 		 * Check if there is a unique key associated with the set
1273 		 * of parameters passed in
1274 		 */
1275 		ourLog.debug("Checking for unique index for query: {}", theParams.toNormalizedQueryString(myContext));
1276 		if (myCallingDao.getConfig().isUniqueIndexesEnabled()) {
1277 			if (myParams.getIncludes().isEmpty()) {
1278 				if (myParams.getRevIncludes().isEmpty()) {
1279 					if (myParams.getEverythingMode() == null) {
1280 						if (myParams.isAllParametersHaveNoModifier()) {
1281 							Set<String> paramNames = theParams.keySet();
1282 							if (paramNames.isEmpty() == false) {
1283 								List<JpaRuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveUniqueSearchParams(myResourceName, paramNames);
1284 								if (searchParams.size() > 0) {
1285 									List<List<String>> params = new ArrayList<>();
1286 									for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamNameToValues : theParams.entrySet()) {
1287 										String nextParamName = nextParamNameToValues.getKey();
1288 										nextParamName = UrlUtil.escapeUrlParam(nextParamName);
1289 										for (List<? extends IQueryParameterType> nextAnd : nextParamNameToValues.getValue()) {
1290 											ArrayList<String> nextValueList = new ArrayList<>();
1291 											params.add(nextValueList);
1292 											for (IQueryParameterType nextOr : nextAnd) {
1293 												String nextOrValue = nextOr.getValueAsQueryToken(myContext);
1294 												nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
1295 												nextValueList.add(nextParamName + "=" + nextOrValue);
1296 											}
1297 										}
1298 									}
1299 
1300 									Set<String> uniqueQueryStrings = BaseHapiFhirDao.extractCompositeStringUniquesValueChains(myResourceName, params);
1301 									if (ourTrackHandlersForUnitTest) {
1302 										ourLastHandlerParamsForUnitTest = theParams;
1303 										ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.UNIQUE_INDEX;
1304 										ourLastHandlerThreadForUnitTest = Thread.currentThread().getName();
1305 									}
1306 									return new UniqueIndexIterator(uniqueQueryStrings);
1307 
1308 								}
1309 							}
1310 						}
1311 					}
1312 				}
1313 			}
1314 		}
1315 
1316 		if (ourTrackHandlersForUnitTest) {
1317 			ourLastHandlerParamsForUnitTest = theParams;
1318 			ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.STANDARD_QUERY;
1319 			ourLastHandlerThreadForUnitTest = Thread.currentThread().getName();
1320 		}
1321 		return new QueryIterator();
1322 	}
1323 
1324 	private TypedQuery<Long> createQuery(SortSpec sort, Integer theMaximumResults) {
1325 		CriteriaQuery<Long> outerQuery;
1326 		/*
1327 		 * Sort
1328 		 *
1329 		 * If we have a sort, we wrap the criteria search (the search that actually
1330 		 * finds the appropriate resources) in an outer search which is then sorted
1331 		 */
1332 		if (sort != null) {
1333 
1334 			outerQuery = myBuilder.createQuery(Long.class);
1335 			Root<ResourceTable> outerQueryFrom = outerQuery.from(ResourceTable.class);
1336 
1337 			List<Order> orders = Lists.newArrayList();
1338 			List<Predicate> predicates = Lists.newArrayList();
1339 
1340 			createSort(myBuilder, outerQueryFrom, sort, orders, predicates);
1341 			if (orders.size() > 0) {
1342 				outerQuery.orderBy(orders);
1343 			}
1344 
1345 			Subquery<Long> subQ = outerQuery.subquery(Long.class);
1346 			Root<ResourceTable> subQfrom = subQ.from(ResourceTable.class);
1347 
1348 			myResourceTableQuery = subQ;
1349 			myResourceTableRoot = subQfrom;
1350 
1351 			Expression<Long> selectExpr = subQfrom.get("myId").as(Long.class);
1352 			subQ.select(selectExpr);
1353 
1354 			predicates.add(0, myBuilder.in(outerQueryFrom.get("myId").as(Long.class)).value(subQ));
1355 
1356 			outerQuery.multiselect(outerQueryFrom.get("myId").as(Long.class));
1357 			outerQuery.where(predicates.toArray(new Predicate[0]));
1358 
1359 		} else {
1360 
1361 			outerQuery = myBuilder.createQuery(Long.class);
1362 			myResourceTableQuery = outerQuery;
1363 			myResourceTableRoot = myResourceTableQuery.from(ResourceTable.class);
1364 			outerQuery.multiselect(myResourceTableRoot.get("myId").as(Long.class));
1365 
1366 		}
1367 
1368 		myPredicates = new ArrayList<>();
1369 
1370 		if (myParams.getEverythingMode() != null) {
1371 			Join<ResourceTable, ResourceLink> join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT);
1372 
1373 			if (myParams.get(IAnyResource.SP_RES_ID) != null) {
1374 				StringParam idParm = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
1375 				Long pid = BaseHapiFhirDao.translateForcedIdToPid(myResourceName, idParm.getValue(), myForcedIdDao);
1376 				if (myAlsoIncludePids == null) {
1377 					myAlsoIncludePids = new ArrayList<>(1);
1378 				}
1379 				myAlsoIncludePids.add(pid);
1380 				myPredicates.add(myBuilder.equal(join.get("myTargetResourcePid").as(Long.class), pid));
1381 			} else {
1382 				Predicate targetTypePredicate = myBuilder.equal(join.get("myTargetResourceType").as(String.class), myResourceName);
1383 				Predicate sourceTypePredicate = myBuilder.equal(myResourceTableRoot.get("myResourceType").as(String.class), myResourceName);
1384 				myPredicates.add(myBuilder.or(sourceTypePredicate, targetTypePredicate));
1385 			}
1386 
1387 		} else {
1388 			// Normal search
1389 			searchForIdsWithAndOr(myParams);
1390 		}
1391 
1392 		/*
1393 		 * Fulltext search
1394 		 */
1395 		if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT)) {
1396 			if (myFulltextSearchSvc == null) {
1397 				if (myParams.containsKey(Constants.PARAM_TEXT)) {
1398 					throw new InvalidRequestException("Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_TEXT);
1399 				} else if (myParams.containsKey(Constants.PARAM_CONTENT)) {
1400 					throw new InvalidRequestException("Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_CONTENT);
1401 				}
1402 			}
1403 
1404 			List<Long> pids;
1405 			if (myParams.getEverythingMode() != null) {
1406 				pids = myFulltextSearchSvc.everything(myResourceName, myParams);
1407 			} else {
1408 				pids = myFulltextSearchSvc.search(myResourceName, myParams);
1409 			}
1410 			if (pids.isEmpty()) {
1411 				// Will never match
1412 				pids = Collections.singletonList(-1L);
1413 			}
1414 
1415 			myPredicates.add(myResourceTableRoot.get("myId").as(Long.class).in(pids));
1416 		}
1417 
1418 		/*
1419 		 * Add a predicate to make sure we only include non-deleted resources, and only include
1420 		 * resources of the right type.
1421 		 *
1422 		 * If we have any joins to index tables, we get this behaviour already guaranteed so we don't
1423 		 * need an explicit predicate for it.
1424 		 */
1425 		if (myIndexJoins.isEmpty()) {
1426 			if (myParams.getEverythingMode() == null) {
1427 				myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), myResourceName));
1428 			}
1429 			myPredicates.add(myBuilder.isNull(myResourceTableRoot.get("myDeleted")));
1430 		}
1431 
1432 		// Last updated
1433 		DateRangeParam lu = myParams.getLastUpdated();
1434 		List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(lu, myBuilder, myResourceTableRoot);
1435 		myPredicates.addAll(lastUpdatedPredicates);
1436 
1437 		myResourceTableQuery.where(myBuilder.and(SearchBuilder.toArray(myPredicates)));
1438 
1439 		/*
1440 		 * Now perform the search
1441 		 */
1442 		final TypedQuery<Long> query = myEntityManager.createQuery(outerQuery);
1443 
1444 		if (theMaximumResults != null) {
1445 			query.setMaxResults(theMaximumResults);
1446 		}
1447 
1448 		return query;
1449 	}
1450 
1451 	private Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From<?, ? extends ResourceLink> from) {
1452 		return createResourceLinkPathPredicate(myCallingDao, myContext, theParamName, from, theResourceName);
1453 	}
1454 
1455 	/**
1456 	 * @return Returns {@literal true} if any search parameter sorts were found, or false if
1457 	 * no sorts were found, or only non-search parameters ones (e.g. _id, _lastUpdated)
1458 	 */
1459 	private boolean createSort(CriteriaBuilder theBuilder, Root<ResourceTable> theFrom, SortSpec theSort, List<Order> theOrders, List<Predicate> thePredicates) {
1460 		if (theSort == null || isBlank(theSort.getParamName())) {
1461 			return false;
1462 		}
1463 
1464 		if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) {
1465 			From<?, ?> forcedIdJoin = theFrom.join("myForcedId", JoinType.LEFT);
1466 			if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
1467 				theOrders.add(theBuilder.asc(forcedIdJoin.get("myForcedId")));
1468 				theOrders.add(theBuilder.asc(theFrom.get("myId")));
1469 			} else {
1470 				theOrders.add(theBuilder.desc(forcedIdJoin.get("myForcedId")));
1471 				theOrders.add(theBuilder.desc(theFrom.get("myId")));
1472 			}
1473 
1474 			return createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates);
1475 		}
1476 
1477 		if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) {
1478 			if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
1479 				theOrders.add(theBuilder.asc(theFrom.get("myUpdated")));
1480 			} else {
1481 				theOrders.add(theBuilder.desc(theFrom.get("myUpdated")));
1482 			}
1483 
1484 			return createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates);
1485 		}
1486 
1487 		RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName);
1488 		RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theSort.getParamName());
1489 		if (param == null) {
1490 			throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'");
1491 		}
1492 
1493 		String joinAttrName;
1494 		String[] sortAttrName;
1495 		JoinEnum joinType;
1496 
1497 		switch (param.getParamType()) {
1498 			case STRING:
1499 				joinAttrName = "myParamsString";
1500 				sortAttrName = new String[] {"myValueExact"};
1501 				joinType = JoinEnum.STRING;
1502 				break;
1503 			case DATE:
1504 				joinAttrName = "myParamsDate";
1505 				sortAttrName = new String[] {"myValueLow"};
1506 				joinType = JoinEnum.DATE;
1507 				break;
1508 			case REFERENCE:
1509 				joinAttrName = "myResourceLinks";
1510 				sortAttrName = new String[] {"myTargetResourcePid"};
1511 				joinType = JoinEnum.REFERENCE;
1512 				break;
1513 			case TOKEN:
1514 				joinAttrName = "myParamsToken";
1515 				sortAttrName = new String[] {"mySystem", "myValue"};
1516 				joinType = JoinEnum.TOKEN;
1517 				break;
1518 			case NUMBER:
1519 				joinAttrName = "myParamsNumber";
1520 				sortAttrName = new String[] {"myValue"};
1521 				joinType = JoinEnum.NUMBER;
1522 				break;
1523 			case URI:
1524 				joinAttrName = "myParamsUri";
1525 				sortAttrName = new String[] {"myUri"};
1526 				joinType = JoinEnum.URI;
1527 				break;
1528 			case QUANTITY:
1529 				joinAttrName = "myParamsQuantity";
1530 				sortAttrName = new String[] {"myValue"};
1531 				joinType = JoinEnum.QUANTITY;
1532 				break;
1533 			case COMPOSITE:
1534 			case HAS:
1535 			default:
1536 				throw new InvalidRequestException("This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName());
1537 		}
1538 
1539 		/*
1540 		 * If we've already got a join for the specific parameter we're
1541 		 * sorting on, we'll also sort with it. Otherwise we need a new join.
1542 		 */
1543 		JoinKey key = new JoinKey(theSort.getParamName(), joinType);
1544 		Join<?, ?> join = myIndexJoins.get(key);
1545 		if (join == null) {
1546 			join = theFrom.join(joinAttrName, JoinType.LEFT);
1547 
1548 			if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
1549 				thePredicates.add(join.get("mySourcePath").as(String.class).in(param.getPathsSplit()));
1550 			} else {
1551 				Predicate joinParam1 = theBuilder.equal(join.get("myParamName"), theSort.getParamName());
1552 				thePredicates.add(joinParam1);
1553 			}
1554 		} else {
1555 			ourLog.debug("Reusing join for {}", theSort.getParamName());
1556 		}
1557 
1558 		for (String next : sortAttrName) {
1559 			if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
1560 				theOrders.add(theBuilder.asc(join.get(next)));
1561 			} else {
1562 				theOrders.add(theBuilder.desc(join.get(next)));
1563 			}
1564 		}
1565 
1566 		createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates);
1567 
1568 		return true;
1569 	}
1570 
1571 	private String determineSystemIfMissing(String theParamName, String code, String theSystem) {
1572 		String retVal = theSystem;
1573 		if (retVal == null) {
1574 			RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName);
1575 			RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theParamName);
1576 			if (param != null) {
1577 				Set<String> valueSetUris = Sets.newHashSet();
1578 				for (String nextPath : param.getPathsSplit()) {
1579 					BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, nextPath);
1580 					if (def instanceof BaseRuntimeDeclaredChildDefinition) {
1581 						String valueSet = ((BaseRuntimeDeclaredChildDefinition) def).getBindingValueSet();
1582 						if (isNotBlank(valueSet)) {
1583 							valueSetUris.add(valueSet);
1584 						}
1585 					}
1586 				}
1587 				if (valueSetUris.size() == 1) {
1588 					String valueSet = valueSetUris.iterator().next();
1589 					List<VersionIndependentConcept> candidateCodes = myTerminologySvc.expandValueSet(valueSet);
1590 					for (VersionIndependentConcept nextCandidate : candidateCodes) {
1591 						if (nextCandidate.getCode().equals(code)) {
1592 							retVal = nextCandidate.getSystem();
1593 							break;
1594 						}
1595 					}
1596 				}
1597 			}
1598 		}
1599 		return retVal;
1600 	}
1601 
1602 	private void doLoadPids(List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation, EntityManager entityManager, FhirContext context, IDao theDao,
1603 									Map<Long, Integer> position, Collection<Long> pids) {
1604 
1605 		// -- get the resource from the searchView
1606 		Collection<ResourceSearchView> resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(pids);
1607 
1608 		//-- preload all tags with tag definition if any
1609 		Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
1610 
1611 		Long resourceId = null;
1612 		for (ResourceSearchView next : resourceSearchViewList) {
1613 
1614 			Class<? extends IBaseResource> resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass();
1615 
1616 			resourceId = next.getId();
1617 
1618 			IBaseResource resource = theDao.toResource(resourceType, next, tagMap.get(resourceId), theForHistoryOperation);
1619 			if (resource == null) {
1620 				ourLog.warn("Unable to find resource {}/{}/_history/{} in database", next.getResourceType(), next.getIdDt().getIdPart(), next.getVersion());
1621 				continue;
1622 			}
1623 			Integer index = position.get(resourceId);
1624 			if (index == null) {
1625 				ourLog.warn("Got back unexpected resource PID {}", resourceId);
1626 				continue;
1627 			}
1628 
1629 			if (resource instanceof IResource) {
1630 				if (theRevIncludedPids.contains(resourceId)) {
1631 					ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.INCLUDE);
1632 				} else {
1633 					ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.MATCH);
1634 				}
1635 			} else {
1636 				if (theRevIncludedPids.contains(resourceId)) {
1637 					ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.INCLUDE.getCode());
1638 				} else {
1639 					ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.MATCH.getCode());
1640 				}
1641 			}
1642 
1643 			theResourceListToPopulate.set(index, resource);
1644 		}
1645 	}
1646 
1647 	private Map<Long, Collection<ResourceTag>> getResourceTagMap(Collection<ResourceSearchView> theResourceSearchViewList) {
1648 
1649 		List<Long> idList = new ArrayList<Long>(theResourceSearchViewList.size());
1650 
1651 		//-- find all resource has tags
1652 		for (ResourceSearchView resource: theResourceSearchViewList) {
1653 			if (resource.isHasTags())
1654 				idList.add(resource.getId());
1655 		}
1656 
1657 		Map<Long, Collection<ResourceTag>> tagMap = new HashMap<>();
1658 
1659 		//-- no tags
1660 		if (idList.size() == 0)
1661 			return tagMap;
1662 
1663 		//-- get all tags for the idList
1664 		Collection<ResourceTag> tagList = myResourceTagDao.findByResourceIds(idList);
1665 
1666 		//-- build the map, key = resourceId, value = list of ResourceTag
1667 		Long resourceId;
1668 		Collection<ResourceTag> tagCol;
1669 		for (ResourceTag tag : tagList) {
1670 
1671 			resourceId = tag.getResourceId();
1672 			tagCol = tagMap.get(resourceId);
1673 			if (tagCol == null) {
1674 				tagCol = new ArrayList<>();
1675 				tagCol.add(tag);
1676 				tagMap.put(resourceId, tagCol);
1677 			} else {
1678 				tagCol.add(tag);
1679 			}
1680 		}
1681 
1682 		return tagMap;
1683 	}
1684 
1685 	@Override
1686 	public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation,
1687 											 EntityManager entityManager, FhirContext context, IDao theDao) {
1688 		if (theIncludePids.isEmpty()) {
1689 			ourLog.debug("The include pids are empty");
1690 			// return;
1691 		}
1692 
1693 		// Dupes will cause a crash later anyhow, but this is expensive so only do it
1694 		// when running asserts
1695 		assert new HashSet<>(theIncludePids).size() == theIncludePids.size() : "PID list contains duplicates: " + theIncludePids;
1696 
1697 		Map<Long, Integer> position = new HashMap<>();
1698 		for (Long next : theIncludePids) {
1699 			position.put(next, theResourceListToPopulate.size());
1700 			theResourceListToPopulate.add(null);
1701 		}
1702 
1703 		/*
1704 		 * As always, Oracle can't handle things that other databases don't mind.. In this
1705 		 * case it doesn't like more than ~1000 IDs in a single load, so we break this up
1706 		 * if it's lots of IDs. I suppose maybe we should be doing this as a join anyhow
1707 		 * but this should work too. Sigh.
1708 		 */
1709 		int maxLoad = 800;
1710 		List<Long> pids = new ArrayList<>(theIncludePids);
1711 		for (int i = 0; i < pids.size(); i += maxLoad) {
1712 			int to = i + maxLoad;
1713 			to = Math.min(to, pids.size());
1714 			List<Long> pidsSubList = pids.subList(i, to);
1715 			doLoadPids(theResourceListToPopulate, theRevIncludedPids, theForHistoryOperation, entityManager, context, theDao, position, pidsSubList);
1716 		}
1717 
1718 	}
1719 
1720 	/**
1721 	 * THIS SHOULD RETURN HASHSET and not just Set because we add to it later (so it can't be Collections.emptySet())
1722 	 */
1723 	@Override
1724 	public HashSet<Long> loadIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes,
1725 												 boolean theReverseMode, DateRangeParam theLastUpdated) {
1726 		if (theMatches.size() == 0) {
1727 			return new HashSet<>();
1728 		}
1729 		if (theRevIncludes == null || theRevIncludes.isEmpty()) {
1730 			return new HashSet<>();
1731 		}
1732 		String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid";
1733 
1734 		Collection<Long> nextRoundMatches = theMatches;
1735 		HashSet<Long> allAdded = new HashSet<>();
1736 		HashSet<Long> original = new HashSet<>(theMatches);
1737 		ArrayList<Include> includes = new ArrayList<>(theRevIncludes);
1738 
1739 		int roundCounts = 0;
1740 		StopWatch w = new StopWatch();
1741 
1742 		boolean addedSomeThisRound;
1743 		do {
1744 			roundCounts++;
1745 
1746 			HashSet<Long> pidsToInclude = new HashSet<>();
1747 
1748 			for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) {
1749 				Include nextInclude = iter.next();
1750 				if (nextInclude.isRecurse() == false) {
1751 					iter.remove();
1752 				}
1753 
1754 				boolean matchAll = "*".equals(nextInclude.getValue());
1755 				if (matchAll) {
1756 					String sql;
1757 					sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) ";
1758 					TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class);
1759 					q.setParameter("target_pids", nextRoundMatches);
1760 					List<ResourceLink> results = q.getResultList();
1761 					for (ResourceLink resourceLink : results) {
1762 						if (theReverseMode) {
1763 							pidsToInclude.add(resourceLink.getSourceResourcePid());
1764 						} else {
1765 							pidsToInclude.add(resourceLink.getTargetResourcePid());
1766 						}
1767 					}
1768 				} else {
1769 
1770 					List<String> paths;
1771 					RuntimeSearchParam param;
1772 					String resType = nextInclude.getParamType();
1773 					if (isBlank(resType)) {
1774 						continue;
1775 					}
1776 					RuntimeResourceDefinition def = theContext.getResourceDefinition(resType);
1777 					if (def == null) {
1778 						ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
1779 						continue;
1780 					}
1781 
1782 					String paramName = nextInclude.getParamName();
1783 					if (isNotBlank(paramName)) {
1784 						param = theCallingDao.getSearchParamByName(def, paramName);
1785 					} else {
1786 						param = null;
1787 					}
1788 					if (param == null) {
1789 						ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
1790 						continue;
1791 					}
1792 
1793 					paths = param.getPathsSplit();
1794 
1795 					String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
1796 					for (String nextPath : paths) {
1797 						String sql;
1798 						boolean haveTargetTypesDefinedByParam = param != null && param.getTargets() != null && param.getTargets().isEmpty() == false;
1799 						if (targetResourceType != null) {
1800 							sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type";
1801 						} else if (haveTargetTypesDefinedByParam) {
1802 							sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)";
1803 						} else {
1804 							sql = "SELECT r FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)";
1805 						}
1806 						TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class);
1807 						q.setParameter("src_path", nextPath);
1808 						q.setParameter("target_pids", nextRoundMatches);
1809 						if (targetResourceType != null) {
1810 							q.setParameter("target_resource_type", targetResourceType);
1811 						} else if (haveTargetTypesDefinedByParam) {
1812 							q.setParameter("target_resource_types", param.getTargets());
1813 						}
1814 						List<ResourceLink> results = q.getResultList();
1815 						for (ResourceLink resourceLink : results) {
1816 							if (theReverseMode) {
1817 								Long pid = resourceLink.getSourceResourcePid();
1818 								if (pid != null) {
1819 									pidsToInclude.add(pid);
1820 								}
1821 							} else {
1822 								Long pid = resourceLink.getTargetResourcePid();
1823 								if (pid != null) {
1824 									pidsToInclude.add(pid);
1825 								}
1826 							}
1827 						}
1828 					}
1829 				}
1830 			}
1831 
1832 			if (theReverseMode) {
1833 				if (theLastUpdated != null && (theLastUpdated.getLowerBoundAsInstant() != null || theLastUpdated.getUpperBoundAsInstant() != null)) {
1834 					pidsToInclude = new HashSet<>(filterResourceIdsByLastUpdated(theEntityManager, theLastUpdated, pidsToInclude));
1835 				}
1836 			}
1837 			for (Long next : pidsToInclude) {
1838 				if (original.contains(next) == false && allAdded.contains(next) == false) {
1839 					theMatches.add(next);
1840 				}
1841 			}
1842 
1843 			addedSomeThisRound = allAdded.addAll(pidsToInclude);
1844 			nextRoundMatches = pidsToInclude;
1845 		} while (includes.size() > 0 && nextRoundMatches.size() > 0 && addedSomeThisRound);
1846 
1847 		ourLog.info("Loaded {} {} in {} rounds and {} ms", allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart());
1848 
1849 		return allAdded;
1850 	}
1851 
1852 	private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams) {
1853 		myParams = theParams;
1854 
1855 		for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : myParams.entrySet()) {
1856 			String nextParamName = nextParamEntry.getKey();
1857 			List<List<? extends IQueryParameterType>> andOrParams = nextParamEntry.getValue();
1858 			searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams);
1859 
1860 		}
1861 
1862 	}
1863 
1864 	private void searchForIdsWithAndOr(String theResourceName, String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams) {
1865 
1866 		for (int andListIdx = 0; andListIdx < theAndOrParams.size(); andListIdx++) {
1867 			List<? extends IQueryParameterType> nextOrList = theAndOrParams.get(andListIdx);
1868 
1869 			for (int orListIdx = 0; orListIdx < nextOrList.size(); orListIdx++) {
1870 				IQueryParameterType nextOr = nextOrList.get(orListIdx);
1871 				boolean hasNoValue = false;
1872 				if (nextOr.getMissing() != null) {
1873 					continue;
1874 				}
1875 				if (nextOr instanceof QuantityParam) {
1876 					if (isBlank(((QuantityParam) nextOr).getValueAsString())) {
1877 						hasNoValue = true;
1878 					}
1879 				}
1880 
1881 				if (hasNoValue) {
1882 					ourLog.debug("Ignoring empty parameter: {}", theParamName);
1883 					nextOrList.remove(orListIdx);
1884 					orListIdx--;
1885 				}
1886 			}
1887 
1888 			if (nextOrList.isEmpty()) {
1889 				theAndOrParams.remove(andListIdx);
1890 				andListIdx--;
1891 			}
1892 		}
1893 
1894 		if (theAndOrParams.isEmpty()) {
1895 			return;
1896 		}
1897 
1898 		if (theParamName.equals(BaseResource.SP_RES_ID)) {
1899 
1900 			addPredicateResourceId(theAndOrParams);
1901 
1902 		} else if (theParamName.equals(BaseResource.SP_RES_LANGUAGE)) {
1903 
1904 			addPredicateLanguage(theAndOrParams);
1905 
1906 		} else if (theParamName.equals(Constants.PARAM_HAS)) {
1907 
1908 			addPredicateHas(theAndOrParams);
1909 
1910 		} else if (theParamName.equals(Constants.PARAM_TAG) || theParamName.equals(Constants.PARAM_PROFILE) || theParamName.equals(Constants.PARAM_SECURITY)) {
1911 
1912 			addPredicateTag(theAndOrParams, theParamName);
1913 
1914 		} else {
1915 
1916 			RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
1917 			if (nextParamDef != null) {
1918 				switch (nextParamDef.getParamType()) {
1919 					case DATE:
1920 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1921 							addPredicateDate(theResourceName, theParamName, nextAnd);
1922 						}
1923 						break;
1924 					case QUANTITY:
1925 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1926 							addPredicateQuantity(theResourceName, theParamName, nextAnd);
1927 						}
1928 						break;
1929 					case REFERENCE:
1930 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1931 							addPredicateReference(theResourceName, theParamName, nextAnd);
1932 						}
1933 						break;
1934 					case STRING:
1935 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1936 							addPredicateString(theResourceName, theParamName, nextAnd);
1937 						}
1938 						break;
1939 					case TOKEN:
1940 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1941 							addPredicateToken(theResourceName, theParamName, nextAnd);
1942 						}
1943 						break;
1944 					case NUMBER:
1945 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1946 							addPredicateNumber(theResourceName, theParamName, nextAnd);
1947 						}
1948 						break;
1949 					case COMPOSITE:
1950 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1951 							addPredicateComposite(theResourceName, nextParamDef, nextAnd);
1952 						}
1953 						break;
1954 					case URI:
1955 						for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1956 							addPredicateUri(theResourceName, theParamName, nextAnd);
1957 						}
1958 						break;
1959 					case HAS:
1960 						// should not happen
1961 						break;
1962 				}
1963 			} else {
1964 				if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) {
1965 					// These are handled later
1966 				} else {
1967 					throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName);
1968 				}
1969 			}
1970 		}
1971 	}
1972 
1973 	@Override
1974 	public void setFetchSize(int theFetchSize) {
1975 		myFetchSize = theFetchSize;
1976 	}
1977 
1978 	@Override
1979 	public void setType(Class<? extends IBaseResource> theResourceType, String theResourceName) {
1980 		myResourceType = theResourceType;
1981 		myResourceName = theResourceName;
1982 	}
1983 
1984 	private IQueryParameterType toParameterType(RuntimeSearchParam theParam) {
1985 		IQueryParameterType qp;
1986 		switch (theParam.getParamType()) {
1987 			case DATE:
1988 				qp = new DateParam();
1989 				break;
1990 			case NUMBER:
1991 				qp = new NumberParam();
1992 				break;
1993 			case QUANTITY:
1994 				qp = new QuantityParam();
1995 				break;
1996 			case STRING:
1997 				qp = new StringParam();
1998 				break;
1999 			case TOKEN:
2000 				qp = new TokenParam();
2001 				break;
2002 			case COMPOSITE:
2003 				List<RuntimeSearchParam> compositeOf = theParam.getCompositeOf();
2004 				if (compositeOf.size() != 2) {
2005 					throw new InternalErrorException("Parameter " + theParam.getName() + " has " + compositeOf.size() + " composite parts. Don't know how handlt this.");
2006 				}
2007 				IQueryParameterType leftParam = toParameterType(compositeOf.get(0));
2008 				IQueryParameterType rightParam = toParameterType(compositeOf.get(1));
2009 				qp = new CompositeParam<IQueryParameterType, IQueryParameterType>(leftParam, rightParam);
2010 				break;
2011 			case REFERENCE:
2012 				qp = new ReferenceParam();
2013 				break;
2014 			case URI:
2015 			case HAS:
2016 			default:
2017 				throw new InternalErrorException("Don't know how to convert param type: " + theParam.getParamType());
2018 		}
2019 		return qp;
2020 	}
2021 
2022 	private IQueryParameterType toParameterType(RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) {
2023 		IQueryParameterType qp = toParameterType(theParam);
2024 
2025 		qp.setValueAsQueryToken(myContext, theParam.getName(), theQualifier, theValueAsQueryToken);
2026 		return qp;
2027 	}
2028 
2029 	/**
2030 	 * Figures out the tolerance for a search. For example, if the user is searching for <code>4.00</code>, this method
2031 	 * returns <code>0.005</code> because we shold actually match values which are
2032 	 * <code>4 (+/-) 0.005</code> according to the FHIR specs.
2033 	 */
2034 	static BigDecimal calculateFuzzAmount(ParamPrefixEnum cmpValue, BigDecimal theValue) {
2035 		if (cmpValue == ParamPrefixEnum.APPROXIMATE) {
2036 			return theValue.multiply(new BigDecimal(0.1));
2037 		} else {
2038 			String plainString = theValue.toPlainString();
2039 			int dotIdx = plainString.indexOf('.');
2040 			if (dotIdx == -1) {
2041 				return new BigDecimal(0.5);
2042 			}
2043 
2044 			int precision = plainString.length() - (dotIdx);
2045 			double mul = Math.pow(10, -precision);
2046 			double val = mul * 5.0d;
2047 			return new BigDecimal(val);
2048 		}
2049 	}
2050 
2051 	private static List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
2052 		List<Predicate> lastUpdatedPredicates = new ArrayList<>();
2053 		if (theLastUpdated != null) {
2054 			if (theLastUpdated.getLowerBoundAsInstant() != null) {
2055 				ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
2056 				Predicate predicateLower = builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
2057 				lastUpdatedPredicates.add(predicateLower);
2058 			}
2059 			if (theLastUpdated.getUpperBoundAsInstant() != null) {
2060 				Predicate predicateUpper = builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
2061 				lastUpdatedPredicates.add(predicateUpper);
2062 			}
2063 		}
2064 		return lastUpdatedPredicates;
2065 	}
2066 
2067 	private static String createLeftAndRightMatchLikeExpression(String likeExpression) {
2068 		return "%" + likeExpression.replace("%", "[%]") + "%";
2069 	}
2070 
2071 	private static String createLeftMatchLikeExpression(String likeExpression) {
2072 		return likeExpression.replace("%", "[%]") + "%";
2073 	}
2074 
2075 	private static Predicate createResourceLinkPathPredicate(IDao theCallingDao, FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
2076 																				String theResourceType) {
2077 		RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
2078 		RuntimeSearchParam param = theCallingDao.getSearchParamByName(resourceDef, theParamName);
2079 		List<String> path = param.getPathsSplit();
2080 
2081 		/*
2082 		 * SearchParameters can declare paths on multiple resources
2083 		 * types. Here we only want the ones that actually apply.
2084 		 */
2085 		for (Iterator<String> iter = path.iterator(); iter.hasNext(); ) {
2086 			if (!iter.next().startsWith(theResourceType + ".")) {
2087 				iter.remove();
2088 			}
2089 		}
2090 		return theFrom.get("mySourcePath").in(path);
2091 	}
2092 
2093 	private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) {
2094 		if (thePids.isEmpty()) {
2095 			return Collections.emptyList();
2096 		}
2097 		CriteriaBuilder builder = theEntityManager.getCriteriaBuilder();
2098 		CriteriaQuery<Long> cq = builder.createQuery(Long.class);
2099 		Root<ResourceTable> from = cq.from(ResourceTable.class);
2100 		cq.select(from.get("myId").as(Long.class));
2101 
2102 		List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from);
2103 		lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(thePids));
2104 
2105 		cq.where(SearchBuilder.toArray(lastUpdatedPredicates));
2106 		TypedQuery<Long> query = theEntityManager.createQuery(cq);
2107 
2108 		List<Long> resultList = query.getResultList();
2109 		return resultList;
2110 	}
2111 
2112 	@VisibleForTesting
2113 	public static HandlerTypeEnum getLastHandlerMechanismForUnitTest() {
2114 		return ourLastHandlerMechanismForUnitTest;
2115 	}
2116 
2117 	@VisibleForTesting
2118 	public static String getLastHandlerParamsForUnitTest() {
2119 		return ourLastHandlerParamsForUnitTest.toString() + " on thread [" + ourLastHandlerThreadForUnitTest +"]";
2120 	}
2121 
2122 	@VisibleForTesting
2123 	public static void resetLastHandlerMechanismForUnitTest() {
2124 		ourLastHandlerMechanismForUnitTest = null;
2125 		ourLastHandlerParamsForUnitTest = null;
2126 		ourLastHandlerThreadForUnitTest = null;
2127 		ourTrackHandlersForUnitTest = true;
2128 	}
2129 
2130 	static Predicate[] toArray(List<Predicate> thePredicates) {
2131 		return thePredicates.toArray(new Predicate[thePredicates.size()]);
2132 	}
2133 
2134 	public enum HandlerTypeEnum {
2135 		UNIQUE_INDEX, STANDARD_QUERY
2136 	}
2137 
2138 	private enum JoinEnum {
2139 		DATE,
2140 		NUMBER,
2141 		QUANTITY,
2142 		REFERENCE,
2143 		STRING,
2144 		TOKEN,
2145 		URI
2146 
2147 	}
2148 
2149 	public class IncludesIterator extends BaseIterator<Long> implements Iterator<Long> {
2150 
2151 		private Iterator<Long> myCurrentIterator;
2152 		private int myCurrentOffset;
2153 		private ArrayList<Long> myCurrentPids;
2154 		private Long myNext;
2155 		private int myPageSize = myCallingDao.getConfig().getEverythingIncludesFetchPageSize();
2156 
2157 		public IncludesIterator(Set<Long> thePidSet) {
2158 			myCurrentPids = new ArrayList<Long>(thePidSet);
2159 			myCurrentIterator = EMPTY_LONG_LIST.iterator();
2160 			myCurrentOffset = 0;
2161 		}
2162 
2163 		private void fetchNext() {
2164 			while (myNext == null) {
2165 
2166 				if (myCurrentIterator.hasNext()) {
2167 					myNext = myCurrentIterator.next();
2168 					break;
2169 				}
2170 
2171 				if (!myCurrentIterator.hasNext()) {
2172 					int start = myCurrentOffset;
2173 					int end = myCurrentOffset + myPageSize;
2174 					if (end > myCurrentPids.size()) {
2175 						end = myCurrentPids.size();
2176 					}
2177 					if (end - start <= 0) {
2178 						myNext = NO_MORE;
2179 						break;
2180 					}
2181 					myCurrentOffset = end;
2182 					Collection<Long> pidsToScan = myCurrentPids.subList(start, end);
2183 					Set<Include> includes = Collections.singleton(new Include("*", true));
2184 					Set<Long> newPids = loadIncludes(myCallingDao, myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated());
2185 					myCurrentIterator = newPids.iterator();
2186 				}
2187 
2188 			}
2189 		}
2190 
2191 		@Override
2192 		public boolean hasNext() {
2193 			fetchNext();
2194 			return myNext != NO_MORE;
2195 		}
2196 
2197 		@Override
2198 		public Long next() {
2199 			fetchNext();
2200 			Long retVal = myNext;
2201 			myNext = null;
2202 			return retVal;
2203 		}
2204 
2205 	}
2206 
2207 	private final class QueryIterator extends BaseIterator<Long> implements Iterator<Long> {
2208 
2209 		private final Set<Long> myPidSet = new HashSet<Long>();
2210 		private boolean myFirst = true;
2211 		private IncludesIterator myIncludesIterator;
2212 		private Long myNext;
2213 		private Iterator<Long> myPreResultsIterator;
2214 		private Iterator<Long> myResultsIterator;
2215 		private SortSpec mySort;
2216 		private boolean myStillNeedToFetchIncludes;
2217 		private StopWatch myStopwatch = null;
2218 
2219 		private QueryIterator() {
2220 			mySort = myParams.getSort();
2221 
2222 			// Includes are processed inline for $everything query
2223 			if (myParams.getEverythingMode() != null) {
2224 				myStillNeedToFetchIncludes = true;
2225 			}
2226 		}
2227 
2228 		private void fetchNext() {
2229 
2230 			if (myFirst) {
2231 				myStopwatch = new StopWatch();
2232 			}
2233 
2234 			// If we don't have a query yet, create one
2235 			if (myResultsIterator == null) {
2236 				Integer maximumResults = myCallingDao.getConfig().getFetchSizeDefaultMaximum();
2237 
2238 				final TypedQuery<Long> query = createQuery(mySort, maximumResults);
2239 
2240 				Query<Long> hibernateQuery = (Query<Long>) query;
2241 				hibernateQuery.setFetchSize(myFetchSize);
2242 				ScrollableResults scroll = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
2243 				myResultsIterator = new ScrollableResultsIterator<>(scroll);
2244 
2245 				// If the query resulted in extra results being requested
2246 				if (myAlsoIncludePids != null) {
2247 					myPreResultsIterator = myAlsoIncludePids.iterator();
2248 				}
2249 			}
2250 
2251 			if (myNext == null) {
2252 
2253 				if (myPreResultsIterator != null && myPreResultsIterator.hasNext()) {
2254 					while (myPreResultsIterator.hasNext()) {
2255 						Long next = myPreResultsIterator.next();
2256 						if (next != null && myPidSet.add(next)) {
2257 							myNext = next;
2258 							break;
2259 						}
2260 					}
2261 				}
2262 
2263 				if (myNext == null) {
2264 					while (myResultsIterator.hasNext()) {
2265 						Long next = myResultsIterator.next();
2266 						if (next != null && myPidSet.add(next)) {
2267 							myNext = next;
2268 							break;
2269 						}
2270 					}
2271 				}
2272 
2273 				if (myNext == null) {
2274 					if (myStillNeedToFetchIncludes) {
2275 						myIncludesIterator = new IncludesIterator(myPidSet);
2276 						myStillNeedToFetchIncludes = false;
2277 					}
2278 					if (myIncludesIterator != null) {
2279 						while (myIncludesIterator.hasNext()) {
2280 							Long next = myIncludesIterator.next();
2281 							if (next != null && myPidSet.add(next)) {
2282 								myNext = next;
2283 								break;
2284 							}
2285 						}
2286 						if (myNext == null) {
2287 							myNext = NO_MORE;
2288 						}
2289 					} else {
2290 						myNext = NO_MORE;
2291 					}
2292 				}
2293 
2294 			} // if we need to fetch the next result
2295 
2296 			if (myFirst) {
2297 				ourLog.debug("Initial query result returned in {}ms for query {}", myStopwatch.getMillis(), mySearchUuid);
2298 				myFirst = false;
2299 			}
2300 
2301 			if (myNext == NO_MORE) {
2302 				ourLog.debug("Query found {} matches in {}ms for query {}", myPidSet.size(), myStopwatch.getMillis(), mySearchUuid);
2303 			}
2304 
2305 		}
2306 
2307 		@Override
2308 		public boolean hasNext() {
2309 			if (myNext == null) {
2310 				fetchNext();
2311 			}
2312 			return myNext != NO_MORE;
2313 		}
2314 
2315 		@Override
2316 		public Long next() {
2317 			fetchNext();
2318 			Long retVal = myNext;
2319 			myNext = null;
2320 			Validate.isTrue(retVal != NO_MORE, "No more elements");
2321 			return retVal;
2322 		}
2323 	}
2324 
2325 	private class UniqueIndexIterator implements Iterator<Long> {
2326 		private final Set<String> myUniqueQueryStrings;
2327 		private Iterator<Long> myWrap = null;
2328 
2329 		public UniqueIndexIterator(Set<String> theUniqueQueryStrings) {
2330 			myUniqueQueryStrings = theUniqueQueryStrings;
2331 		}
2332 
2333 		private void ensureHaveQuery() {
2334 			if (myWrap == null) {
2335 				ourLog.debug("Searching for unique index matches over {} candidate query strings", myUniqueQueryStrings.size());
2336 				StopWatch sw = new StopWatch();
2337 				Collection<Long> resourcePids = myCallingDao.getResourceIndexedCompositeStringUniqueDao().findResourcePidsByQueryStrings(myUniqueQueryStrings);
2338 				ourLog.debug("Found {} unique index matches in {}ms", resourcePids.size(), sw.getMillis());
2339 				myWrap = resourcePids.iterator();
2340 			}
2341 		}
2342 
2343 		@Override
2344 		public boolean hasNext() {
2345 			ensureHaveQuery();
2346 			return myWrap.hasNext();
2347 		}
2348 
2349 		@Override
2350 		public Long next() {
2351 			ensureHaveQuery();
2352 			return myWrap.next();
2353 		}
2354 
2355 		@Override
2356 		public void remove() {
2357 			throw new UnsupportedOperationException();
2358 		}
2359 	}
2360 
2361 	private static class JoinKey {
2362 		private final JoinEnum myJoinType;
2363 		private final String myParamName;
2364 
2365 		public JoinKey(String theParamName, JoinEnum theJoinType) {
2366 			super();
2367 			myParamName = theParamName;
2368 			myJoinType = theJoinType;
2369 		}
2370 
2371 		@Override
2372 		public boolean equals(Object theObj) {
2373 			JoinKey obj = (JoinKey) theObj;
2374 			return new EqualsBuilder()
2375 				.append(myParamName, obj.myParamName)
2376 				.append(myJoinType, obj.myJoinType)
2377 				.isEquals();
2378 		}
2379 
2380 		@Override
2381 		public int hashCode() {
2382 			return new HashCodeBuilder()
2383 				.append(myParamName)
2384 				.append(myJoinType)
2385 				.toHashCode();
2386 		}
2387 	}
2388 
2389 }