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