View Javadoc
1   package ca.uhn.fhir.jpa.dao.r4;
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.ConfigurationException;
24  import ca.uhn.fhir.context.FhirContext;
25  import ca.uhn.fhir.context.RuntimeSearchParam;
26  import ca.uhn.fhir.jpa.dao.*;
27  import ca.uhn.fhir.jpa.entity.*;
28  import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
29  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
30  import com.google.common.annotations.VisibleForTesting;
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.commons.lang3.tuple.Pair;
33  import org.hl7.fhir.exceptions.FHIRException;
34  import org.hl7.fhir.exceptions.PathEngineException;
35  import org.hl7.fhir.instance.model.api.IBase;
36  import org.hl7.fhir.instance.model.api.IBaseResource;
37  import org.hl7.fhir.instance.model.api.IPrimitiveType;
38  import org.hl7.fhir.r4.context.IWorkerContext;
39  import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
40  import org.hl7.fhir.r4.model.*;
41  import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestSecurityComponent;
42  import org.hl7.fhir.r4.model.Enumeration;
43  import org.hl7.fhir.r4.model.Location.LocationPositionComponent;
44  import org.hl7.fhir.r4.model.Patient.PatientCommunicationComponent;
45  import org.hl7.fhir.r4.utils.FHIRPathEngine;
46  import org.springframework.beans.factory.annotation.Autowired;
47  
48  import javax.measure.unit.NonSI;
49  import javax.measure.unit.Unit;
50  import java.math.BigDecimal;
51  import java.util.*;
52  import java.util.regex.Matcher;
53  import java.util.regex.Pattern;
54  
55  import static org.apache.commons.lang3.StringUtils.isBlank;
56  import static org.apache.commons.lang3.StringUtils.isNotBlank;
57  import static org.apache.commons.lang3.StringUtils.trim;
58  
59  public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor {
60  
61  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorR4.class);
62  	@Autowired
63  	private org.hl7.fhir.r4.hapi.ctx.IValidationSupport myValidationSupport;
64  
65  	/**
66  	 * Constructor
67  	 */
68  	public SearchParamExtractorR4() {
69  		super();
70  	}
71  
72  	public SearchParamExtractorR4(DaoConfig theDaoConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) {
73  		super(theDaoConfig, theCtx, theSearchParamRegistry);
74  		myValidationSupport = theValidationSupport;
75  	}
76  
77  	private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
78  		if (!nextValue.getValueElement().isEmpty()) {
79  			BigDecimal nextValueValue = nextValue.getValueElement().getValue();
80  			String nextValueString = nextValue.getSystemElement().getValueAsString();
81  			String nextValueCode = nextValue.getCode();
82  			ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
83  			nextEntity.setResource(theEntity);
84  			retVal.add(nextEntity);
85  		}
86  	}
87  
88  	private void addSearchTerm(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> retVal, String resourceName, String searchTerm) {
89  		if (isBlank(searchTerm)) {
90  			return;
91  		}
92  		if (searchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) {
93  			searchTerm = searchTerm.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH);
94  		}
95  
96  		ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm);
97  		nextEntity.setResource(theEntity);
98  		retVal.add(nextEntity);
99  	}
100 
101 	private void addStringParam(ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> retVal, RuntimeSearchParam nextSpDef, String value) {
102 		if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) {
103 			value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH);
104 		}
105 		ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value);
106 		nextEntity.setResource(theEntity);
107 		retVal.add(nextEntity);
108 	}
109 
110 	@Override
111 	public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
112 		ArrayList<PathAndRef> retVal = new ArrayList<>();
113 
114 		String[] nextPathsSplit = SPLIT_R4.split(theNextSpDef.getPath());
115 		for (String path : nextPathsSplit) {
116 			path = path.trim();
117 			if (isNotBlank(path)) {
118 
119 				for (Object next : extractValues(path, theResource)) {
120 					retVal.add(new PathAndRef(path, next));
121 				}
122 			}
123 		}
124 
125 		return retVal;
126 	}
127 
128 	@Override
129 	public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
130 		// TODO: implement
131 		return Collections.emptySet();
132 	}
133 
134 	/*
135 	 * (non-Javadoc)
136 	 *
137 	 * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
138 	 */
139 	@Override
140 	public Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
141 		HashSet<ResourceIndexedSearchParamDate> retVal = new HashSet<>();
142 
143 		Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
144 		for (RuntimeSearchParam nextSpDef : searchParams) {
145 			if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.DATE) {
146 				continue;
147 			}
148 
149 			String nextPath = nextSpDef.getPath();
150 			if (isBlank(nextPath)) {
151 				continue;
152 			}
153 
154 			boolean multiType = false;
155 			if (nextPath.endsWith("[x]")) {
156 				multiType = true;
157 			}
158 
159 			for (Object nextObject : extractValues(nextPath, theResource)) {
160 				if (nextObject == null) {
161 					continue;
162 				}
163 
164 				ResourceIndexedSearchParamDate nextEntity;
165 				if (nextObject instanceof BaseDateTimeType) {
166 					BaseDateTimeType nextValue = (BaseDateTimeType) nextObject;
167 					if (nextValue.isEmpty()) {
168 						continue;
169 					}
170 					nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), nextValue.getValue(), nextValue.getValue(), nextValue.getValueAsString());
171 				} else if (nextObject instanceof Period) {
172 					Period nextValue = (Period) nextObject;
173 					if (nextValue.isEmpty()) {
174 						continue;
175 					}
176 					nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), nextValue.getStart(), nextValue.getEnd(), nextValue.getStartElement().getValueAsString());
177 				} else if (nextObject instanceof Timing) {
178 					Timing nextValue = (Timing) nextObject;
179 					if (nextValue.isEmpty()) {
180 						continue;
181 					}
182 					TreeSet<Date> dates = new TreeSet<>();
183 					String firstValue = null;
184 					for (DateTimeType nextEvent : nextValue.getEvent()) {
185 						if (nextEvent.getValue() != null) {
186 							dates.add(nextEvent.getValue());
187 							if (firstValue == null) {
188 								firstValue = nextEvent.getValueAsString();
189 							}
190 						}
191 					}
192 					if (dates.isEmpty()) {
193 						continue;
194 					}
195 
196 					nextEntity = new ResourceIndexedSearchParamDate(nextSpDef.getName(), dates.first(), dates.last(), firstValue);
197 				} else if (nextObject instanceof StringType) {
198 					// CarePlan.activitydate can be a string
199 					continue;
200 				} else {
201 					if (!multiType) {
202 						throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass());
203 					} else {
204 						continue;
205 					}
206 				}
207 				if (nextEntity != null) {
208 					nextEntity.setResource(theEntity);
209 					retVal.add(nextEntity);
210 				}
211 			}
212 		}
213 
214 		return retVal;
215 	}
216 
217 	/*
218 	 * (non-Javadoc)
219 	 *
220 	 * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
221 	 */
222 	@Override
223 	public HashSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
224 		HashSet<ResourceIndexedSearchParamNumber> retVal = new HashSet<>();
225 
226 		Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
227 		for (RuntimeSearchParam nextSpDef : searchParams) {
228 			if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.NUMBER) {
229 				continue;
230 			}
231 
232 			String nextPath = nextSpDef.getPath();
233 			if (isBlank(nextPath)) {
234 				continue;
235 			}
236 
237 			for (Object nextObject : extractValues(nextPath, theResource)) {
238 				if (nextObject == null || ((IBase) nextObject).isEmpty()) {
239 					continue;
240 				}
241 
242 				String resourceName = nextSpDef.getName();
243 				boolean multiType = false;
244 				if (nextPath.endsWith("[x]")) {
245 					multiType = true;
246 				}
247 
248 				if (nextObject instanceof Duration) {
249 					Duration nextValue = (Duration) nextObject;
250 					if (nextValue.getValueElement().isEmpty()) {
251 						continue;
252 					}
253 
254 					if (BaseHapiFhirDao.UCUM_NS.equals(nextValue.getSystem())) {
255 						if (isNotBlank(nextValue.getCode())) {
256 
257 							Unit<? extends javax.measure.quantity.Quantity> unit = Unit.valueOf(nextValue.getCode());
258 							javax.measure.converter.UnitConverter dayConverter = unit.getConverterTo(NonSI.DAY);
259 							double dayValue = dayConverter.convert(nextValue.getValue().doubleValue());
260 							Duration newValue = new Duration();
261 							newValue.setSystem(BaseHapiFhirDao.UCUM_NS);
262 							newValue.setCode(NonSI.DAY.toString());
263 							newValue.setValue(dayValue);
264 							nextValue = newValue;
265 
266 							/*
267 							 * @SuppressWarnings("unchecked") PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>> unit = (PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>>)
268 							 * UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if (unit.isCompatible(UCUM.DAY)) {
269 							 *
270 							 * @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = (PhysicsUnit<Time>) unit; UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY);
271 							 * double dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); Duration newValue = new Duration(); newValue.setSystem(UCUM_NS);
272 							 * newValue.setCode(UCUM.DAY.getSymbol()); newValue.setValue(dayValue); nextValue=newValue; }
273 							 */
274 						}
275 					}
276 
277 					ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, nextValue.getValue());
278 					nextEntity.setResource(theEntity);
279 					retVal.add(nextEntity);
280 				} else if (nextObject instanceof Quantity) {
281 					Quantity nextValue = (Quantity) nextObject;
282 					if (nextValue.getValueElement().isEmpty()) {
283 						continue;
284 					}
285 
286 					ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, nextValue.getValue());
287 					nextEntity.setResource(theEntity);
288 					retVal.add(nextEntity);
289 				} else if (nextObject instanceof IntegerType) {
290 					IntegerType nextValue = (IntegerType) nextObject;
291 					if (nextValue.getValue() == null) {
292 						continue;
293 					}
294 
295 					ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, new BigDecimal(nextValue.getValue()));
296 					nextEntity.setResource(theEntity);
297 					retVal.add(nextEntity);
298 				} else if (nextObject instanceof DecimalType) {
299 					DecimalType nextValue = (DecimalType) nextObject;
300 					if (nextValue.getValue() == null) {
301 						continue;
302 					}
303 
304 					ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(resourceName, nextValue.getValue());
305 					nextEntity.setResource(theEntity);
306 					retVal.add(nextEntity);
307 				} else {
308 					if (!multiType) {
309 						throw new ConfigurationException("Search param " + resourceName + " is of unexpected datatype: " + nextObject.getClass());
310 					} else {
311 						continue;
312 					}
313 				}
314 			}
315 		}
316 
317 		return retVal;
318 	}
319 
320 	/*
321 	 * (non-Javadoc)
322 	 *
323 	 * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
324 	 */
325 	@Override
326 	public Set<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) {
327 		HashSet<ResourceIndexedSearchParamQuantity> retVal = new HashSet<>();
328 
329 		Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
330 		for (RuntimeSearchParam nextSpDef : searchParams) {
331 			if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.QUANTITY) {
332 				continue;
333 			}
334 
335 			String nextPath = nextSpDef.getPath();
336 			if (isBlank(nextPath)) {
337 				continue;
338 			}
339 
340 			for (Object nextObject : extractValues(nextPath, theResource)) {
341 				if (nextObject == null || ((IBase) nextObject).isEmpty()) {
342 					continue;
343 				}
344 
345 				String resourceName = nextSpDef.getName();
346 				boolean multiType = false;
347 				if (nextPath.endsWith("[x]")) {
348 					multiType = true;
349 				}
350 
351 				if (nextObject instanceof Quantity) {
352 					Quantity nextValue = (Quantity) nextObject;
353 					addQuantity(theEntity, retVal, resourceName, nextValue);
354 				} else if (nextObject instanceof Range) {
355 					Range nextValue = (Range) nextObject;
356 					addQuantity(theEntity, retVal, resourceName, nextValue.getLow());
357 					addQuantity(theEntity, retVal, resourceName, nextValue.getHigh());
358 				} else if (nextObject instanceof LocationPositionComponent) {
359 					continue;
360 				} else {
361 					if (!multiType) {
362 						throw new ConfigurationException("Search param " + resourceName + " is of unexpected datatype: " + nextObject.getClass());
363 					} else {
364 						continue;
365 					}
366 				}
367 			}
368 		}
369 
370 		return retVal;
371 	}
372 
373 	/*
374 	 * (non-Javadoc)
375 	 *
376 	 * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
377 	 */
378 	@Override
379 	public Set<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) {
380 		HashSet<ResourceIndexedSearchParamString> retVal = new HashSet<>();
381 
382 		String resourceName = getContext().getResourceDefinition(theResource).getName();
383 
384 		Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
385 		for (RuntimeSearchParam nextSpDef : searchParams) {
386 			if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.STRING) {
387 				continue;
388 			}
389 
390 			String nextPath = nextSpDef.getPath();
391 			String nextSpName = nextSpDef.getName();
392 
393 			if (isBlank(nextPath)) {
394 
395 				// // TODO: implement phonetic, and any others that have no path
396 				//
397 				// // TODO: do we still need this check?
398 				// if ("Questionnaire".equals(nextSpName) && nextSpDef.getName().equals("title")) {
399 				// Questionnaire q = (Questionnaire) theResource;
400 				// String title = "";// q.getGroup().getTitle();
401 				// addSearchTerm(theEntity, retVal, nextSpName, title);
402 				// }
403 
404 				continue;
405 			}
406 
407 			for (Object nextObject : extractValues(nextPath, theResource)) {
408 				if (nextObject == null || ((IBase) nextObject).isEmpty()) {
409 					continue;
410 				}
411 
412 				boolean multiType = false;
413 				if (nextPath.endsWith("[x]")) {
414 					multiType = true;
415 				}
416 
417 				if (nextObject instanceof IPrimitiveType<?>) {
418 					IPrimitiveType<?> nextValue = (IPrimitiveType<?>) nextObject;
419 					String searchTerm = nextValue.getValueAsString();
420 					addSearchTerm(theEntity, retVal, nextSpName, searchTerm);
421 				} else {
422 					if (nextObject instanceof HumanName) {
423 						ArrayList<StringType> allNames = new ArrayList<>();
424 						HumanName nextHumanName = (HumanName) nextObject;
425 						if (isNotBlank(nextHumanName.getFamily())) {
426 							allNames.add(nextHumanName.getFamilyElement());
427 						}
428 						allNames.addAll(nextHumanName.getGiven());
429 						for (StringType nextName : allNames) {
430 							addSearchTerm(theEntity, retVal, nextSpName, nextName.getValue());
431 						}
432 					} else if (nextObject instanceof Address) {
433 						ArrayList<StringType> allNames = new ArrayList<>();
434 						Address nextAddress = (Address) nextObject;
435 						allNames.addAll(nextAddress.getLine());
436 						allNames.add(nextAddress.getCityElement());
437 						allNames.add(nextAddress.getStateElement());
438 						allNames.add(nextAddress.getCountryElement());
439 						allNames.add(nextAddress.getPostalCodeElement());
440 						for (StringType nextName : allNames) {
441 							addSearchTerm(theEntity, retVal, nextSpName, nextName.getValue());
442 						}
443 					} else if (nextObject instanceof ContactPoint) {
444 						ContactPoint nextContact = (ContactPoint) nextObject;
445 						if (nextContact.getValueElement().isEmpty() == false) {
446 							addSearchTerm(theEntity, retVal, nextSpName, nextContact.getValue());
447 						}
448 					} else if (nextObject instanceof Quantity) {
449 						BigDecimal value = ((Quantity) nextObject).getValue();
450 						if (value != null) {
451 							addSearchTerm(theEntity, retVal, nextSpName, value.toPlainString());
452 						}
453 					} else if (nextObject instanceof Range) {
454 						Quantity low = ((Range) nextObject).getLow();
455 						if (low != null) {
456 							BigDecimal value = low.getValue();
457 							if (value != null) {
458 								addSearchTerm(theEntity, retVal, nextSpName, value.toPlainString());
459 							}
460 						}
461 					} else {
462 						if (!multiType) {
463 							throw new ConfigurationException("Search param " + nextSpName + " is of unexpected datatype: " + nextObject.getClass());
464 						}
465 					}
466 				}
467 			}
468 		}
469 
470 		return retVal;
471 	}
472 
473 	/*
474 	 * (non-Javadoc)
475 	 *
476 	 * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
477 	 */
478 	@Override
479 	public Set<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) {
480 		HashSet<BaseResourceIndexedSearchParam> retVal = new HashSet<>();
481 
482 		String useSystem = null;
483 		if (theResource instanceof CodeSystem) {
484 			CodeSystem cs = (CodeSystem) theResource;
485 			useSystem = cs.getUrl();
486 		}
487 
488 		Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
489 		for (RuntimeSearchParam nextSpDef : searchParams) {
490 			if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.TOKEN) {
491 				continue;
492 			}
493 
494 			String resourceType = theEntity.getResourceType();
495 			String nextPath = nextSpDef.getPath();
496 			if (isBlank(nextPath)) {
497 				continue;
498 			}
499 
500 			boolean multiType = false;
501 			if (nextPath.endsWith("[x]")) {
502 				multiType = true;
503 			}
504 
505 			List<String> systems = new ArrayList<>();
506 			List<String> codes = new ArrayList<>();
507 
508 			for (Object nextObject : extractValues(nextPath, theResource)) {
509 
510 				if (nextObject == null) {
511 					continue;
512 				}
513 
514 				// Patient:language
515 				if (nextObject instanceof PatientCommunicationComponent) {
516 					PatientCommunicationComponent nextValue = (PatientCommunicationComponent) nextObject;
517 					nextObject = nextValue.getLanguage();
518 				}
519 
520 				if (nextObject instanceof Identifier) {
521 					Identifier nextValue = (Identifier) nextObject;
522 					if (nextValue.isEmpty()) {
523 						continue;
524 					}
525 					String system = StringUtils.defaultIfBlank(nextValue.getSystemElement().getValueAsString(), null);
526 					String value = nextValue.getValueElement().getValue();
527 					if (isNotBlank(value)) {
528 						systems.add(system);
529 						codes.add(value);
530 					}
531 
532 					if (isNotBlank(nextValue.getType().getText())) {
533 						addStringParam(theEntity, retVal, nextSpDef, nextValue.getType().getText());
534 					}
535 
536 				} else if (nextObject instanceof ContactPoint) {
537 					ContactPoint nextValue = (ContactPoint) nextObject;
538 					if (nextValue.isEmpty()) {
539 						continue;
540 					}
541 					systems.add(nextValue.getSystemElement().getValueAsString());
542 					codes.add(nextValue.getValueElement().getValue());
543 				} else if (nextObject instanceof Enumeration<?>) {
544 					Enumeration<?> obj = (Enumeration<?>) nextObject;
545 					String system = extractSystem(obj);
546 					String code = obj.getValueAsString();
547 					if (isNotBlank(code)) {
548 						systems.add(system);
549 						codes.add(code);
550 					}
551 				} else if (nextObject instanceof IPrimitiveType<?>) {
552 					IPrimitiveType<?> nextValue = (IPrimitiveType<?>) nextObject;
553 					if (nextValue.isEmpty()) {
554 						continue;
555 					}
556 					if ("CodeSystem.concept.code".equals(nextPath)) {
557 						systems.add(useSystem);
558 					} else {
559 						systems.add(null);
560 					}
561 					codes.add(nextValue.getValueAsString());
562 				} else if (nextObject instanceof Coding) {
563 					Coding nextValue = (Coding) nextObject;
564 					extractTokensFromCoding(systems, codes, theEntity, retVal, nextSpDef, nextValue);
565 				} else if (nextObject instanceof CodeableConcept) {
566 					CodeableConcept nextCC = (CodeableConcept) nextObject;
567 					if (!nextCC.getTextElement().isEmpty()) {
568 						addStringParam(theEntity, retVal, nextSpDef, nextCC.getTextElement().getValue());
569 					}
570 
571 					extractTokensFromCodeableConcept(systems, codes, nextCC, theEntity, retVal, nextSpDef);
572 				} else if (nextObject instanceof CapabilityStatementRestSecurityComponent) {
573 					// Conformance.security search param points to something kind of useless right now - This should probably
574 					// be fixed.
575 					CapabilityStatementRestSecurityComponent sec = (CapabilityStatementRestSecurityComponent) nextObject;
576 					for (CodeableConcept nextCC : sec.getService()) {
577 						extractTokensFromCodeableConcept(systems, codes, nextCC, theEntity, retVal, nextSpDef);
578 					}
579 				} else if (nextObject instanceof LocationPositionComponent) {
580 					ourLog.warn("Position search not currently supported, not indexing location");
581 					continue;
582 				} else if (nextObject instanceof StructureDefinition.StructureDefinitionContextComponent) {
583 					ourLog.warn("StructureDefinition context indexing not currently supported"); // TODO: implement this
584 					continue;
585 				} else if (resourceType.equals("Consent") && nextPath.equals("Consent.source")) {
586 					// Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that
587 					continue;
588 				} else {
589 					if (!multiType) {
590 						throw new ConfigurationException("Search param " + nextSpDef.getName() + " with path " + nextPath + " is of unexpected datatype: " + nextObject.getClass());
591 					} else {
592 						continue;
593 					}
594 				}
595 			}
596 
597 			assert systems.size() == codes.size() : "Systems contains " + systems + ", codes contains: " + codes;
598 
599 			Set<Pair<String, String>> haveValues = new HashSet<>();
600 			for (int i = 0; i < systems.size(); i++) {
601 				String system = systems.get(i);
602 				String code = codes.get(i);
603 				if (isBlank(system) && isBlank(code)) {
604 					continue;
605 				}
606 
607 				if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
608 					system = system.substring(0, ResourceIndexedSearchParamToken.MAX_LENGTH);
609 				}
610 				if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
611 					code = code.substring(0, ResourceIndexedSearchParamToken.MAX_LENGTH);
612 				}
613 
614 				Pair<String, String> nextPair = Pair.of(system, code);
615 				if (haveValues.contains(nextPair)) {
616 					continue;
617 				}
618 				haveValues.add(nextPair);
619 
620 				ResourceIndexedSearchParamToken nextEntity;
621 				nextEntity = new ResourceIndexedSearchParamToken(nextSpDef.getName(), system, code);
622 				nextEntity.setResource(theEntity);
623 				retVal.add(nextEntity);
624 
625 			}
626 
627 		}
628 
629 		return retVal;
630 	}
631 
632 	@Override
633 	public Set<ResourceIndexedSearchParamUri> extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) {
634 		HashSet<ResourceIndexedSearchParamUri> retVal = new HashSet<>();
635 
636 		Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
637 		for (RuntimeSearchParam nextSpDef : searchParams) {
638 			if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.URI) {
639 				continue;
640 			}
641 
642 			String nextPath = nextSpDef.getPath();
643 			if (isBlank(nextPath)) {
644 				continue;
645 			}
646 
647 			for (Object nextObject : extractValues(nextPath, theResource)) {
648 				if (nextObject == null || ((IBase) nextObject).isEmpty()) {
649 					continue;
650 				}
651 
652 				String resourceName = nextSpDef.getName();
653 				boolean multiType = false;
654 				if (nextPath.endsWith("[x]")) {
655 					multiType = true;
656 				}
657 
658 				if (nextObject instanceof UriType) {
659 					UriType nextValue = (UriType) nextObject;
660 					if (isBlank(nextValue.getValue())) {
661 						continue;
662 					}
663 
664 					ourLog.trace("Adding param: {}, {}", resourceName, nextValue.getValue());
665 
666 					ResourceIndexedSearchParamUri nextEntity = new ResourceIndexedSearchParamUri(resourceName, nextValue.getValue());
667 
668 					nextEntity.setResource(theEntity);
669 					retVal.add(nextEntity);
670 				} else {
671 					if (!multiType) {
672 						throw new ConfigurationException("Search param " + resourceName + " is of unexpected datatype: " + nextObject.getClass());
673 					} else {
674 						continue;
675 					}
676 				}
677 			}
678 		}
679 
680 		return retVal;
681 	}
682 
683 	private void extractTokensFromCodeableConcept(List<String> theSystems, List<String> theCodes, CodeableConcept theCodeableConcept, ResourceTable theEntity,
684 																 Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
685 		for (Coding nextCoding : theCodeableConcept.getCoding()) {
686 			extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding);
687 		}
688 	}
689 
690 	private void extractTokensFromCoding(List<String> theSystems, List<String> theCodes, ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> theListToPopulate,
691 													 RuntimeSearchParam theParameterDef, Coding nextCoding) {
692 		if (nextCoding != null && !nextCoding.isEmpty()) {
693 
694 			String nextSystem = nextCoding.getSystemElement().getValueAsString();
695 			String nextCode = nextCoding.getCodeElement().getValue();
696 			if (isNotBlank(nextSystem) || isNotBlank(nextCode)) {
697 				theSystems.add(nextSystem);
698 				theCodes.add(nextCode);
699 			}
700 
701 			if (!nextCoding.getDisplayElement().isEmpty()) {
702 				addStringParam(theEntity, theListToPopulate, theParameterDef, nextCoding.getDisplayElement().getValue());
703 			}
704 
705 		}
706 	}
707 
708 	/**
709 	 * Override parent because we're using FHIRPath here
710 	 */
711 	@Override
712 	protected List<Object> extractValues(String thePaths, IBaseResource theResource) {
713 		IWorkerContext worker = new org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext(getContext(), myValidationSupport);
714 		FHIRPathEngine fp = new FHIRPathEngine(worker);
715 		fp.setHostServices(new SearchParamExtractorR4HostServices());
716 
717 		List<Object> values = new ArrayList<>();
718 		String[] nextPathsSplit = SPLIT_R4.split(thePaths);
719 		for (String nextPath : nextPathsSplit) {
720 			List<Base> allValues;
721 			try {
722 				allValues = fp.evaluate((Base) theResource, nextPath);
723 			} catch (FHIRException e) {
724 				String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
725 				throw new InternalErrorException(msg, e);
726 			}
727 			if (allValues.isEmpty() == false) {
728 				values.addAll(allValues);
729 			}
730 		}
731 
732 		for (int i = 0; i < values.size(); i++) {
733 			Object nextObject = values.get(i);
734 			if (nextObject instanceof Extension) {
735 				Extension nextExtension = (Extension) nextObject;
736 				nextObject = nextExtension.getValue();
737 				values.set(i, nextObject);
738 			}
739 		}
740 
741 		return values;
742 	}
743 
744 	@VisibleForTesting
745 	void setValidationSupportForTesting(org.hl7.fhir.r4.hapi.ctx.IValidationSupport theValidationSupport) {
746 		myValidationSupport = theValidationSupport;
747 	}
748 
749 	private static <T extends Enum<?>> String extractSystem(Enumeration<T> theBoundCode) {
750 		if (theBoundCode.getValue() != null) {
751 			return theBoundCode.getEnumFactory().toSystem(theBoundCode.getValue());
752 		}
753 		return null;
754 	}
755 
756 
757 	private class SearchParamExtractorR4HostServices implements FHIRPathEngine.IEvaluationContext {
758 		@Override
759 		public Base resolveConstant(Object appContext, String name) throws PathEngineException {
760 			return null;
761 		}
762 
763 		@Override
764 		public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
765 			return null;
766 		}
767 
768 		@Override
769 		public boolean log(String argument, List<Base> focus) {
770 			return false;
771 		}
772 
773 		@Override
774 		public FunctionDetails resolveFunction(String functionName) {
775 			return null;
776 		}
777 
778 		@Override
779 		public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
780 			return null;
781 		}
782 
783 		@Override
784 		public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) {
785 			return null;
786 		}
787 
788 		private Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
789 
790 		@Override
791 		public Base resolveReference(Object theAppContext, String theUrl) throws FHIRException {
792 
793 			/*
794 			 * When we're doing resolution within the SearchParamExtractor, if we want
795 			 * to do a resolve() it's just to check the type, so there is no point
796 			 * going through the heavyweight test. We can just return a stub and
797 			 * that's good enough since we're just doing something like
798 			 *    Encounter.patient.where(resolve() is Patient)
799 			 */
800 			IdType url = new IdType(theUrl);
801 			Base retVal = null;
802 			if (isNotBlank(url.getResourceType())) {
803 
804 				retVal = myResourceTypeToStub.get(url.getResourceType());
805 				if (retVal != null) {
806 					return retVal;
807 				}
808 
809 				ResourceType resourceType = ResourceType.fromCode(url.getResourceType());
810 				if (resourceType != null) {
811 					retVal = new Resource(){
812 						@Override
813 						public Resource copy() {
814 							return this;
815 						}
816 
817 						@Override
818 						public ResourceType getResourceType() {
819 							return resourceType;
820 						}
821 
822 						@Override
823 						public String fhirType() {
824 							return url.getResourceType();
825 						}
826 
827 					};
828 					myResourceTypeToStub.put(url.getResourceType(), retVal);
829 				}
830 			}
831 			return retVal;
832 		}
833 
834 		@Override
835 		public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
836 			return false;
837 		}
838 	}
839 
840 }