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