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.*;
25  import ca.uhn.fhir.jpa.entity.*;
26  import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
27  import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
28  import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
29  import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
30  import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
31  import ca.uhn.fhir.jpa.util.DeleteConflict;
32  import ca.uhn.fhir.jpa.util.ExpungeOptions;
33  import ca.uhn.fhir.jpa.util.ExpungeOutcome;
34  import ca.uhn.fhir.jpa.util.JpaConstants;
35  import ca.uhn.fhir.model.api.*;
36  import ca.uhn.fhir.model.base.composite.BaseCodingDt;
37  import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
38  import ca.uhn.fhir.model.primitive.IdDt;
39  import ca.uhn.fhir.model.primitive.InstantDt;
40  import ca.uhn.fhir.model.primitive.StringDt;
41  import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
42  import ca.uhn.fhir.parser.DataFormatException;
43  import ca.uhn.fhir.parser.IParser;
44  import ca.uhn.fhir.parser.LenientErrorHandler;
45  import ca.uhn.fhir.rest.api.Constants;
46  import ca.uhn.fhir.rest.api.QualifiedParamList;
47  import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
48  import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
49  import ca.uhn.fhir.rest.api.server.IBundleProvider;
50  import ca.uhn.fhir.rest.api.server.RequestDetails;
51  import ca.uhn.fhir.rest.param.*;
52  import ca.uhn.fhir.rest.server.exceptions.*;
53  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
54  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
55  import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
56  import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
57  import ca.uhn.fhir.util.*;
58  import com.google.common.annotations.VisibleForTesting;
59  import com.google.common.base.Charsets;
60  import com.google.common.collect.ArrayListMultimap;
61  import com.google.common.collect.Sets;
62  import com.google.common.hash.HashFunction;
63  import com.google.common.hash.Hashing;
64  import org.apache.commons.lang3.NotImplementedException;
65  import org.apache.commons.lang3.StringUtils;
66  import org.apache.commons.lang3.Validate;
67  import org.apache.http.NameValuePair;
68  import org.apache.http.client.utils.URLEncodedUtils;
69  import org.hibernate.Session;
70  import org.hibernate.internal.SessionImpl;
71  import org.hl7.fhir.instance.model.api.*;
72  import org.hl7.fhir.r4.model.BaseResource;
73  import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
74  import org.hl7.fhir.r4.model.CanonicalType;
75  import org.hl7.fhir.r4.model.Reference;
76  import org.springframework.beans.BeansException;
77  import org.springframework.beans.factory.annotation.Autowired;
78  import org.springframework.context.ApplicationContext;
79  import org.springframework.context.ApplicationContextAware;
80  import org.springframework.data.domain.PageRequest;
81  import org.springframework.data.domain.Pageable;
82  import org.springframework.data.domain.Slice;
83  import org.springframework.data.domain.SliceImpl;
84  import org.springframework.stereotype.Repository;
85  import org.springframework.transaction.PlatformTransactionManager;
86  import org.springframework.transaction.support.TransactionTemplate;
87  
88  import javax.persistence.*;
89  import javax.persistence.criteria.CriteriaBuilder;
90  import javax.persistence.criteria.CriteriaQuery;
91  import javax.persistence.criteria.Predicate;
92  import javax.persistence.criteria.Root;
93  import javax.xml.stream.events.Characters;
94  import javax.xml.stream.events.XMLEvent;
95  import java.io.CharArrayWriter;
96  import java.io.UnsupportedEncodingException;
97  import java.text.Normalizer;
98  import java.util.*;
99  import java.util.Map.Entry;
100 import java.util.concurrent.atomic.AtomicInteger;
101 import java.util.stream.Collectors;
102 
103 import static org.apache.commons.lang3.StringUtils.*;
104 
105 @SuppressWarnings("WeakerAccess")
106 @Repository
107 public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ApplicationContextAware {
108 
109 	public static final long INDEX_STATUS_INDEXED = 1L;
110 	public static final long INDEX_STATUS_INDEXING_FAILED = 2L;
111 	public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile";
112 	public static final String OO_SEVERITY_ERROR = "error";
113 	public static final String OO_SEVERITY_INFO = "information";
114 	public static final String OO_SEVERITY_WARN = "warning";
115 	public static final String UCUM_NS = "http://unitsofmeasure.org";
116 	static final Set<String> EXCLUDE_ELEMENTS_IN_ENCODED;
117 	/**
118 	 * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
119 	 */
120 	static final Map<String, Class<? extends IQueryParameterAnd<?>>> RESOURCE_META_AND_PARAMS;
121 	/**
122 	 * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
123 	 */
124 	static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS;
125 	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class);
126 	private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<FhirVersionEnum, FhirContext>();
127 	private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest";
128 	private static boolean ourValidationDisabledForUnitTest;
129 	private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
130 
131 	static {
132 		Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<String, Class<? extends IQueryParameterType>>();
133 		Map<String, Class<? extends IQueryParameterAnd<?>>> resourceMetaAndParams = new HashMap<String, Class<? extends IQueryParameterAnd<?>>>();
134 		resourceMetaParams.put(BaseResource.SP_RES_ID, StringParam.class);
135 		resourceMetaAndParams.put(BaseResource.SP_RES_ID, StringAndListParam.class);
136 		resourceMetaParams.put(BaseResource.SP_RES_LANGUAGE, StringParam.class);
137 		resourceMetaAndParams.put(BaseResource.SP_RES_LANGUAGE, StringAndListParam.class);
138 		resourceMetaParams.put(Constants.PARAM_TAG, TokenParam.class);
139 		resourceMetaAndParams.put(Constants.PARAM_TAG, TokenAndListParam.class);
140 		resourceMetaParams.put(Constants.PARAM_PROFILE, UriParam.class);
141 		resourceMetaAndParams.put(Constants.PARAM_PROFILE, UriAndListParam.class);
142 		resourceMetaParams.put(Constants.PARAM_SECURITY, TokenParam.class);
143 		resourceMetaAndParams.put(Constants.PARAM_SECURITY, TokenAndListParam.class);
144 		RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams);
145 		RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
146 
147 		HashSet<String> excludeElementsInEncoded = new HashSet<String>();
148 		excludeElementsInEncoded.add("id");
149 		excludeElementsInEncoded.add("*.meta");
150 		EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded);
151 	}
152 
153 	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
154 	protected EntityManager myEntityManager;
155 	@Autowired
156 	protected IForcedIdDao myForcedIdDao;
157 	@Autowired(required = false)
158 	protected IFulltextSearchSvc myFulltextSearchSvc;
159 	@Autowired()
160 	protected IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
161 	@Autowired()
162 	protected IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao;
163 	@Autowired()
164 	protected IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao;
165 	@Autowired()
166 	protected IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao;
167 	@Autowired()
168 	protected IResourceIndexedSearchParamQuantityDao myResourceIndexedSearchParamQuantityDao;
169 	@Autowired()
170 	protected IResourceIndexedSearchParamCoordsDao myResourceIndexedSearchParamCoordsDao;
171 	@Autowired()
172 	protected IResourceIndexedSearchParamNumberDao myResourceIndexedSearchParamNumberDao;
173 	@Autowired
174 	protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
175 	@Autowired
176 	protected ISearchParamRegistry mySerarchParamRegistry;
177 	@Autowired()
178 	protected IHapiTerminologySvc myTerminologySvc;
179 	@Autowired
180 	protected IResourceHistoryTableDao myResourceHistoryTableDao;
181 	@Autowired
182 	protected IResourceHistoryTagDao myResourceHistoryTagDao;
183 	@Autowired
184 	protected IResourceTableDao myResourceTableDao;
185 	@Autowired
186 	protected IResourceTagDao myResourceTagDao;
187 	@Autowired(required = true)
188 	private DaoConfig myConfig;
189 	private FhirContext myContext;
190 	@Autowired
191 	private PlatformTransactionManager myPlatformTransactionManager;
192 	@Autowired
193 	private ISearchDao mySearchDao;
194 	@Autowired
195 	private ISearchParamExtractor mySearchParamExtractor;
196 	@Autowired
197 	private ISearchParamPresenceSvc mySearchParamPresenceSvc;
198 	@Autowired
199 	private ISearchParamRegistry mySearchParamRegistry;
200 	@Autowired
201 	private ISearchResultDao mySearchResultDao;
202 	@Autowired
203 	private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
204 	private ApplicationContext myApplicationContext;
205 	private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> myResourceTypeToDao;
206 
207 	protected void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
208 		if (theRequestDetails != null) {
209 			theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
210 		}
211 	}
212 
213 	protected void createForcedIdIfNeeded(ResourceTable theEntity, IIdType theId) {
214 		if (theId.isEmpty() == false && theId.hasIdPart()) {
215 			if (isValidPid(theId)) {
216 				return;
217 			}
218 
219 			ForcedId fid = new ForcedId();
220 			fid.setResourceType(theEntity.getResourceType());
221 			fid.setForcedId(theId.getIdPart());
222 			fid.setResource(theEntity);
223 			theEntity.setForcedId(fid);
224 		}
225 	}
226 
227 	protected ExpungeOutcome doExpunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions) {
228 
229 		if (!getConfig().isExpungeEnabled()) {
230 			throw new MethodNotAllowedException("$expunge is not enabled on this server");
231 		}
232 
233 		AtomicInteger remainingCount = new AtomicInteger(theExpungeOptions.getLimit());
234 
235 		if (theResourceName == null && theResourceId == null && theVersion == null) {
236 			if (theExpungeOptions.isExpungeEverything()) {
237 				doExpungeEverything();
238 			}
239 		}
240 
241 		if (theExpungeOptions.isExpungeDeletedResources() && theVersion == null) {
242 
243 			/*
244 			 * Delete historical versions of deleted resources
245 			 */
246 			Pageable page = new PageRequest(0, remainingCount.get());
247 			Slice<Long> resourceIds;
248 			if (theResourceId != null) {
249 				resourceIds = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceId, theResourceName);
250 			} else {
251 				if (theResourceName != null) {
252 					resourceIds = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceName);
253 				} else {
254 					resourceIds = myResourceTableDao.findIdsOfDeletedResources(page);
255 				}
256 			}
257 			for (Long next : resourceIds) {
258 				expungeHistoricalVersionsOfId(next, remainingCount);
259 				if (remainingCount.get() <= 0) {
260 					return toExpungeOutcome(theExpungeOptions, remainingCount);
261 				}
262 			}
263 
264 			/*
265 			 * Delete current versions of deleted resources
266 			 */
267 			for (Long next : resourceIds) {
268 				expungeCurrentVersionOfResource(next);
269 				if (remainingCount.get() <= 0) {
270 					return toExpungeOutcome(theExpungeOptions, remainingCount);
271 				}
272 			}
273 
274 		}
275 
276 		if (theExpungeOptions.isExpungeOldVersions()) {
277 
278 			/*
279 			 * Delete historical versions of non-deleted resources
280 			 */
281 			Pageable page = new PageRequest(0, remainingCount.get());
282 			Slice<Long> historicalIds;
283 			if (theResourceId != null && theVersion != null) {
284 				historicalIds = toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion));
285 			} else {
286 				if (theResourceName != null) {
287 					historicalIds = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName);
288 				} else {
289 					historicalIds = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page);
290 				}
291 			}
292 			for (Long next : historicalIds) {
293 				expungeHistoricalVersion(next);
294 				if (remainingCount.decrementAndGet() <= 0) {
295 					return toExpungeOutcome(theExpungeOptions, remainingCount);
296 				}
297 			}
298 
299 		}
300 		return toExpungeOutcome(theExpungeOptions, remainingCount);
301 	}
302 
303 	private void doExpungeEverything() {
304 
305 		ourLog.info("** BEGINNING GLOBAL $expunge **");
306 		TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
307 		txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
308 		txTemplate.execute(t -> {
309 			doExpungeEverythingQuery("UPDATE " + ResourceHistoryTable.class.getSimpleName() + " d SET d.myForcedId = null");
310 			doExpungeEverythingQuery("UPDATE " + ResourceTable.class.getSimpleName() + " d SET d.myForcedId = null");
311 			doExpungeEverythingQuery("UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null");
312 			return null;
313 		});
314 		txTemplate.execute(t -> {
315 			doExpungeEverythingQuery("DELETE from " + SearchParamPresent.class.getSimpleName() + " d");
316 			doExpungeEverythingQuery("DELETE from " + SearchParam.class.getSimpleName() + " d");
317 			doExpungeEverythingQuery("DELETE from " + ForcedId.class.getSimpleName() + " d");
318 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamDate.class.getSimpleName() + " d");
319 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamNumber.class.getSimpleName() + " d");
320 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamQuantity.class.getSimpleName() + " d");
321 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamString.class.getSimpleName() + " d");
322 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamToken.class.getSimpleName() + " d");
323 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamUri.class.getSimpleName() + " d");
324 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamCoords.class.getSimpleName() + " d");
325 			doExpungeEverythingQuery("DELETE from " + ResourceIndexedCompositeStringUnique.class.getSimpleName() + " d");
326 			doExpungeEverythingQuery("DELETE from " + ResourceLink.class.getSimpleName() + " d");
327 			doExpungeEverythingQuery("DELETE from " + SearchResult.class.getSimpleName() + " d");
328 			doExpungeEverythingQuery("DELETE from " + SearchInclude.class.getSimpleName() + " d");
329 			doExpungeEverythingQuery("DELETE from " + TermConceptParentChildLink.class.getSimpleName() + " d");
330 			return null;
331 		});
332 		txTemplate.execute(t -> {
333 			doExpungeEverythingQuery("DELETE from " + TermConceptMapGroupElementTarget.class.getSimpleName() + " d");
334 			doExpungeEverythingQuery("DELETE from " + TermConceptMapGroupElement.class.getSimpleName() + " d");
335 			doExpungeEverythingQuery("DELETE from " + TermConceptMapGroup.class.getSimpleName() + " d");
336 			doExpungeEverythingQuery("DELETE from " + TermConceptMap.class.getSimpleName() + " d");
337 			return null;
338 		});
339 		txTemplate.execute(t -> {
340 			doExpungeEverythingQuery("DELETE from " + TermConceptProperty.class.getSimpleName() + " d");
341 			doExpungeEverythingQuery("DELETE from " + TermConceptDesignation.class.getSimpleName() + " d");
342 			doExpungeEverythingQuery("DELETE from " + TermConcept.class.getSimpleName() + " d");
343 			for (TermCodeSystem next : myEntityManager.createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class).getResultList()) {
344 				next.setCurrentVersion(null);
345 				myEntityManager.merge(next);
346 			}
347 			return null;
348 		});
349 		txTemplate.execute(t -> {
350 			doExpungeEverythingQuery("DELETE from " + TermCodeSystemVersion.class.getSimpleName() + " d");
351 			doExpungeEverythingQuery("DELETE from " + TermCodeSystem.class.getSimpleName() + " d");
352 			return null;
353 		});
354 		txTemplate.execute(t -> {
355 			doExpungeEverythingQuery("DELETE from " + SubscriptionTable.class.getSimpleName() + " d");
356 			doExpungeEverythingQuery("DELETE from " + ResourceHistoryTag.class.getSimpleName() + " d");
357 			doExpungeEverythingQuery("DELETE from " + ResourceTag.class.getSimpleName() + " d");
358 			doExpungeEverythingQuery("DELETE from " + TagDefinition.class.getSimpleName() + " d");
359 			doExpungeEverythingQuery("DELETE from " + ResourceHistoryTable.class.getSimpleName() + " d");
360 			doExpungeEverythingQuery("DELETE from " + ResourceTable.class.getSimpleName() + " d");
361 			doExpungeEverythingQuery("DELETE from " + org.hibernate.search.jpa.Search.class.getSimpleName() + " d");
362 			return null;
363 		});
364 
365 		ourLog.info("** COMPLETED GLOBAL $expunge **");
366 	}
367 
368 	private void doExpungeEverythingQuery(String theQuery) {
369 		StopWatch sw = new StopWatch();
370 		int outcome = myEntityManager.createQuery(theQuery).executeUpdate();
371 		ourLog.info("Query affected {} rows in {}: {}", outcome, sw.toString(), theQuery);
372 	}
373 
374 	private void expungeCurrentVersionOfResource(Long theResourceId) {
375 		ResourceTable resource = myResourceTableDao.findOne(theResourceId);
376 
377 		ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion());
378 		expungeHistoricalVersion(currentVersion.getId());
379 
380 		ourLog.info("Deleting current version of resource {}", resource.getIdDt().getValue());
381 
382 		myResourceIndexedSearchParamUriDao.delete(resource.getParamsUri());
383 		myResourceIndexedSearchParamCoordsDao.delete(resource.getParamsCoords());
384 		myResourceIndexedSearchParamDateDao.delete(resource.getParamsDate());
385 		myResourceIndexedSearchParamNumberDao.delete(resource.getParamsNumber());
386 		myResourceIndexedSearchParamQuantityDao.delete(resource.getParamsQuantity());
387 		myResourceIndexedSearchParamStringDao.delete(resource.getParamsString());
388 		myResourceIndexedSearchParamTokenDao.delete(resource.getParamsToken());
389 
390 		myResourceTagDao.delete(resource.getTags());
391 		resource.getTags().clear();
392 
393 		if (resource.getForcedId() != null) {
394 			ForcedId forcedId = resource.getForcedId();
395 			resource.setForcedId(null);
396 			myResourceTableDao.saveAndFlush(resource);
397 			myForcedIdDao.delete(forcedId);
398 		}
399 
400 		myResourceTableDao.delete(resource);
401 
402 	}
403 
404 	protected void expungeHistoricalVersion(Long theNextVersionId) {
405 		ResourceHistoryTable version = myResourceHistoryTableDao.findOne(theNextVersionId);
406 		ourLog.info("Deleting resource version {}", version.getIdDt().getValue());
407 
408 		myResourceHistoryTagDao.delete(version.getTags());
409 		myResourceHistoryTableDao.delete(version);
410 	}
411 
412 	protected void expungeHistoricalVersionsOfId(Long theResourceId, AtomicInteger theRemainingCount) {
413 		ResourceTable resource = myResourceTableDao.findOne(theResourceId);
414 
415 		Pageable page = new PageRequest(0, theRemainingCount.get());
416 
417 		Slice<Long> versionIds = myResourceHistoryTableDao.findForResourceId(page, resource.getId(), resource.getVersion());
418 		for (Long nextVersionId : versionIds) {
419 			expungeHistoricalVersion(nextVersionId);
420 			if (theRemainingCount.decrementAndGet() <= 0) {
421 				return;
422 			}
423 		}
424 	}
425 
426 	private Set<ResourceIndexedCompositeStringUnique> extractCompositeStringUniques(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> theStringParams, Set<ResourceIndexedSearchParamToken> theTokenParams, Set<ResourceIndexedSearchParamNumber> theNumberParams, Set<ResourceIndexedSearchParamQuantity> theQuantityParams, Set<ResourceIndexedSearchParamDate> theDateParams, Set<ResourceIndexedSearchParamUri> theUriParams, Set<ResourceLink> theLinks) {
427 		Set<ResourceIndexedCompositeStringUnique> compositeStringUniques;
428 		compositeStringUniques = new HashSet<>();
429 		List<JpaRuntimeSearchParam> uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(theEntity.getResourceType());
430 		for (JpaRuntimeSearchParam next : uniqueSearchParams) {
431 
432 			List<List<String>> partsChoices = new ArrayList<>();
433 
434 			for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) {
435 				Set<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
436 				Set<ResourceLink> linksForCompositePart = null;
437 				Set<String> linksForCompositePartWantPaths = null;
438 				switch (nextCompositeOf.getParamType()) {
439 					case NUMBER:
440 						paramsListForCompositePart = theNumberParams;
441 						break;
442 					case DATE:
443 						paramsListForCompositePart = theDateParams;
444 						break;
445 					case STRING:
446 						paramsListForCompositePart = theStringParams;
447 						break;
448 					case TOKEN:
449 						paramsListForCompositePart = theTokenParams;
450 						break;
451 					case REFERENCE:
452 						linksForCompositePart = theLinks;
453 						linksForCompositePartWantPaths = new HashSet<>();
454 						linksForCompositePartWantPaths.addAll(nextCompositeOf.getPathsSplit());
455 						break;
456 					case QUANTITY:
457 						paramsListForCompositePart = theQuantityParams;
458 						break;
459 					case URI:
460 						paramsListForCompositePart = theUriParams;
461 						break;
462 					case COMPOSITE:
463 					case HAS:
464 						break;
465 				}
466 
467 				ArrayList<String> nextChoicesList = new ArrayList<>();
468 				partsChoices.add(nextChoicesList);
469 
470 				String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
471 				if (paramsListForCompositePart != null) {
472 					for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
473 						if (nextParam.getParamName().equals(nextCompositeOf.getName())) {
474 							IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
475 							String value = nextParamAsClientParam.getValueAsQueryToken(getContext());
476 							if (isNotBlank(value)) {
477 								value = UrlUtil.escapeUrlParam(value);
478 								nextChoicesList.add(key + "=" + value);
479 							}
480 						}
481 					}
482 				}
483 				if (linksForCompositePart != null) {
484 					for (ResourceLink nextLink : linksForCompositePart) {
485 						if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
486 							String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
487 							if (isNotBlank(value)) {
488 								value = UrlUtil.escapeUrlParam(value);
489 								nextChoicesList.add(key + "=" + value);
490 							}
491 						}
492 					}
493 				}
494 			}
495 
496 			Set<String> queryStringsToPopulate = extractCompositeStringUniquesValueChains(theEntity.getResourceType(), partsChoices);
497 
498 			for (String nextQueryString : queryStringsToPopulate) {
499 				if (isNotBlank(nextQueryString)) {
500 					compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
501 				}
502 			}
503 		}
504 
505 		return compositeStringUniques;
506 	}
507 
508 	/**
509 	 * @return Returns a set containing all of the parameter names that
510 	 * were found to have a value
511 	 */
512 	@SuppressWarnings("unchecked")
513 	protected Set<String> extractResourceLinks(ResourceTable theEntity, IBaseResource theResource, Set<ResourceLink> theLinks, Date theUpdateTime) {
514 		HashSet<String> retVal = new HashSet<>();
515 		String resourceType = theEntity.getResourceType();
516 
517 		/*
518 		 * For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing..
519 		 */
520 		if (theResource instanceof IBaseBundle) {
521 			return Collections.emptySet();
522 		}
523 
524 		Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(toResourceName(theResource.getClass()));
525 		for (RuntimeSearchParam nextSpDef : searchParams.values()) {
526 
527 			if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
528 				continue;
529 			}
530 
531 			String nextPathsUnsplit = nextSpDef.getPath();
532 			if (isBlank(nextPathsUnsplit)) {
533 				continue;
534 			}
535 
536 			boolean multiType = false;
537 			if (nextPathsUnsplit.endsWith("[x]")) {
538 				multiType = true;
539 			}
540 
541 			List<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef);
542 			for (PathAndRef nextPathAndRef : refs) {
543 				Object nextObject = nextPathAndRef.getRef();
544 
545 				/*
546 				 * A search parameter on an extension field that contains
547 				 * references should index those references
548 				 */
549 				if (nextObject instanceof IBaseExtension<?, ?>) {
550 					nextObject = ((IBaseExtension<?, ?>) nextObject).getValue();
551 				}
552 
553 				if (nextObject instanceof CanonicalType) {
554 					nextObject = new Reference(((CanonicalType) nextObject).getValueAsString());
555 				}
556 
557 				IIdType nextId;
558 				if (nextObject instanceof IBaseReference) {
559 					IBaseReference nextValue = (IBaseReference) nextObject;
560 					if (nextValue.isEmpty()) {
561 						continue;
562 					}
563 					nextId = nextValue.getReferenceElement();
564 
565 					/*
566 					 * This can only really happen if the DAO is being called
567 					 * programatically with a Bundle (not through the FHIR REST API)
568 					 * but Smile does this
569 					 */
570 					if (nextId.isEmpty() && nextValue.getResource() != null) {
571 						nextId = nextValue.getResource().getIdElement();
572 					}
573 
574 					if (nextId.isEmpty() || nextId.getValue().startsWith("#")) {
575 						// This is a blank or contained resource reference
576 						continue;
577 					}
578 				} else if (nextObject instanceof IBaseResource) {
579 					nextId = ((IBaseResource) nextObject).getIdElement();
580 					if (nextId == null || nextId.hasIdPart() == false) {
581 						continue;
582 					}
583 				} else if (myContext.getElementDefinition((Class<? extends IBase>) nextObject.getClass()).getName().equals("uri")) {
584 					continue;
585 				} else if (resourceType.equals("Consent") && nextPathAndRef.getPath().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 						if (nextSpDef.getName().equals("sourceuri")) {
591 							continue; // TODO: disable this eventually - ConceptMap:sourceuri is of type reference but points to a URI
592 						}
593 						throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass());
594 					} else {
595 						continue;
596 					}
597 				}
598 
599 				retVal.add(nextSpDef.getName());
600 
601 				if (isLogicalReference(nextId)) {
602 					ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
603 					if (theLinks.add(resourceLink)) {
604 						ourLog.debug("Indexing remote resource reference URL: {}", nextId);
605 					}
606 					continue;
607 				}
608 
609 				String baseUrl = nextId.getBaseUrl();
610 				String typeString = nextId.getResourceType();
611 				if (isBlank(typeString)) {
612 					throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue());
613 				}
614 				RuntimeResourceDefinition resourceDefinition;
615 				try {
616 					resourceDefinition = getContext().getResourceDefinition(typeString);
617 				} catch (DataFormatException e) {
618 					throw new InvalidRequestException(
619 						"Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue());
620 				}
621 
622 				if (isNotBlank(baseUrl)) {
623 					if (!getConfig().getTreatBaseUrlsAsLocal().contains(baseUrl) && !getConfig().isAllowExternalReferences()) {
624 						String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue());
625 						throw new InvalidRequestException(msg);
626 					} else {
627 						ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
628 						if (theLinks.add(resourceLink)) {
629 							ourLog.debug("Indexing remote resource reference URL: {}", nextId);
630 						}
631 						continue;
632 					}
633 				}
634 
635 				Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
636 				String id = nextId.getIdPart();
637 				if (StringUtils.isBlank(id)) {
638 					throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue());
639 				}
640 
641 				IFhirResourceDao<?> dao = getDao(type);
642 				if (dao == null) {
643 					StringBuilder b = new StringBuilder();
644 					b.append("This server (version ");
645 					b.append(myContext.getVersion().getVersion());
646 					b.append(") is not able to handle resources of type[");
647 					b.append(nextId.getResourceType());
648 					b.append("] - Valid resource types for this server: ");
649 					b.append(myResourceTypeToDao.keySet().toString());
650 
651 					throw new InvalidRequestException(b.toString());
652 				}
653 				Long valueOf;
654 				try {
655 					valueOf = translateForcedIdToPid(typeString, id);
656 				} catch (ResourceNotFoundException e) {
657 					if (myConfig.isEnforceReferentialIntegrityOnWrite() == false) {
658 						continue;
659 					}
660 					RuntimeResourceDefinition missingResourceDef = getContext().getResourceDefinition(type);
661 					String resName = missingResourceDef.getName();
662 
663 					if (getConfig().isAutoCreatePlaceholderReferenceTargets()) {
664 						IBaseResource newResource = missingResourceDef.newInstance();
665 						newResource.setId(resName + "/" + id);
666 						IFhirResourceDao<IBaseResource> placeholderResourceDao = (IFhirResourceDao<IBaseResource>) getDao(newResource.getClass());
667 						ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue());
668 						valueOf = placeholderResourceDao.update(newResource).getEntity().getId();
669 					} else {
670 						throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
671 					}
672 				}
673 				ResourceTable target = myEntityManager.find(ResourceTable.class, valueOf);
674 				RuntimeResourceDefinition targetResourceDef = getContext().getResourceDefinition(type);
675 				if (target == null) {
676 					String resName = targetResourceDef.getName();
677 					throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
678 				}
679 
680 				if (!typeString.equals(target.getResourceType())) {
681 					throw new UnprocessableEntityException(
682 						"Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType());
683 				}
684 
685 				if (target.getDeleted() != null) {
686 					String resName = targetResourceDef.getName();
687 					throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit);
688 				}
689 
690 				if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) {
691 					continue;
692 				}
693 
694 				ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, target, theUpdateTime);
695 				theLinks.add(resourceLink);
696 			}
697 
698 		}
699 
700 		theEntity.setHasLinks(theLinks.size() > 0);
701 
702 		return retVal;
703 	}
704 
705 
706 	@SuppressWarnings("unchecked")
707 	public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
708 		Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> resourceTypeToDao = getDaos();
709 		IFhirResourceDao<R> dao = (IFhirResourceDao<R>) resourceTypeToDao.get(theType);
710 		return dao;
711 	}
712 
713 	protected IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
714 		IFhirResourceDao<? extends IBaseResource> retVal = getDao(theClass);
715 		if (retVal == null) {
716 			List<String> supportedResourceTypes = getDaos()
717 				.keySet()
718 				.stream()
719 				.map(t->myContext.getResourceDefinition(t).getName())
720 				.sorted()
721 				.collect(Collectors.toList());
722 			throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + getContext().getResourceDefinition(theClass).getName() + " - Can handle: " + supportedResourceTypes);
723 		}
724 		return retVal;
725 	}
726 
727 
728 	private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> getDaos() {
729 		if (myResourceTypeToDao == null) {
730 			Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> theResourceTypeToDao = new HashMap<>();
731 			Map<String, IFhirResourceDao> daos = myApplicationContext.getBeansOfType(IFhirResourceDao.class, false, false);
732 			for (IFhirResourceDao<?> next : daos.values()) {
733 				theResourceTypeToDao.put(next.getResourceType(), next);
734 			}
735 
736 			if (this instanceof IFhirResourceDao<?>) {
737 				IFhirResourceDao<?> thiz = (IFhirResourceDao<?>) this;
738 				theResourceTypeToDao.put(thiz.getResourceType(), thiz);
739 			}
740 
741 			myResourceTypeToDao = theResourceTypeToDao;
742 		}
743 
744 		return Collections.unmodifiableMap(myResourceTypeToDao);
745 	}
746 
747 
748 	protected Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
749 		return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource);
750 	}
751 
752 	protected Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
753 		return mySearchParamExtractor.extractSearchParamDates(theEntity, theResource);
754 	}
755 
756 	protected Set<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
757 		return mySearchParamExtractor.extractSearchParamNumber(theEntity, theResource);
758 	}
759 
760 	protected Set<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) {
761 		return mySearchParamExtractor.extractSearchParamQuantity(theEntity, theResource);
762 	}
763 
764 	protected Set<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) {
765 		return mySearchParamExtractor.extractSearchParamStrings(theEntity, theResource);
766 	}
767 
768 	protected Set<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) {
769 		return mySearchParamExtractor.extractSearchParamTokens(theEntity, theResource);
770 	}
771 
772 	protected Set<ResourceIndexedSearchParamUri> extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) {
773 		return mySearchParamExtractor.extractSearchParamUri(theEntity, theResource);
774 	}
775 
776 	private void extractTagsHapi(IResource theResource, ResourceTable theEntity, Set<ResourceTag> allDefs) {
777 		TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource);
778 		if (tagList != null) {
779 			for (Tag next : tagList) {
780 				TagDefinition def = getTagOrNull(TagTypeEnum.TAG, next.getScheme(), next.getTerm(), next.getLabel());
781 				if (def != null) {
782 					ResourceTag tag = theEntity.addTag(def);
783 					allDefs.add(tag);
784 					theEntity.setHasTags(true);
785 				}
786 			}
787 		}
788 
789 		List<BaseCodingDt> securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource);
790 		if (securityLabels != null) {
791 			for (BaseCodingDt next : securityLabels) {
792 				TagDefinition def = getTagOrNull(TagTypeEnum.SECURITY_LABEL, next.getSystemElement().getValue(), next.getCodeElement().getValue(), next.getDisplayElement().getValue());
793 				if (def != null) {
794 					ResourceTag tag = theEntity.addTag(def);
795 					allDefs.add(tag);
796 					theEntity.setHasTags(true);
797 				}
798 			}
799 		}
800 
801 		List<IdDt> profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource);
802 		if (profiles != null) {
803 			for (IIdType next : profiles) {
804 				TagDefinition def = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
805 				if (def != null) {
806 					ResourceTag tag = theEntity.addTag(def);
807 					allDefs.add(tag);
808 					theEntity.setHasTags(true);
809 				}
810 			}
811 		}
812 	}
813 
814 	private void extractTagsRi(IAnyResource theResource, ResourceTable theEntity, Set<ResourceTag> theAllTags) {
815 		List<? extends IBaseCoding> tagList = theResource.getMeta().getTag();
816 		if (tagList != null) {
817 			for (IBaseCoding next : tagList) {
818 				TagDefinition def = getTagOrNull(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay());
819 				if (def != null) {
820 					ResourceTag tag = theEntity.addTag(def);
821 					theAllTags.add(tag);
822 					theEntity.setHasTags(true);
823 				}
824 			}
825 		}
826 
827 		List<? extends IBaseCoding> securityLabels = theResource.getMeta().getSecurity();
828 		if (securityLabels != null) {
829 			for (IBaseCoding next : securityLabels) {
830 				TagDefinition def = getTagOrNull(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay());
831 				if (def != null) {
832 					ResourceTag tag = theEntity.addTag(def);
833 					theAllTags.add(tag);
834 					theEntity.setHasTags(true);
835 				}
836 			}
837 		}
838 
839 		List<? extends IPrimitiveType<String>> profiles = theResource.getMeta().getProfile();
840 		if (profiles != null) {
841 			for (IPrimitiveType<String> next : profiles) {
842 				TagDefinition def = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
843 				if (def != null) {
844 					ResourceTag tag = theEntity.addTag(def);
845 					theAllTags.add(tag);
846 					theEntity.setHasTags(true);
847 				}
848 			}
849 		}
850 	}
851 
852 	private void findMatchingTagIds(String theResourceName, IIdType theResourceId, Set<Long> tagIds, Class<? extends BaseTag> entityClass) {
853 		{
854 			CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
855 			CriteriaQuery<Tuple> cq = builder.createTupleQuery();
856 			Root<? extends BaseTag> from = cq.from(entityClass);
857 			cq.multiselect(from.get("myTagId").as(Long.class)).distinct(true);
858 
859 			if (theResourceName != null) {
860 				Predicate typePredicate = builder.equal(from.get("myResourceType"), theResourceName);
861 				if (theResourceId != null) {
862 					cq.where(typePredicate, builder.equal(from.get("myResourceId"), translateForcedIdToPid(theResourceName, theResourceId.getIdPart())));
863 				} else {
864 					cq.where(typePredicate);
865 				}
866 			}
867 
868 			TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
869 			for (Tuple next : query.getResultList()) {
870 				tagIds.add(next.get(0, Long.class));
871 			}
872 		}
873 	}
874 
875 	@SuppressWarnings("unchecked")
876 	private <RT extends BaseResourceIndexedSearchParam> void findMissingSearchParams(ResourceTable theEntity, Set<Entry<String, RuntimeSearchParam>> activeSearchParams, RestSearchParameterTypeEnum type,
877 																												Set<RT> paramCollection) {
878 		for (Entry<String, RuntimeSearchParam> nextEntry : activeSearchParams) {
879 			String nextParamName = nextEntry.getKey();
880 			if (nextEntry.getValue().getParamType() == type) {
881 				boolean haveParam = false;
882 				for (BaseResourceIndexedSearchParam nextParam : paramCollection) {
883 					if (nextParam.getParamName().equals(nextParamName)) {
884 						haveParam = true;
885 						break;
886 					}
887 				}
888 
889 				if (!haveParam) {
890 					BaseResourceIndexedSearchParam param;
891 					switch (type) {
892 						case DATE:
893 							param = new ResourceIndexedSearchParamDate();
894 							break;
895 						case NUMBER:
896 							param = new ResourceIndexedSearchParamNumber();
897 							break;
898 						case QUANTITY:
899 							param = new ResourceIndexedSearchParamQuantity();
900 							break;
901 						case STRING:
902 							param = new ResourceIndexedSearchParamString();
903 							break;
904 						case TOKEN:
905 							param = new ResourceIndexedSearchParamToken();
906 							break;
907 						case URI:
908 							param = new ResourceIndexedSearchParamUri();
909 							break;
910 						case COMPOSITE:
911 						case HAS:
912 						case REFERENCE:
913 						default:
914 							continue;
915 					}
916 					param.setResource(theEntity);
917 					param.setMissing(true);
918 					param.setParamName(nextParamName);
919 					paramCollection.add((RT) param);
920 				}
921 			}
922 		}
923 	}
924 
925 	protected void flushJpaSession() {
926 		SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class);
927 		int insertionCount = session.getActionQueue().numberOfInsertions();
928 		int updateCount = session.getActionQueue().numberOfUpdates();
929 
930 		StopWatch sw = new StopWatch();
931 		myEntityManager.flush();
932 		ourLog.debug("Session flush took {}ms for {} inserts and {} updates", sw.getMillis(), insertionCount, updateCount);
933 	}
934 
935 	private Set<ResourceTag> getAllTagDefinitions(ResourceTable theEntity) {
936 		HashSet<ResourceTag> retVal = Sets.newHashSet();
937 		if (theEntity.isHasTags()) {
938 			for (ResourceTag next : theEntity.getTags()) {
939 				retVal.add(next);
940 			}
941 		}
942 		return retVal;
943 	}
944 
945 	protected DaoConfig getConfig() {
946 		return myConfig;
947 	}
948 
949 	@Override
950 	public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
951 		myApplicationContext = theApplicationContext;
952 	}
953 
954 	public void setConfig(DaoConfig theConfig) {
955 		myConfig = theConfig;
956 	}
957 
958 	@Override
959 	public FhirContext getContext() {
960 		return myContext;
961 	}
962 
963 	@Autowired
964 	public void setContext(FhirContext theContext) {
965 		myContext = theContext;
966 	}
967 
968 	public FhirContext getContext(FhirVersionEnum theVersion) {
969 		Validate.notNull(theVersion, "theVersion must not be null");
970 		synchronized (ourRetrievalContexts) {
971 			FhirContext retVal = ourRetrievalContexts.get(theVersion);
972 			if (retVal == null) {
973 				retVal = new FhirContext(theVersion);
974 				ourRetrievalContexts.put(theVersion, retVal);
975 			}
976 			return retVal;
977 		}
978 	}
979 
980 	public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao() {
981 		return myResourceIndexedCompositeStringUniqueDao;
982 	}
983 
984 	@Override
985 	public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
986 		Map<String, RuntimeSearchParam> params = mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName());
987 		return params.get(theParamName);
988 	}
989 
990 	@Override
991 	public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
992 		return mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName()).values();
993 	}
994 
995 	protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
996 		if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) {
997 			return null;
998 		}
999 
1000 		CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
1001 		CriteriaQuery<TagDefinition> cq = builder.createQuery(TagDefinition.class);
1002 		Root<TagDefinition> from = cq.from(TagDefinition.class);
1003 
1004 		if (isNotBlank(theScheme)) {
1005 			cq.where(
1006 				builder.and(
1007 					builder.equal(from.get("myTagType"), theTagType),
1008 					builder.equal(from.get("mySystem"), theScheme),
1009 					builder.equal(from.get("myCode"), theTerm)));
1010 		} else {
1011 			cq.where(
1012 				builder.and(
1013 					builder.equal(from.get("myTagType"), theTagType),
1014 					builder.isNull(from.get("mySystem")),
1015 					builder.equal(from.get("myCode"), theTerm)));
1016 		}
1017 
1018 		TypedQuery<TagDefinition> q = myEntityManager.createQuery(cq);
1019 		try {
1020 			return q.getSingleResult();
1021 		} catch (NoResultException e) {
1022 			TagDefinition retVal = new TagDefinition(theTagType, theScheme, theTerm, theLabel);
1023 			myEntityManager.persist(retVal);
1024 			return retVal;
1025 		}
1026 	}
1027 
1028 	protected TagList getTags(Class<? extends IBaseResource> theResourceType, IIdType theResourceId) {
1029 		String resourceName = null;
1030 		if (theResourceType != null) {
1031 			resourceName = toResourceName(theResourceType);
1032 			if (theResourceId != null && theResourceId.hasVersionIdPart()) {
1033 				IFhirResourceDao<? extends IBaseResource> dao = getDao(theResourceType);
1034 				BaseHasResource entity = dao.readEntity(theResourceId);
1035 				TagList retVal = new TagList();
1036 				for (BaseTag next : entity.getTags()) {
1037 					retVal.add(next.getTag().toTag());
1038 				}
1039 				return retVal;
1040 			}
1041 		}
1042 
1043 		Set<Long> tagIds = new HashSet<>();
1044 		findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceTag.class);
1045 		findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceHistoryTag.class);
1046 		if (tagIds.isEmpty()) {
1047 			return new TagList();
1048 		}
1049 		{
1050 			CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
1051 			CriteriaQuery<TagDefinition> cq = builder.createQuery(TagDefinition.class);
1052 			Root<TagDefinition> from = cq.from(TagDefinition.class);
1053 			cq.where(from.get("myId").in(tagIds));
1054 			cq.orderBy(builder.asc(from.get("mySystem")), builder.asc(from.get("myCode")));
1055 			TypedQuery<TagDefinition> q = myEntityManager.createQuery(cq);
1056 			q.setMaxResults(getConfig().getHardTagListLimit());
1057 
1058 			TagList retVal = new TagList();
1059 			for (TagDefinition next : q.getResultList()) {
1060 				retVal.add(next.toTag());
1061 			}
1062 
1063 			return retVal;
1064 		}
1065 	}
1066 
1067 	protected IBundleProvider history(String theResourceName, Long theId, Date theSince, Date theUntil) {
1068 
1069 		String resourceName = defaultIfBlank(theResourceName, null);
1070 
1071 		Search search = new Search();
1072 		search.setCreated(new Date());
1073 		search.setSearchLastReturned(new Date());
1074 		search.setLastUpdated(theSince, theUntil);
1075 		search.setUuid(UUID.randomUUID().toString());
1076 		search.setResourceType(resourceName);
1077 		search.setResourceId(theId);
1078 		search.setSearchType(SearchTypeEnum.HISTORY);
1079 		search.setStatus(SearchStatusEnum.FINISHED);
1080 
1081 		if (theSince != null) {
1082 			if (resourceName == null) {
1083 				search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes(theSince));
1084 			} else if (theId == null) {
1085 				search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName, theSince));
1086 			} else {
1087 				search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId, theSince));
1088 			}
1089 		} else {
1090 			if (resourceName == null) {
1091 				search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes());
1092 			} else if (theId == null) {
1093 				search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName));
1094 			} else {
1095 				search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId));
1096 			}
1097 		}
1098 
1099 		search = mySearchDao.save(search);
1100 
1101 		return new PersistedJpaBundleProvider(search.getUuid(), this);
1102 	}
1103 
1104 	void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) {
1105 		String newVersion;
1106 		long newVersionLong;
1107 		if (theResourceId == null || theResourceId.getVersionIdPart() == null) {
1108 			newVersion = "1";
1109 			newVersionLong = 1;
1110 		} else {
1111 			newVersionLong = theResourceId.getVersionIdPartAsLong() + 1;
1112 			newVersion = Long.toString(newVersionLong);
1113 		}
1114 
1115 		IIdType newId = theResourceId.withVersion(newVersion);
1116 		theResource.getIdElement().setValue(newId.getValue());
1117 		theSavedEntity.setVersion(newVersionLong);
1118 	}
1119 
1120 	@Override
1121 	public void injectDependenciesIntoBundleProvider(PersistedJpaBundleProvider theProvider) {
1122 		theProvider.setContext(getContext());
1123 		theProvider.setEntityManager(myEntityManager);
1124 		theProvider.setPlatformTransactionManager(myPlatformTransactionManager);
1125 		theProvider.setSearchDao(mySearchDao);
1126 		theProvider.setSearchCoordinatorSvc(mySearchCoordinatorSvc);
1127 	}
1128 
1129 	protected boolean isLogicalReference(IIdType theId) {
1130 		Set<String> treatReferencesAsLogical = myConfig.getTreatReferencesAsLogical();
1131 		if (treatReferencesAsLogical != null) {
1132 			for (String nextLogicalRef : treatReferencesAsLogical) {
1133 				nextLogicalRef = trim(nextLogicalRef);
1134 				if (nextLogicalRef.charAt(nextLogicalRef.length() - 1) == '*') {
1135 					if (theId.getValue().startsWith(nextLogicalRef.substring(0, nextLogicalRef.length() - 1))) {
1136 						return true;
1137 					}
1138 				} else {
1139 					if (theId.getValue().equals(nextLogicalRef)) {
1140 						return true;
1141 					}
1142 				}
1143 			}
1144 
1145 		}
1146 		return false;
1147 	}
1148 
1149 	protected void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
1150 		if (theRequestDetails != null) {
1151 			theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE);
1152 		}
1153 	}
1154 
1155 	@Override
1156 	public SearchBuilder newSearchBuilder() {
1157 		SearchBuilder builder = new SearchBuilder(getContext(), myEntityManager, myFulltextSearchSvc, this, myResourceIndexedSearchParamUriDao,
1158 			myForcedIdDao,
1159 			myTerminologySvc, mySerarchParamRegistry);
1160 		return builder;
1161 	}
1162 
1163 	protected void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) {
1164 		if (theRequestDetails.getId() != null && theRequestDetails.getId().hasResourceType() && isNotBlank(theRequestDetails.getResourceType())) {
1165 			if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) {
1166 				throw new InternalErrorException(
1167 					"Inconsistent server state - Resource types don't match: " + theRequestDetails.getId().getResourceType() + " / " + theRequestDetails.getResourceType());
1168 			}
1169 		}
1170 
1171 		if (theRequestDetails.getUserData().get(PROCESSING_SUB_REQUEST) == Boolean.TRUE) {
1172 			theRequestDetails.notifyIncomingRequestPreHandled(theOperationType);
1173 		}
1174 		List<IServerInterceptor> interceptors = getConfig().getInterceptors();
1175 		for (IServerInterceptor next : interceptors) {
1176 			next.incomingRequestPreHandled(theOperationType, theRequestDetails);
1177 		}
1178 	}
1179 
1180 	public String parseContentTextIntoWords(IBaseResource theResource) {
1181 		StringBuilder retVal = new StringBuilder();
1182 		@SuppressWarnings("rawtypes")
1183 		List<IPrimitiveType> childElements = getContext().newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class);
1184 		for (@SuppressWarnings("rawtypes")
1185 			IPrimitiveType nextType : childElements) {
1186 			if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) {
1187 				String nextValue = nextType.getValueAsString();
1188 				if (isNotBlank(nextValue)) {
1189 					retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
1190 					retVal.append("\n");
1191 				}
1192 			}
1193 		}
1194 		return retVal.toString();
1195 	}
1196 
1197 	@Override
1198 	public void populateFullTextFields(final IBaseResource theResource, ResourceTable theEntity) {
1199 		if (theEntity.getDeleted() != null) {
1200 			theEntity.setNarrativeTextParsedIntoWords(null);
1201 			theEntity.setContentTextParsedIntoWords(null);
1202 		} else {
1203 			theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource));
1204 			theEntity.setContentTextParsedIntoWords(parseContentTextIntoWords(theResource));
1205 		}
1206 	}
1207 
1208 	private void populateResourceIdFromEntity(BaseHasResource theEntity, final IBaseResource theResource) {
1209 		IIdType id = theEntity.getIdDt();
1210 		if (getContext().getVersion().getVersion().isRi()) {
1211 			id = getContext().getVersion().newIdType().setValue(id.getValue());
1212 		}
1213 		theResource.setId(id);
1214 	}
1215 
1216 	/**
1217 	 * Returns true if the resource has changed (either the contents or the tags)
1218 	 */
1219 	protected EncodedResource populateResourceIntoEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, boolean theUpdateHash) {
1220 		if (theEntity.getResourceType() == null) {
1221 			theEntity.setResourceType(toResourceName(theResource));
1222 		}
1223 
1224 		if (theResource != null) {
1225 			List<BaseResourceReferenceDt> refs = myContext.newTerser().getAllPopulatedChildElementsOfType(theResource, BaseResourceReferenceDt.class);
1226 			for (BaseResourceReferenceDt nextRef : refs) {
1227 				if (nextRef.getReference().isEmpty() == false) {
1228 					if (nextRef.getReference().hasVersionIdPart()) {
1229 						nextRef.setReference(nextRef.getReference().toUnqualifiedVersionless());
1230 					}
1231 				}
1232 			}
1233 		}
1234 
1235 		byte[] bytes;
1236 		ResourceEncodingEnum encoding;
1237 		boolean changed = false;
1238 
1239 		if (theEntity.getDeleted() == null) {
1240 
1241 			encoding = myConfig.getResourceEncoding();
1242 			IParser parser = encoding.newParser(myContext);
1243 			parser.setDontEncodeElements(EXCLUDE_ELEMENTS_IN_ENCODED);
1244 			String encoded = parser.encodeResourceToString(theResource);
1245 
1246 			theEntity.setFhirVersion(myContext.getVersion().getVersion());
1247 			switch (encoding) {
1248 				case JSON:
1249 					bytes = encoded.getBytes(Charsets.UTF_8);
1250 					break;
1251 				case JSONC:
1252 					bytes = GZipUtil.compress(encoded);
1253 					break;
1254 				default:
1255 				case DEL:
1256 					bytes = new byte[0];
1257 					break;
1258 			}
1259 
1260 			ourLog.debug("Encoded {} chars of resource body as {} bytes", encoded.length(), bytes.length);
1261 
1262 			if (theUpdateHash) {
1263 				HashFunction sha256 = Hashing.sha256();
1264 				String hashSha256 = sha256.hashBytes(bytes).toString();
1265 				if (hashSha256.equals(theEntity.getHashSha256()) == false) {
1266 					changed = true;
1267 				}
1268 				theEntity.setHashSha256(hashSha256);
1269 			}
1270 
1271 			Set<ResourceTag> allDefs = new HashSet<>();
1272 			Set<ResourceTag> allTagsOld = getAllTagDefinitions(theEntity);
1273 
1274 			if (theResource instanceof IResource) {
1275 				extractTagsHapi((IResource) theResource, theEntity, allDefs);
1276 			} else {
1277 				extractTagsRi((IAnyResource) theResource, theEntity, allDefs);
1278 			}
1279 
1280 			RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
1281 			if (def.isStandardType() == false) {
1282 				String profile = def.getResourceProfile("");
1283 				if (isNotBlank(profile)) {
1284 					TagDefinition profileDef = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, profile, null);
1285 					if (def != null) {
1286 						ResourceTag tag = theEntity.addTag(profileDef);
1287 						allDefs.add(tag);
1288 						theEntity.setHasTags(true);
1289 					}
1290 				}
1291 			}
1292 
1293 			// Don't keep duplicate tags
1294 			Set<TagDefinition> allDefsPresent = new HashSet<>();
1295 			theEntity.getTags().removeIf(theResourceTag -> !allDefsPresent.add(theResourceTag.getTag()));
1296 
1297 			// Remove any tags that have been removed
1298 			for (ResourceTag next : allTagsOld) {
1299 				if (!allDefs.contains(next)) {
1300 					if (shouldDroppedTagBeRemovedOnUpdate(theRequest, next)) {
1301 						theEntity.getTags().remove(next);
1302 					}
1303 				}
1304 			}
1305 
1306 			Set<ResourceTag> allTagsNew = getAllTagDefinitions(theEntity);
1307 			if (!allTagsOld.equals(allTagsNew)) {
1308 				changed = true;
1309 			}
1310 			theEntity.setHasTags(!allTagsNew.isEmpty());
1311 
1312 		} else {
1313 			theEntity.setHashSha256(null);
1314 			bytes = null;
1315 			encoding = ResourceEncodingEnum.DEL;
1316 		}
1317 
1318 		if (changed == false) {
1319 			if (theEntity.getId() == null) {
1320 				changed = true;
1321 			} else {
1322 				ResourceHistoryTable currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion());
1323 				if (currentHistoryVersion == null || currentHistoryVersion.getResource() == null) {
1324 					changed = true;
1325 				} else {
1326 					changed = !Arrays.equals(currentHistoryVersion.getResource(), bytes);
1327 				}
1328 			}
1329 		}
1330 
1331 		EncodedResource retVal = new EncodedResource();
1332 		retVal.setEncoding(encoding);
1333 		retVal.setResource(bytes);
1334 		retVal.setChanged(changed);
1335 
1336 		return retVal;
1337 	}
1338 
1339 	@SuppressWarnings("unchecked")
1340 	private <R extends IBaseResource> R populateResourceMetadataHapi(Class<R> theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IResource res) {
1341 		R retVal = (R) res;
1342 		if (theEntity.getDeleted() != null) {
1343 			res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance();
1344 			retVal = (R) res;
1345 			ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
1346 			if (theForHistoryOperation) {
1347 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE);
1348 			}
1349 		} else if (theForHistoryOperation) {
1350 			/*
1351 			 * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT.
1352 			 */
1353 			Date published = theEntity.getPublished().getValue();
1354 			Date updated = theEntity.getUpdated().getValue();
1355 			if (published.equals(updated)) {
1356 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST);
1357 			} else {
1358 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT);
1359 			}
1360 		}
1361 
1362 		res.setId(theEntity.getIdDt());
1363 
1364 		ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion()));
1365 		ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished());
1366 		ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated());
1367 		IDao.RESOURCE_PID.put(res, theEntity.getId());
1368 
1369 		Collection<? extends BaseTag> tags = theEntity.getTags();
1370 		if (theEntity.isHasTags()) {
1371 			TagList tagList = new TagList();
1372 			List<IBaseCoding> securityLabels = new ArrayList<>();
1373 			List<IdDt> profiles = new ArrayList<>();
1374 			for (BaseTag next : tags) {
1375 				switch (next.getTag().getTagType()) {
1376 					case PROFILE:
1377 						profiles.add(new IdDt(next.getTag().getCode()));
1378 						break;
1379 					case SECURITY_LABEL:
1380 						IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt();
1381 						secLabel.setSystem(next.getTag().getSystem());
1382 						secLabel.setCode(next.getTag().getCode());
1383 						secLabel.setDisplay(next.getTag().getDisplay());
1384 						securityLabels.add(secLabel);
1385 						break;
1386 					case TAG:
1387 						tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay()));
1388 						break;
1389 				}
1390 			}
1391 			if (tagList.size() > 0) {
1392 				ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList);
1393 			}
1394 			if (securityLabels.size() > 0) {
1395 				ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels));
1396 			}
1397 			if (profiles.size() > 0) {
1398 				ResourceMetadataKeyEnum.PROFILES.put(res, profiles);
1399 			}
1400 		}
1401 
1402 		return retVal;
1403 	}
1404 
1405 	@SuppressWarnings("unchecked")
1406 	private <R extends IBaseResource> R populateResourceMetadataRi(Class<R> theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IAnyResource res) {
1407 		R retVal = (R) res;
1408 		if (theEntity.getDeleted() != null) {
1409 			res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance();
1410 			retVal = (R) res;
1411 			ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
1412 			if (theForHistoryOperation) {
1413 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.DELETE.toCode());
1414 			}
1415 		} else if (theForHistoryOperation) {
1416 			/*
1417 			 * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT.
1418 			 */
1419 			Date published = theEntity.getPublished().getValue();
1420 			Date updated = theEntity.getUpdated().getValue();
1421 			if (published.equals(updated)) {
1422 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.POST.toCode());
1423 			} else {
1424 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.PUT.toCode());
1425 			}
1426 		}
1427 
1428 		res.getMeta().getTag().clear();
1429 		res.getMeta().getProfile().clear();
1430 		res.getMeta().getSecurity().clear();
1431 		res.getMeta().setLastUpdated(null);
1432 		res.getMeta().setVersionId(null);
1433 
1434 		populateResourceIdFromEntity(theEntity, res);
1435 
1436 		res.getMeta().setLastUpdated(theEntity.getUpdatedDate());
1437 		IDao.RESOURCE_PID.put(res, theEntity.getId());
1438 
1439 		Collection<? extends BaseTag> tags = theEntity.getTags();
1440 
1441 		if (theEntity.isHasTags()) {
1442 			for (BaseTag next : tags) {
1443 				switch (next.getTag().getTagType()) {
1444 					case PROFILE:
1445 						res.getMeta().addProfile(next.getTag().getCode());
1446 						break;
1447 					case SECURITY_LABEL:
1448 						IBaseCoding sec = res.getMeta().addSecurity();
1449 						sec.setSystem(next.getTag().getSystem());
1450 						sec.setCode(next.getTag().getCode());
1451 						sec.setDisplay(next.getTag().getDisplay());
1452 						break;
1453 					case TAG:
1454 						IBaseCoding tag = res.getMeta().addTag();
1455 						tag.setSystem(next.getTag().getSystem());
1456 						tag.setCode(next.getTag().getCode());
1457 						tag.setDisplay(next.getTag().getDisplay());
1458 						break;
1459 				}
1460 			}
1461 		}
1462 		return retVal;
1463 	}
1464 
1465 	/**
1466 	 * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time.
1467 	 *
1468 	 * @param theEntity   The entity being updated (Do not modify the entity! Undefined behaviour will occur!)
1469 	 * @param theResource The resource being persisted
1470 	 */
1471 	protected void postPersist(ResourceTable theEntity, T theResource) {
1472 		// nothing
1473 	}
1474 
1475 	/**
1476 	 * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
1477 	 *
1478 	 * @param theEntity   The resource
1479 	 * @param theResource The resource being persisted
1480 	 */
1481 	protected void postUpdate(ResourceTable theEntity, T theResource) {
1482 		// nothing
1483 	}
1484 
1485 	@Override
1486 	public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType) {
1487 		RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType);
1488 
1489 		SearchParameterMap paramMap = translateMatchUrl(this, myContext, theMatchUrl, resourceDef);
1490 		paramMap.setLoadSynchronous(true);
1491 
1492 		if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) {
1493 			throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters");
1494 		}
1495 
1496 		IFhirResourceDao<R> dao = getDao(theResourceType);
1497 		if (dao == null) {
1498 			throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
1499 		}
1500 
1501 		return dao.searchForIds(paramMap);
1502 	}
1503 
1504 	@CoverageIgnore
1505 	public BaseHasResource readEntity(IIdType theValueId) {
1506 		throw new NotImplementedException("");
1507 	}
1508 
1509 	private <T> Collection<T> removeCommon(Collection<T> theInput, Collection<T> theToRemove) {
1510 		assert theInput != theToRemove;
1511 
1512 		if (theInput.isEmpty()) {
1513 			return theInput;
1514 		}
1515 
1516 		ArrayList<T> retVal = new ArrayList<>(theInput);
1517 		retVal.removeAll(theToRemove);
1518 		return retVal;
1519 	}
1520 
1521 	private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) {
1522 		for (BaseResourceIndexedSearchParam nextSearchParam : theParams) {
1523 			nextSearchParam.setUpdated(theUpdateTime);
1524 		}
1525 	}
1526 
1527 	/**
1528 	 * This method is called when an update to an existing resource detects that the resource supplied for update is missing a tag/profile/security label that the currently persisted resource holds.
1529 	 * <p>
1530 	 * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour.
1531 	 * </p>
1532 	 * <p>
1533 	 * See <a href="http://hl7.org/fhir/resource.html#tag-updates">Updates to Tags, Profiles, and Security Labels</a> for a description of the logic that the default behaviour folows.
1534 	 * </p>
1535 	 *
1536 	 * @param theTag The tag
1537 	 * @return Returns <code>true</code> if the tag should be removed
1538 	 */
1539 	protected boolean shouldDroppedTagBeRemovedOnUpdate(RequestDetails theRequest, ResourceTag theTag) {
1540 
1541 		Set<TagTypeEnum> metaSnapshotModeTokens = null;
1542 
1543 		if (theRequest != null) {
1544 			List<String> metaSnapshotMode = theRequest.getHeaders(JpaConstants.HEADER_META_SNAPSHOT_MODE);
1545 			if (metaSnapshotMode != null && !metaSnapshotMode.isEmpty()) {
1546 				metaSnapshotModeTokens = new HashSet<>();
1547 				for (String nextHeaderValue : metaSnapshotMode) {
1548 					StringTokenizer tok = new StringTokenizer(nextHeaderValue, ",");
1549 					while (tok.hasMoreTokens()) {
1550 						switch (trim(tok.nextToken())) {
1551 							case "TAG":
1552 								metaSnapshotModeTokens.add(TagTypeEnum.TAG);
1553 								break;
1554 							case "PROFILE":
1555 								metaSnapshotModeTokens.add(TagTypeEnum.PROFILE);
1556 								break;
1557 							case "SECURITY_LABEL":
1558 								metaSnapshotModeTokens.add(TagTypeEnum.SECURITY_LABEL);
1559 								break;
1560 						}
1561 					}
1562 				}
1563 			}
1564 		}
1565 
1566 		if (metaSnapshotModeTokens == null) {
1567 			metaSnapshotModeTokens = Collections.singleton(TagTypeEnum.PROFILE);
1568 		}
1569 
1570 		if (metaSnapshotModeTokens.contains(theTag.getTag().getTagType())) {
1571 			return true;
1572 		}
1573 
1574 		return false;
1575 	}
1576 
1577 	private ExpungeOutcome toExpungeOutcome(ExpungeOptions theExpungeOptions, AtomicInteger theRemainingCount) {
1578 		return new ExpungeOutcome()
1579 			.setDeletedCount(theExpungeOptions.getLimit() - theRemainingCount.get());
1580 	}
1581 
1582 	@Override
1583 	public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) {
1584 		RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType());
1585 		Class<? extends IBaseResource> resourceType = type.getImplementingClass();
1586 		return toResource(resourceType, theEntity, theForHistoryOperation);
1587 	}
1588 
1589 	@SuppressWarnings("unchecked")
1590 	@Override
1591 	public <R extends IBaseResource> R toResource(Class<R> theResourceType, BaseHasResource theEntity,
1592 																 boolean theForHistoryOperation) {
1593 
1594 		ResourceHistoryTable history;
1595 		if (theEntity instanceof ResourceHistoryTable) {
1596 			history = (ResourceHistoryTable) theEntity;
1597 		} else {
1598 			history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion());
1599 		}
1600 
1601 		if (history == null) {
1602 			return null;
1603 		}
1604 
1605 		byte[] resourceBytes = history.getResource();
1606 		ResourceEncodingEnum resourceEncoding = history.getEncoding();
1607 
1608 		String resourceText = null;
1609 		switch (resourceEncoding) {
1610 			case JSON:
1611 				try {
1612 					resourceText = new String(resourceBytes, "UTF-8");
1613 				} catch (UnsupportedEncodingException e) {
1614 					throw new Error("Should not happen", e);
1615 				}
1616 				break;
1617 			case JSONC:
1618 				resourceText = GZipUtil.decompress(resourceBytes);
1619 				break;
1620 			case DEL:
1621 				break;
1622 		}
1623 
1624 		/*
1625 		 * Use the appropriate custom type if one is specified in the context
1626 		 */
1627 		Class<R> resourceType = theResourceType;
1628 		if (myContext.hasDefaultTypeForProfile()) {
1629 			for (BaseTag nextTag : theEntity.getTags()) {
1630 				if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) {
1631 					String profile = nextTag.getTag().getCode();
1632 					if (isNotBlank(profile)) {
1633 						Class<? extends IBaseResource> newType = myContext.getDefaultTypeForProfile(profile);
1634 						if (newType != null && theResourceType.isAssignableFrom(newType)) {
1635 							ourLog.debug("Using custom type {} for profile: {}", newType.getName(), profile);
1636 							resourceType = (Class<R>) newType;
1637 							break;
1638 						}
1639 					}
1640 				}
1641 			}
1642 		}
1643 
1644 		R retVal;
1645 		if (resourceEncoding != ResourceEncodingEnum.DEL) {
1646 			IParser parser = resourceEncoding.newParser(getContext(theEntity.getFhirVersion()));
1647 			parser.setParserErrorHandler(new LenientErrorHandler(false).setErrorOnInvalidValue(false));
1648 
1649 			try {
1650 				retVal = parser.parseResource(resourceType, resourceText);
1651 			} catch (Exception e) {
1652 				StringBuilder b = new StringBuilder();
1653 				b.append("Failed to parse database resource[");
1654 				b.append(resourceType);
1655 				b.append("/");
1656 				b.append(theEntity.getIdDt().getIdPart());
1657 				b.append(" (pid ");
1658 				b.append(theEntity.getId());
1659 				b.append(", version ");
1660 				b.append(theEntity.getFhirVersion().name());
1661 				b.append("): ");
1662 				b.append(e.getMessage());
1663 				String msg = b.toString();
1664 				ourLog.error(msg, e);
1665 				throw new DataFormatException(msg, e);
1666 			}
1667 
1668 		} else {
1669 
1670 			retVal = (R) myContext.getResourceDefinition(theEntity.getResourceType()).newInstance();
1671 
1672 		}
1673 
1674 		if (retVal instanceof IResource) {
1675 			IResource res = (IResource) retVal;
1676 			retVal = populateResourceMetadataHapi(resourceType, theEntity, theForHistoryOperation, res);
1677 		} else {
1678 			IAnyResource res = (IAnyResource) retVal;
1679 			retVal = populateResourceMetadataRi(resourceType, theEntity, theForHistoryOperation, res);
1680 		}
1681 
1682 
1683 		return retVal;
1684 	}
1685 
1686 	protected String toResourceName(Class<? extends IBaseResource> theResourceType) {
1687 		return myContext.getResourceDefinition(theResourceType).getName();
1688 	}
1689 
1690 	String toResourceName(IBaseResource theResource) {
1691 		return myContext.getResourceDefinition(theResource).getName();
1692 	}
1693 
1694 	private Slice<Long> toSlice(ResourceHistoryTable theVersion) {
1695 		Validate.notNull(theVersion);
1696 		return new SliceImpl<>(Collections.singletonList(theVersion.getId()));
1697 	}
1698 
1699 	Long translateForcedIdToPid(String theResourceName, String theResourceId) {
1700 		return translateForcedIdToPids(new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0);
1701 	}
1702 
1703 	protected List<Long> translateForcedIdToPids(IIdType theId) {
1704 		return translateForcedIdToPids(theId, myForcedIdDao);
1705 	}
1706 
1707 	private String translatePidIdToForcedId(String theResourceType, Long theId) {
1708 		ForcedId forcedId = myForcedIdDao.findByResourcePid(theId);
1709 		if (forcedId != null) {
1710 			return forcedId.getResourceType() + '/' + forcedId.getForcedId();
1711 		} else {
1712 			return theResourceType + '/' + theId.toString();
1713 		}
1714 	}
1715 
1716 	@SuppressWarnings("unchecked")
1717 	protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable
1718 		theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
1719 													 boolean theUpdateVersion, Date theUpdateTime, boolean theForceUpdate, boolean theCreateNewHistoryEntry) {
1720 		ourLog.debug("Starting entity update");
1721 
1722 		/*
1723 		 * This should be the very first thing..
1724 		 */
1725 		if (theResource != null) {
1726 			if (thePerformIndexing) {
1727 				if (!ourValidationDisabledForUnitTest) {
1728 					validateResourceForStorage((T) theResource, theEntity);
1729 				}
1730 			}
1731 			String resourceType = myContext.getResourceDefinition(theResource).getName();
1732 			if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) {
1733 				throw new UnprocessableEntityException(
1734 					"Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]");
1735 			}
1736 		}
1737 
1738 		if (theEntity.getPublished() == null) {
1739 			ourLog.debug("Entity has published time: {}", new InstantDt(theUpdateTime));
1740 
1741 			theEntity.setPublished(theUpdateTime);
1742 		}
1743 
1744 		Collection<ResourceIndexedSearchParamString> existingStringParams = new ArrayList<>();
1745 		if (theEntity.isParamsStringPopulated()) {
1746 			existingStringParams.addAll(theEntity.getParamsString());
1747 		}
1748 		Collection<ResourceIndexedSearchParamToken> existingTokenParams = new ArrayList<>();
1749 		if (theEntity.isParamsTokenPopulated()) {
1750 			existingTokenParams.addAll(theEntity.getParamsToken());
1751 		}
1752 		Collection<ResourceIndexedSearchParamNumber> existingNumberParams = new ArrayList<>();
1753 		if (theEntity.isParamsNumberPopulated()) {
1754 			existingNumberParams.addAll(theEntity.getParamsNumber());
1755 		}
1756 		Collection<ResourceIndexedSearchParamQuantity> existingQuantityParams = new ArrayList<>();
1757 		if (theEntity.isParamsQuantityPopulated()) {
1758 			existingQuantityParams.addAll(theEntity.getParamsQuantity());
1759 		}
1760 		Collection<ResourceIndexedSearchParamDate> existingDateParams = new ArrayList<>();
1761 		if (theEntity.isParamsDatePopulated()) {
1762 			existingDateParams.addAll(theEntity.getParamsDate());
1763 		}
1764 		Collection<ResourceIndexedSearchParamUri> existingUriParams = new ArrayList<>();
1765 		if (theEntity.isParamsUriPopulated()) {
1766 			existingUriParams.addAll(theEntity.getParamsUri());
1767 		}
1768 		Collection<ResourceIndexedSearchParamCoords> existingCoordsParams = new ArrayList<>();
1769 		if (theEntity.isParamsCoordsPopulated()) {
1770 			existingCoordsParams.addAll(theEntity.getParamsCoords());
1771 		}
1772 		Collection<ResourceLink> existingResourceLinks = new ArrayList<>();
1773 		if (theEntity.isHasLinks()) {
1774 			existingResourceLinks.addAll(theEntity.getResourceLinks());
1775 		}
1776 
1777 		Collection<ResourceIndexedCompositeStringUnique> existingCompositeStringUniques = new ArrayList<>();
1778 		if (theEntity.isParamsCompositeStringUniquePresent()) {
1779 			existingCompositeStringUniques.addAll(theEntity.getParamsCompositeStringUnique());
1780 		}
1781 
1782 		Set<ResourceIndexedSearchParamString> stringParams = null;
1783 		Set<ResourceIndexedSearchParamToken> tokenParams = null;
1784 		Set<ResourceIndexedSearchParamNumber> numberParams = null;
1785 		Set<ResourceIndexedSearchParamQuantity> quantityParams = null;
1786 		Set<ResourceIndexedSearchParamDate> dateParams = null;
1787 		Set<ResourceIndexedSearchParamUri> uriParams = null;
1788 		Set<ResourceIndexedSearchParamCoords> coordsParams = null;
1789 		Set<ResourceIndexedCompositeStringUnique> compositeStringUniques = null;
1790 		Set<ResourceLink> links = null;
1791 
1792 		Set<String> populatedResourceLinkParameters = Collections.emptySet();
1793 		EncodedResource changed;
1794 		if (theDeletedTimestampOrNull != null) {
1795 
1796 			stringParams = Collections.emptySet();
1797 			tokenParams = Collections.emptySet();
1798 			numberParams = Collections.emptySet();
1799 			quantityParams = Collections.emptySet();
1800 			dateParams = Collections.emptySet();
1801 			uriParams = Collections.emptySet();
1802 			coordsParams = Collections.emptySet();
1803 			links = Collections.emptySet();
1804 			compositeStringUniques = Collections.emptySet();
1805 
1806 			theEntity.setDeleted(theDeletedTimestampOrNull);
1807 			theEntity.setUpdated(theDeletedTimestampOrNull);
1808 			theEntity.setNarrativeTextParsedIntoWords(null);
1809 			theEntity.setContentTextParsedIntoWords(null);
1810 			theEntity.setHashSha256(null);
1811 			changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
1812 
1813 		} else {
1814 
1815 			theEntity.setDeleted(null);
1816 
1817 			if (thePerformIndexing) {
1818 
1819 				stringParams = extractSearchParamStrings(theEntity, theResource);
1820 				numberParams = extractSearchParamNumber(theEntity, theResource);
1821 				quantityParams = extractSearchParamQuantity(theEntity, theResource);
1822 				dateParams = extractSearchParamDates(theEntity, theResource);
1823 				uriParams = extractSearchParamUri(theEntity, theResource);
1824 				coordsParams = extractSearchParamCoords(theEntity, theResource);
1825 
1826 				ourLog.trace("Storing date indexes: {}", dateParams);
1827 
1828 				tokenParams = new HashSet<>();
1829 				for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) {
1830 					if (next instanceof ResourceIndexedSearchParamToken) {
1831 						tokenParams.add((ResourceIndexedSearchParamToken) next);
1832 					} else {
1833 						stringParams.add((ResourceIndexedSearchParamString) next);
1834 					}
1835 				}
1836 
1837 				Set<Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
1838 				if (myConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
1839 					findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.STRING, stringParams);
1840 					findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams);
1841 					findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams);
1842 					findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.DATE, dateParams);
1843 					findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.URI, uriParams);
1844 					findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams);
1845 				}
1846 
1847 				setUpdatedTime(stringParams, theUpdateTime);
1848 				setUpdatedTime(numberParams, theUpdateTime);
1849 				setUpdatedTime(quantityParams, theUpdateTime);
1850 				setUpdatedTime(dateParams, theUpdateTime);
1851 				setUpdatedTime(uriParams, theUpdateTime);
1852 				setUpdatedTime(coordsParams, theUpdateTime);
1853 				setUpdatedTime(tokenParams, theUpdateTime);
1854 
1855 				/*
1856 				 * Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
1857 				 * matching resource.
1858 				 */
1859 				if (myConfig.isAllowInlineMatchUrlReferences()) {
1860 					FhirTerser terser = getContext().newTerser();
1861 					List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
1862 					for (IBaseReference nextRef : allRefs) {
1863 						IIdType nextId = nextRef.getReferenceElement();
1864 						String nextIdText = nextId.getValue();
1865 						if (nextIdText == null) {
1866 							continue;
1867 						}
1868 						int qmIndex = nextIdText.indexOf('?');
1869 						if (qmIndex != -1) {
1870 							for (int i = qmIndex - 1; i >= 0; i--) {
1871 								if (nextIdText.charAt(i) == '/') {
1872 									if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
1873 										// Just in case the URL is in the form Patient/?foo=bar
1874 										continue;
1875 									}
1876 									nextIdText = nextIdText.substring(i + 1);
1877 									break;
1878 								}
1879 							}
1880 							String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
1881 							RuntimeResourceDefinition matchResourceDef = getContext().getResourceDefinition(resourceTypeString);
1882 							if (matchResourceDef == null) {
1883 								String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
1884 								throw new InvalidRequestException(msg);
1885 							}
1886 							Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
1887 							Set<Long> matches = processMatchUrl(nextIdText, matchResourceType);
1888 							if (matches.isEmpty()) {
1889 								String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
1890 								throw new ResourceNotFoundException(msg);
1891 							}
1892 							if (matches.size() > 1) {
1893 								String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
1894 								throw new PreconditionFailedException(msg);
1895 							}
1896 							Long next = matches.iterator().next();
1897 							String newId = translatePidIdToForcedId(resourceTypeString, next);
1898 							ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
1899 							nextRef.setReference(newId);
1900 						}
1901 					}
1902 				}
1903 
1904 				links = new HashSet<>();
1905 				populatedResourceLinkParameters = extractResourceLinks(theEntity, theResource, links, theUpdateTime);
1906 
1907 				/*
1908 				 * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
1909 				 */
1910 				for (Iterator<ResourceLink> existingLinkIter = existingResourceLinks.iterator(); existingLinkIter.hasNext(); ) {
1911 					ResourceLink nextExisting = existingLinkIter.next();
1912 					if (links.remove(nextExisting)) {
1913 						existingLinkIter.remove();
1914 						links.add(nextExisting);
1915 					}
1916 				}
1917 
1918 				/*
1919 				 * Handle composites
1920 				 */
1921 				compositeStringUniques = extractCompositeStringUniques(theEntity, stringParams, tokenParams, numberParams, quantityParams, dateParams, uriParams, links);
1922 
1923 				changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
1924 
1925 				theEntity.setUpdated(theUpdateTime);
1926 				if (theResource instanceof IResource) {
1927 					theEntity.setLanguage(((IResource) theResource).getLanguage().getValue());
1928 				} else {
1929 					theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue());
1930 				}
1931 				theEntity.setParamsString(stringParams);
1932 				theEntity.setParamsStringPopulated(stringParams.isEmpty() == false);
1933 				theEntity.setParamsToken(tokenParams);
1934 				theEntity.setParamsTokenPopulated(tokenParams.isEmpty() == false);
1935 				theEntity.setParamsNumber(numberParams);
1936 				theEntity.setParamsNumberPopulated(numberParams.isEmpty() == false);
1937 				theEntity.setParamsQuantity(quantityParams);
1938 				theEntity.setParamsQuantityPopulated(quantityParams.isEmpty() == false);
1939 				theEntity.setParamsDate(dateParams);
1940 				theEntity.setParamsDatePopulated(dateParams.isEmpty() == false);
1941 				theEntity.setParamsUri(uriParams);
1942 				theEntity.setParamsUriPopulated(uriParams.isEmpty() == false);
1943 				theEntity.setParamsCoords(coordsParams);
1944 				theEntity.setParamsCoordsPopulated(coordsParams.isEmpty() == false);
1945 				theEntity.setParamsCompositeStringUniquePresent(compositeStringUniques.isEmpty() == false);
1946 				theEntity.setResourceLinks(links);
1947 				theEntity.setHasLinks(links.isEmpty() == false);
1948 				theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
1949 				populateFullTextFields(theResource, theEntity);
1950 
1951 			} else {
1952 
1953 				changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false);
1954 
1955 				theEntity.setUpdated(theUpdateTime);
1956 				// theEntity.setLanguage(theResource.getLanguage().getValue());
1957 				theEntity.setIndexStatus(null);
1958 
1959 			}
1960 
1961 		}
1962 
1963 		if (!changed.isChanged() && !theForceUpdate && myConfig.isSuppressUpdatesWithNoChange()) {
1964 			ourLog.debug("Resource {} has not changed", theEntity.getIdDt().toUnqualified().getValue());
1965 			if (theResource != null) {
1966 				populateResourceIdFromEntity(theEntity, theResource);
1967 			}
1968 			theEntity.setUnchangedInCurrentOperation(true);
1969 			return theEntity;
1970 		}
1971 
1972 		if (theUpdateVersion) {
1973 			theEntity.setVersion(theEntity.getVersion() + 1);
1974 		}
1975 
1976 		/*
1977 		 * Save the resource itself
1978 		 */
1979 		if (theEntity.getId() == null) {
1980 			myEntityManager.persist(theEntity);
1981 
1982 			if (theEntity.getForcedId() != null) {
1983 				myEntityManager.persist(theEntity.getForcedId());
1984 			}
1985 
1986 			postPersist(theEntity, (T) theResource);
1987 
1988 		} else {
1989 			theEntity = myEntityManager.merge(theEntity);
1990 
1991 			postUpdate(theEntity, (T) theResource);
1992 		}
1993 
1994 		/*
1995 		 * Create history entry
1996 		 */
1997 		if (theCreateNewHistoryEntry) {
1998 			final ResourceHistoryTable historyEntry = theEntity.toHistory();
1999 //			if (theEntity.getVersion() > 1) {
2000 //				existing = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion());
2001 //				ourLog.warn("Reusing existing history entry entity {}", theEntity.getIdDt().getValue());
2002 //			}
2003 			historyEntry.setEncoding(changed.getEncoding());
2004 			historyEntry.setResource(changed.getResource());
2005 
2006 			ourLog.debug("Saving history entry {}", historyEntry.getIdDt());
2007 			myResourceHistoryTableDao.save(historyEntry);
2008 		}
2009 
2010 		/*
2011 		 * Update the "search param present" table which is used for the
2012 		 * ?foo:missing=true queries
2013 		 *
2014 		 * Note that we're only populating this for reference params
2015 		 * because the index tables for all other types have a MISSING column
2016 		 * right on them for handling the :missing queries. We can't use the
2017 		 * index table for resource links (reference indexes) because we index
2018 		 * those by path and not by parameter name.
2019 		 */
2020 		if (thePerformIndexing) {
2021 			Map<String, Boolean> presentSearchParams = new HashMap<>();
2022 			for (String nextKey : populatedResourceLinkParameters) {
2023 				presentSearchParams.put(nextKey, Boolean.TRUE);
2024 			}
2025 			Set<Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
2026 			for (Entry<String, RuntimeSearchParam> nextSpEntry : activeSearchParams) {
2027 				if (nextSpEntry.getValue().getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
2028 					if (!presentSearchParams.containsKey(nextSpEntry.getKey())) {
2029 						presentSearchParams.put(nextSpEntry.getKey(), Boolean.FALSE);
2030 					}
2031 				}
2032 			}
2033 			mySearchParamPresenceSvc.updatePresence(theEntity, presentSearchParams);
2034 		}
2035 
2036 		/*
2037 		 * Indexing
2038 		 */
2039 		if (thePerformIndexing) {
2040 
2041 			for (ResourceIndexedSearchParamString next : removeCommon(existingStringParams, stringParams)) {
2042 				myEntityManager.remove(next);
2043 				theEntity.getParamsString().remove(next);
2044 			}
2045 			for (ResourceIndexedSearchParamString next : removeCommon(stringParams, existingStringParams)) {
2046 				myEntityManager.persist(next);
2047 			}
2048 
2049 			for (ResourceIndexedSearchParamToken next : removeCommon(existingTokenParams, tokenParams)) {
2050 				myEntityManager.remove(next);
2051 				theEntity.getParamsToken().remove(next);
2052 			}
2053 			for (ResourceIndexedSearchParamToken next : removeCommon(tokenParams, existingTokenParams)) {
2054 				myEntityManager.persist(next);
2055 			}
2056 
2057 			for (ResourceIndexedSearchParamNumber next : removeCommon(existingNumberParams, numberParams)) {
2058 				myEntityManager.remove(next);
2059 				theEntity.getParamsNumber().remove(next);
2060 			}
2061 			for (ResourceIndexedSearchParamNumber next : removeCommon(numberParams, existingNumberParams)) {
2062 				myEntityManager.persist(next);
2063 			}
2064 
2065 			for (ResourceIndexedSearchParamQuantity next : removeCommon(existingQuantityParams, quantityParams)) {
2066 				myEntityManager.remove(next);
2067 				theEntity.getParamsQuantity().remove(next);
2068 			}
2069 			for (ResourceIndexedSearchParamQuantity next : removeCommon(quantityParams, existingQuantityParams)) {
2070 				myEntityManager.persist(next);
2071 			}
2072 
2073 			// Store date SP's
2074 			for (ResourceIndexedSearchParamDate next : removeCommon(existingDateParams, dateParams)) {
2075 				myEntityManager.remove(next);
2076 				theEntity.getParamsDate().remove(next);
2077 			}
2078 			for (ResourceIndexedSearchParamDate next : removeCommon(dateParams, existingDateParams)) {
2079 				myEntityManager.persist(next);
2080 			}
2081 
2082 			// Store URI SP's
2083 			for (ResourceIndexedSearchParamUri next : removeCommon(existingUriParams, uriParams)) {
2084 				myEntityManager.remove(next);
2085 				theEntity.getParamsUri().remove(next);
2086 			}
2087 			for (ResourceIndexedSearchParamUri next : removeCommon(uriParams, existingUriParams)) {
2088 				myEntityManager.persist(next);
2089 			}
2090 
2091 			// Store Coords SP's
2092 			for (ResourceIndexedSearchParamCoords next : removeCommon(existingCoordsParams, coordsParams)) {
2093 				myEntityManager.remove(next);
2094 				theEntity.getParamsCoords().remove(next);
2095 			}
2096 			for (ResourceIndexedSearchParamCoords next : removeCommon(coordsParams, existingCoordsParams)) {
2097 				myEntityManager.persist(next);
2098 			}
2099 
2100 			// Store resource links
2101 			for (ResourceLink next : removeCommon(existingResourceLinks, links)) {
2102 				myEntityManager.remove(next);
2103 				theEntity.getResourceLinks().remove(next);
2104 			}
2105 			for (ResourceLink next : removeCommon(links, existingResourceLinks)) {
2106 				myEntityManager.persist(next);
2107 			}
2108 			// make sure links are indexed
2109 			theEntity.setResourceLinks(links);
2110 
2111 			// Store composite string uniques
2112 			if (getConfig().isUniqueIndexesEnabled()) {
2113 				for (ResourceIndexedCompositeStringUnique next : removeCommon(existingCompositeStringUniques, compositeStringUniques)) {
2114 					ourLog.debug("Removing unique index: {}", next);
2115 					myEntityManager.remove(next);
2116 					theEntity.getParamsCompositeStringUnique().remove(next);
2117 				}
2118 				for (ResourceIndexedCompositeStringUnique next : removeCommon(compositeStringUniques, existingCompositeStringUniques)) {
2119 					if (myConfig.isUniqueIndexesCheckedBeforeSave()) {
2120 						ResourceIndexedCompositeStringUnique existing = myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString());
2121 						if (existing != null) {
2122 							String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue());
2123 							throw new PreconditionFailedException(msg);
2124 						}
2125 					}
2126 					ourLog.debug("Persisting unique index: {}", next);
2127 					myEntityManager.persist(next);
2128 				}
2129 			}
2130 
2131 		} // if thePerformIndexing
2132 
2133 		theEntity = myEntityManager.merge(theEntity);
2134 
2135 		if (theResource != null) {
2136 			populateResourceIdFromEntity(theEntity, theResource);
2137 		}
2138 
2139 		return theEntity;
2140 	}
2141 
2142 	protected ResourceTable updateEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable
2143 		entity, Date theDeletedTimestampOrNull, Date theUpdateTime) {
2144 		return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true);
2145 	}
2146 
2147 	public ResourceTable updateInternal(RequestDetails theRequest, T theResource, boolean thePerformIndexing,
2148 													boolean theForceUpdateVersion, RequestDetails theRequestDetails, ResourceTable theEntity, IIdType
2149 														theResourceId, IBaseResource theOldResource) {
2150 		// Notify interceptors
2151 		ActionRequestDetails requestDetails = null;
2152 		if (theRequestDetails != null) {
2153 			requestDetails = new ActionRequestDetails(theRequestDetails, theResource, theResourceId.getResourceType(), theResourceId);
2154 			notifyInterceptors(RestOperationTypeEnum.UPDATE, requestDetails);
2155 		}
2156 
2157 		// Notify IServerOperationInterceptors about pre-action call
2158 		if (theRequestDetails != null) {
2159 			theRequestDetails.getRequestOperationCallback().resourcePreUpdate(theOldResource, theResource);
2160 		}
2161 		for (IServerInterceptor next : getConfig().getInterceptors()) {
2162 			if (next instanceof IServerOperationInterceptor) {
2163 				((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource);
2164 			}
2165 		}
2166 
2167 		// Perform update
2168 		ResourceTable savedEntity = updateEntity(theRequest, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing);
2169 
2170 		/*
2171 		 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
2172 		 * we'll manually increase the version. This is important because we want the updated version number
2173 		 * to be reflected in the resource shared with interceptors
2174 		 */
2175 		if (!thePerformIndexing && !savedEntity.isUnchangedInCurrentOperation() && !ourDisableIncrementOnUpdateForUnitTest) {
2176 			if (theResourceId.hasVersionIdPart() == false) {
2177 				theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion()));
2178 			}
2179 			incrementId(theResource, savedEntity, theResourceId);
2180 		}
2181 
2182 		// Notify interceptors
2183 		if (!savedEntity.isUnchangedInCurrentOperation()) {
2184 			if (theRequestDetails != null) {
2185 				theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
2186 				theRequestDetails.getRequestOperationCallback().resourceUpdated(theOldResource, theResource);
2187 			}
2188 			for (IServerInterceptor next : getConfig().getInterceptors()) {
2189 				if (next instanceof IServerOperationInterceptor) {
2190 					((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource);
2191 					((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource);
2192 				}
2193 			}
2194 		}
2195 		return savedEntity;
2196 	}
2197 
2198 	private void validateChildReferences(IBase theElement, String thePath) {
2199 		if (theElement == null) {
2200 			return;
2201 		}
2202 		BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
2203 		if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
2204 			return;
2205 		}
2206 
2207 		BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def;
2208 		for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) {
2209 
2210 			List<IBase> values = nextChildDef.getAccessor().getValues(theElement);
2211 			if (values == null || values.isEmpty()) {
2212 				continue;
2213 			}
2214 
2215 			String newPath = thePath + "." + nextChildDef.getElementName();
2216 
2217 			for (IBase nextChild : values) {
2218 				validateChildReferences(nextChild, newPath);
2219 			}
2220 
2221 			if (nextChildDef instanceof RuntimeChildResourceDefinition) {
2222 				RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef;
2223 				Set<String> validTypes = new HashSet<>();
2224 				boolean allowAny = false;
2225 				for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) {
2226 					if (nextValidType.isInterface()) {
2227 						allowAny = true;
2228 						break;
2229 					}
2230 					validTypes.add(getContext().getResourceDefinition(nextValidType).getName());
2231 				}
2232 
2233 				if (allowAny) {
2234 					continue;
2235 				}
2236 
2237 				for (IBase nextChild : values) {
2238 					IBaseReference nextRef = (IBaseReference) nextChild;
2239 					IIdType referencedId = nextRef.getReferenceElement();
2240 					if (!isBlank(referencedId.getResourceType())) {
2241 						if (!isLogicalReference(referencedId)) {
2242 							if (!referencedId.getValue().contains("?")) {
2243 								if (!validTypes.contains(referencedId.getResourceType())) {
2244 									throw new UnprocessableEntityException(
2245 										"Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path");
2246 								}
2247 							}
2248 						}
2249 					}
2250 				}
2251 
2252 			}
2253 		}
2254 	}
2255 
2256 	protected void validateDeleteConflictsEmptyOrThrowException(List<DeleteConflict> theDeleteConflicts) {
2257 		if (theDeleteConflicts.isEmpty()) {
2258 			return;
2259 		}
2260 
2261 		IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
2262 		String firstMsg = null;
2263 		for (DeleteConflict next : theDeleteConflicts) {
2264 			StringBuilder b = new StringBuilder();
2265 			b.append("Unable to delete ");
2266 			b.append(next.getTargetId().toUnqualifiedVersionless().getValue());
2267 			b.append(" because at least one resource has a reference to this resource. First reference found was resource ");
2268 			b.append(next.getTargetId().toUnqualifiedVersionless().getValue());
2269 			b.append(" in path ");
2270 			b.append(next.getSourcePath());
2271 			String msg = b.toString();
2272 			if (firstMsg == null) {
2273 				firstMsg = msg;
2274 			}
2275 			OperationOutcomeUtil.addIssue(getContext(), oo, OO_SEVERITY_ERROR, msg, null, "processing");
2276 		}
2277 
2278 		throw new ResourceVersionConflictException(firstMsg, oo);
2279 	}
2280 
2281 	protected void validateMetaCount(int theMetaCount) {
2282 		if (myConfig.getResourceMetaCountHardLimit() != null) {
2283 			if (theMetaCount > myConfig.getResourceMetaCountHardLimit()) {
2284 				throw new UnprocessableEntityException("Resource contains " + theMetaCount + " meta entries (tag/profile/security label), maximum is " + myConfig.getResourceMetaCountHardLimit());
2285 			}
2286 		}
2287 	}
2288 
2289 	/**
2290 	 * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow the DAO to ensure that it is valid for persistence. By default, checks for the
2291 	 * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check.
2292 	 *
2293 	 * @param theResource     The resource that is about to be persisted
2294 	 * @param theEntityToSave TODO
2295 	 */
2296 	protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) {
2297 		Object tag = null;
2298 
2299 		int totalMetaCount = 0;
2300 
2301 		if (theResource instanceof IResource) {
2302 			IResource res = (IResource) theResource;
2303 			TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res);
2304 			if (tagList != null) {
2305 				tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM, Constants.TAG_SUBSETTED_CODE);
2306 				totalMetaCount += tagList.size();
2307 			}
2308 			List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res);
2309 			if (profileList != null) {
2310 				totalMetaCount += profileList.size();
2311 			}
2312 		} else {
2313 			IAnyResource res = (IAnyResource) theResource;
2314 			tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM, Constants.TAG_SUBSETTED_CODE);
2315 			totalMetaCount += res.getMeta().getTag().size();
2316 			totalMetaCount += res.getMeta().getProfile().size();
2317 			totalMetaCount += res.getMeta().getSecurity().size();
2318 		}
2319 
2320 		if (tag != null) {
2321 			throw new UnprocessableEntityException("Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data");
2322 		}
2323 
2324 		String resName = getContext().getResourceDefinition(theResource).getName();
2325 		validateChildReferences(theResource, resName);
2326 
2327 		validateMetaCount(totalMetaCount);
2328 
2329 	}
2330 
2331 	/**
2332 	 * This method is used to create a set of all possible combinations of
2333 	 * parameters across a set of search parameters. An example of why
2334 	 * this is needed:
2335 	 * <p>
2336 	 * Let's say we have a unique index on (Patient:gender AND Patient:name).
2337 	 * Then we pass in <code>SMITH, John</code> with a gender of <code>male</code>.
2338 	 * </p>
2339 	 * <p>
2340 	 * In this case, because the name parameter matches both first and last name,
2341 	 * we now need two unique indexes:
2342 	 * <ul>
2343 	 * <li>Patient?gender=male&amp;name=SMITH</li>
2344 	 * <li>Patient?gender=male&amp;name=JOHN</li>
2345 	 * </ul>
2346 	 * </p>
2347 	 * <p>
2348 	 * So this recursive algorithm calculates those
2349 	 * </p>
2350 	 *
2351 	 * @param theResourceType E.g. <code>Patient
2352 	 * @param thePartsChoices E.g. <code>[[gender=male], [name=SMITH, name=JOHN]]</code>
2353 	 */
2354 	public static Set<String> extractCompositeStringUniquesValueChains(String
2355 																								 theResourceType, List<List<String>> thePartsChoices) {
2356 
2357 		for (List<String> next : thePartsChoices) {
2358 			next.removeIf(StringUtils::isBlank);
2359 			if (next.isEmpty()) {
2360 				return Collections.emptySet();
2361 			}
2362 		}
2363 
2364 		if (thePartsChoices.isEmpty()) {
2365 			return Collections.emptySet();
2366 		}
2367 
2368 		thePartsChoices.sort((o1, o2) -> {
2369 			String str1 = null;
2370 			String str2 = null;
2371 			if (o1.size() > 0) {
2372 				str1 = o1.get(0);
2373 			}
2374 			if (o2.size() > 0) {
2375 				str2 = o2.get(0);
2376 			}
2377 			return compare(str1, str2);
2378 		});
2379 
2380 		List<String> values = new ArrayList<>();
2381 		Set<String> queryStringsToPopulate = new HashSet<>();
2382 		extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices, values, queryStringsToPopulate);
2383 		return queryStringsToPopulate;
2384 	}
2385 
2386 	private static void extractCompositeStringUniquesValueChains(String
2387 																						 theResourceType, List<List<String>> thePartsChoices, List<String> theValues, Set<String> theQueryStringsToPopulate) {
2388 		if (thePartsChoices.size() > 0) {
2389 			List<String> nextList = thePartsChoices.get(0);
2390 			Collections.sort(nextList);
2391 			for (String nextChoice : nextList) {
2392 				theValues.add(nextChoice);
2393 				extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices.subList(1, thePartsChoices.size()), theValues, theQueryStringsToPopulate);
2394 				theValues.remove(theValues.size() - 1);
2395 			}
2396 		} else {
2397 			if (theValues.size() > 0) {
2398 				StringBuilder uniqueString = new StringBuilder();
2399 				uniqueString.append(theResourceType);
2400 
2401 				for (int i = 0; i < theValues.size(); i++) {
2402 					uniqueString.append(i == 0 ? "?" : "&");
2403 					uniqueString.append(theValues.get(i));
2404 				}
2405 
2406 				theQueryStringsToPopulate.add(uniqueString.toString());
2407 			}
2408 		}
2409 	}
2410 
2411 	protected static boolean isValidPid(IIdType theId) {
2412 		if (theId == null || theId.getIdPart() == null) {
2413 			return false;
2414 		}
2415 		String idPart = theId.getIdPart();
2416 		for (int i = 0; i < idPart.length(); i++) {
2417 			char nextChar = idPart.charAt(i);
2418 			if (nextChar < '0' || nextChar > '9') {
2419 				return false;
2420 			}
2421 		}
2422 		return true;
2423 	}
2424 
2425 	@CoverageIgnore
2426 	protected static IQueryParameterAnd<?> newInstanceAnd(String chain) {
2427 		IQueryParameterAnd<?> type;
2428 		Class<? extends IQueryParameterAnd<?>> clazz = RESOURCE_META_AND_PARAMS.get(chain);
2429 		try {
2430 			type = clazz.newInstance();
2431 		} catch (Exception e) {
2432 			throw new InternalErrorException("Failure creating instance of " + clazz, e);
2433 		}
2434 		return type;
2435 	}
2436 
2437 	@CoverageIgnore
2438 	protected static IQueryParameterType newInstanceType(String chain) {
2439 		IQueryParameterType type;
2440 		Class<? extends IQueryParameterType> clazz = RESOURCE_META_PARAMS.get(chain);
2441 		try {
2442 			type = clazz.newInstance();
2443 		} catch (Exception e) {
2444 			throw new InternalErrorException("Failure creating instance of " + clazz, e);
2445 		}
2446 		return type;
2447 	}
2448 
2449 	public static String normalizeString(String theString) {
2450 		CharArrayWriter outBuffer = new CharArrayWriter(theString.length());
2451 
2452 		/*
2453 		 * The following block of code is used to strip out diacritical marks from latin script
2454 		 * and also convert to upper case. E.g. "j?mes" becomes "JAMES".
2455 		 *
2456 		 * See http://www.unicode.org/charts/PDF/U0300.pdf for the logic
2457 		 * behind stripping 0300-036F
2458 		 *
2459 		 * See #454 for an issue where we were completely stripping non latin characters
2460 		 * See #832 for an issue where we normalize korean characters, which are decomposed
2461 		 */
2462 		String string = Normalizer.normalize(theString, Normalizer.Form.NFD);
2463 		for (int i = 0, n = string.length(); i < n; ++i) {
2464 			char c = string.charAt(i);
2465 			if (c >= '\u0300' && c <= '\u036F') {
2466 				continue;
2467 			} else {
2468 				outBuffer.append(c);
2469 			}
2470 		}
2471 
2472 		return new String(outBuffer.toCharArray()).toUpperCase();
2473 	}
2474 
2475 	private static String parseNarrativeTextIntoWords(IBaseResource theResource) {
2476 
2477 		StringBuilder b = new StringBuilder();
2478 		if (theResource instanceof IResource) {
2479 			IResource resource = (IResource) theResource;
2480 			List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
2481 			if (xmlEvents != null) {
2482 				for (XMLEvent next : xmlEvents) {
2483 					if (next.isCharacters()) {
2484 						Characters characters = next.asCharacters();
2485 						b.append(characters.getData()).append(" ");
2486 					}
2487 				}
2488 			}
2489 		} else if (theResource instanceof IDomainResource) {
2490 			IDomainResource resource = (IDomainResource) theResource;
2491 			try {
2492 				String divAsString = resource.getText().getDivAsString();
2493 				List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
2494 				if (xmlEvents != null) {
2495 					for (XMLEvent next : xmlEvents) {
2496 						if (next.isCharacters()) {
2497 							Characters characters = next.asCharacters();
2498 							b.append(characters.getData()).append(" ");
2499 						}
2500 					}
2501 				}
2502 			} catch (Exception e) {
2503 				throw new DataFormatException("Unable to convert DIV to string", e);
2504 			}
2505 
2506 		}
2507 		return b.toString();
2508 	}
2509 
2510 	@VisibleForTesting
2511 	public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) {
2512 		ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest;
2513 	}
2514 
2515 	/**
2516 	 * Do not call this method outside of unit tests
2517 	 */
2518 	@VisibleForTesting
2519 	public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
2520 		ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
2521 	}
2522 
2523 	private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) {
2524 		ArrayList<BaseCodingDt> retVal = new ArrayList<>(theSecurityLabels.size());
2525 		for (IBaseCoding next : theSecurityLabels) {
2526 			retVal.add((BaseCodingDt) next);
2527 		}
2528 		return retVal;
2529 	}
2530 
2531 	protected static Long translateForcedIdToPid(String theResourceName, String theResourceId, IForcedIdDao
2532 		theForcedIdDao) {
2533 		return translateForcedIdToPids(new IdDt(theResourceName, theResourceId), theForcedIdDao).get(0);
2534 	}
2535 
2536 	static List<Long> translateForcedIdToPids(IIdType theId, IForcedIdDao theForcedIdDao) {
2537 		Validate.isTrue(theId.hasIdPart());
2538 
2539 		if (isValidPid(theId)) {
2540 			return Collections.singletonList(theId.getIdPartAsLong());
2541 		} else {
2542 			List<ForcedId> forcedId;
2543 			if (theId.hasResourceType()) {
2544 				forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart());
2545 			} else {
2546 				forcedId = theForcedIdDao.findByForcedId(theId.getIdPart());
2547 			}
2548 
2549 			if (forcedId.isEmpty() == false) {
2550 				List<Long> retVal = new ArrayList<>(forcedId.size());
2551 				for (ForcedId next : forcedId) {
2552 					retVal.add(next.getResourcePid());
2553 				}
2554 				return retVal;
2555 			} else {
2556 				throw new ResourceNotFoundException(theId);
2557 			}
2558 		}
2559 	}
2560 
2561 	public static SearchParameterMap translateMatchUrl(IDao theCallingDao, FhirContext theContext, String
2562 		theMatchUrl, RuntimeResourceDefinition resourceDef) {
2563 		SearchParameterMap paramMap = new SearchParameterMap();
2564 		List<NameValuePair> parameters = translateMatchUrl(theMatchUrl);
2565 
2566 		ArrayListMultimap<String, QualifiedParamList> nameToParamLists = ArrayListMultimap.create();
2567 		for (NameValuePair next : parameters) {
2568 			if (isBlank(next.getValue())) {
2569 				continue;
2570 			}
2571 
2572 			String paramName = next.getName();
2573 			String qualifier = null;
2574 			for (int i = 0; i < paramName.length(); i++) {
2575 				switch (paramName.charAt(i)) {
2576 					case '.':
2577 					case ':':
2578 						qualifier = paramName.substring(i);
2579 						paramName = paramName.substring(0, i);
2580 						i = Integer.MAX_VALUE - 1;
2581 						break;
2582 				}
2583 			}
2584 
2585 			QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue());
2586 			nameToParamLists.put(paramName, paramList);
2587 		}
2588 
2589 		for (String nextParamName : nameToParamLists.keySet()) {
2590 			List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
2591 			if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
2592 				if (paramList != null && paramList.size() > 0) {
2593 					if (paramList.size() > 2) {
2594 						throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions");
2595 					} else {
2596 						DateRangeParam p1 = new DateRangeParam();
2597 						p1.setValuesAsQueryTokens(theContext, nextParamName, paramList);
2598 						paramMap.setLastUpdated(p1);
2599 					}
2600 				}
2601 				continue;
2602 			}
2603 
2604 			if (Constants.PARAM_HAS.equals(nextParamName)) {
2605 				IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(theContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList);
2606 				paramMap.add(nextParamName, param);
2607 				continue;
2608 			}
2609 
2610 			if (Constants.PARAM_COUNT.equals(nextParamName)) {
2611 				if (paramList.size() > 0 && paramList.get(0).size() > 0) {
2612 					String intString = paramList.get(0).get(0);
2613 					try {
2614 						paramMap.setCount(Integer.parseInt(intString));
2615 					} catch (NumberFormatException e) {
2616 						throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString);
2617 					}
2618 				}
2619 				continue;
2620 			}
2621 
2622 			if (RESOURCE_META_PARAMS.containsKey(nextParamName)) {
2623 				if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) {
2624 					throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier());
2625 				}
2626 				IQueryParameterAnd<?> type = newInstanceAnd(nextParamName);
2627 				type.setValuesAsQueryTokens(theContext, nextParamName, (paramList));
2628 				paramMap.add(nextParamName, type);
2629 			} else if (nextParamName.startsWith("_")) {
2630 				// ignore these since they aren't search params (e.g. _sort)
2631 			} else {
2632 				RuntimeSearchParam paramDef = theCallingDao.getSearchParamByName(resourceDef, nextParamName);
2633 				if (paramDef == null) {
2634 					throw new InvalidRequestException(
2635 						"Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName);
2636 				}
2637 
2638 				IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(theContext, paramDef, nextParamName, paramList);
2639 				paramMap.add(nextParamName, param);
2640 			}
2641 		}
2642 		return paramMap;
2643 	}
2644 
2645 	public static List<NameValuePair> translateMatchUrl(String theMatchUrl) {
2646 		List<NameValuePair> parameters;
2647 		String matchUrl = theMatchUrl;
2648 		int questionMarkIndex = matchUrl.indexOf('?');
2649 		if (questionMarkIndex != -1) {
2650 			matchUrl = matchUrl.substring(questionMarkIndex + 1);
2651 		}
2652 		matchUrl = matchUrl.replace("|", "%7C");
2653 		matchUrl = matchUrl.replace("=>=", "=%3E%3D");
2654 		matchUrl = matchUrl.replace("=<=", "=%3C%3D");
2655 		matchUrl = matchUrl.replace("=>", "=%3E");
2656 		matchUrl = matchUrl.replace("=<", "=%3C");
2657 		if (matchUrl.contains(" ")) {
2658 			throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)");
2659 		}
2660 
2661 		parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&');
2662 		return parameters;
2663 	}
2664 
2665 	public static void validateResourceType(BaseHasResource theEntity, String theResourceName) {
2666 		if (!theResourceName.equals(theEntity.getResourceType())) {
2667 			throw new ResourceNotFoundException(
2668 				"Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType());
2669 		}
2670 	}
2671 
2672 }