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