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