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