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