View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   import ca.uhn.fhir.context.*;
4   import ca.uhn.fhir.jpa.dao.data.*;
5   import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
6   import ca.uhn.fhir.jpa.dao.index.IdHelperService;
7   import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
8   import ca.uhn.fhir.jpa.entity.*;
9   import ca.uhn.fhir.jpa.model.entity.*;
10  import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
11  import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
12  import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
13  import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
14  import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
15  import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
16  import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper;
17  import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
18  import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
19  import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
20  import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
21  import ca.uhn.fhir.jpa.util.DeleteConflict;
22  import ca.uhn.fhir.jpa.util.ExpungeOptions;
23  import ca.uhn.fhir.jpa.util.ExpungeOutcome;
24  import ca.uhn.fhir.jpa.util.JpaConstants;
25  import ca.uhn.fhir.model.api.IResource;
26  import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
27  import ca.uhn.fhir.model.api.Tag;
28  import ca.uhn.fhir.model.api.TagList;
29  import ca.uhn.fhir.model.base.composite.BaseCodingDt;
30  import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
31  import ca.uhn.fhir.model.primitive.IdDt;
32  import ca.uhn.fhir.model.primitive.InstantDt;
33  import ca.uhn.fhir.model.primitive.StringDt;
34  import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
35  import ca.uhn.fhir.parser.DataFormatException;
36  import ca.uhn.fhir.parser.IParser;
37  import ca.uhn.fhir.parser.LenientErrorHandler;
38  import ca.uhn.fhir.rest.api.Constants;
39  import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
40  import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
41  import ca.uhn.fhir.rest.api.server.IBundleProvider;
42  import ca.uhn.fhir.rest.api.server.RequestDetails;
43  import ca.uhn.fhir.rest.server.exceptions.*;
44  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
45  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
46  import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
47  import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
48  import ca.uhn.fhir.util.CoverageIgnore;
49  import ca.uhn.fhir.util.OperationOutcomeUtil;
50  import ca.uhn.fhir.util.StopWatch;
51  import ca.uhn.fhir.util.XmlUtil;
52  import com.google.common.annotations.VisibleForTesting;
53  import com.google.common.base.Charsets;
54  import com.google.common.collect.Lists;
55  import com.google.common.collect.Sets;
56  import com.google.common.hash.HashFunction;
57  import com.google.common.hash.Hashing;
58  import org.apache.commons.lang3.NotImplementedException;
59  import org.apache.commons.lang3.Validate;
60  import org.hibernate.Session;
61  import org.hibernate.internal.SessionImpl;
62  import org.hl7.fhir.instance.model.api.*;
63  import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
64  import org.springframework.beans.BeansException;
65  import org.springframework.beans.factory.annotation.Autowired;
66  import org.springframework.context.ApplicationContext;
67  import org.springframework.context.ApplicationContextAware;
68  import org.springframework.data.domain.PageRequest;
69  import org.springframework.data.domain.Pageable;
70  import org.springframework.data.domain.Slice;
71  import org.springframework.data.domain.SliceImpl;
72  import org.springframework.stereotype.Repository;
73  import org.springframework.transaction.PlatformTransactionManager;
74  import org.springframework.transaction.TransactionDefinition;
75  import org.springframework.transaction.support.TransactionSynchronizationAdapter;
76  import org.springframework.transaction.support.TransactionSynchronizationManager;
77  import org.springframework.transaction.support.TransactionTemplate;
78  
79  import javax.persistence.*;
80  import javax.persistence.criteria.CriteriaBuilder;
81  import javax.persistence.criteria.CriteriaQuery;
82  import javax.persistence.criteria.Predicate;
83  import javax.persistence.criteria.Root;
84  import javax.xml.stream.events.Characters;
85  import javax.xml.stream.events.XMLEvent;
86  import java.util.*;
87  import java.util.Map.Entry;
88  import java.util.concurrent.atomic.AtomicInteger;
89  
90  import static org.apache.commons.lang3.StringUtils.*;
91  
92  /*
93   * #%L
94   * HAPI FHIR JPA Server
95   * %%
96   * Copyright (C) 2014 - 2019 University Health Network
97   * %%
98   * Licensed under the Apache License, Version 2.0 (the "License");
99   * you may not use this file except in compliance with the License.
100  * You may obtain a copy of the License at
101  * 
102  * http://www.apache.org/licenses/LICENSE-2.0
103  * 
104  * Unless required by applicable law or agreed to in writing, software
105  * distributed under the License is distributed on an "AS IS" BASIS,
106  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
107  * See the License for the specific language governing permissions and
108  * limitations under the License.
109  * #L%
110  */
111 
112 @SuppressWarnings("WeakerAccess")
113 @Repository
114 public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ApplicationContextAware {
115 
116 	public static final long INDEX_STATUS_INDEXED = 1L;
117 	public static final long INDEX_STATUS_INDEXING_FAILED = 2L;
118 	public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile";
119 	public static final String OO_SEVERITY_ERROR = "error";
120 	public static final String OO_SEVERITY_INFO = "information";
121 	public static final String OO_SEVERITY_WARN = "warning";
122 	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class);
123 	private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<>();
124 	private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest";
125 	private static boolean ourValidationDisabledForUnitTest;
126 	private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
127 
128 	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
129 	protected EntityManager myEntityManager;
130 	@Autowired
131 	protected IdHelperService myIdHelperService;
132 	@Autowired
133 	protected IInterceptorBroadcaster myInterceptorBroadcaster;
134 	@Autowired
135 	protected IForcedIdDao myForcedIdDao;
136 	@Autowired
137 	protected ISearchResultDao mySearchResultDao;
138 	@Autowired(required = false)
139 	protected IFulltextSearchSvc myFulltextSearchSvc;
140 	@Autowired()
141 	protected IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
142 	@Autowired()
143 	protected IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao;
144 	@Autowired()
145 	protected IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao;
146 	@Autowired
147 	protected IResourceLinkDao myResourceLinkDao;
148 	@Autowired()
149 	protected IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao;
150 	@Autowired()
151 	protected IResourceIndexedSearchParamQuantityDao myResourceIndexedSearchParamQuantityDao;
152 	@Autowired()
153 	protected IResourceIndexedSearchParamCoordsDao myResourceIndexedSearchParamCoordsDao;
154 	@Autowired()
155 	protected IResourceIndexedSearchParamNumberDao myResourceIndexedSearchParamNumberDao;
156 	@Autowired
157 	protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
158 	@Autowired
159 	protected ISearchParamRegistry mySerarchParamRegistry;
160 	@Autowired()
161 	protected IHapiTerminologySvc myTerminologySvc;
162 	@Autowired
163 	protected IResourceHistoryTableDao myResourceHistoryTableDao;
164 	@Autowired
165 	protected IResourceHistoryTagDao myResourceHistoryTagDao;
166 	@Autowired
167 	protected IResourceTableDao myResourceTableDao;
168 	@Autowired
169 	protected IResourceTagDao myResourceTagDao;
170 	@Autowired
171 	protected IResourceSearchViewDao myResourceViewDao;
172 	@Autowired
173 	protected ISearchParamRegistry mySearchParamRegistry;
174 	@Autowired(required = true)
175 	private DaoConfig myConfig;
176 	private FhirContext myContext;
177 	@Autowired
178 	private PlatformTransactionManager myPlatformTransactionManager;
179 	@Autowired
180 	private ISearchDao mySearchDao;
181 	@Autowired
182 	private ISearchParamPresenceSvc mySearchParamPresenceSvc;
183 	//@Autowired
184 	//private ISearchResultDao mySearchResultDao;
185 	@Autowired
186 	private DaoRegistry myDaoRegistry;
187 	@Autowired
188 	private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor;
189 	@Autowired
190 	private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
191 	@Autowired
192 	private SearchBuilderFactory mySearchBuilderFactory;
193 
194 	private ApplicationContext myApplicationContext;
195 
196 	/**
197 	 * Returns the newly created forced ID. If the entity already had a forced ID, or if
198 	 * none was created, returns null.
199 	 */
200 	protected ForcedId createForcedIdIfNeeded(ResourceTable theEntity, IIdType theId, boolean theCreateForPureNumericIds) {
201 		if (theId.isEmpty() == false && theId.hasIdPart() && theEntity.getForcedId() == null) {
202 			if (!theCreateForPureNumericIds && IdHelperService.isValidPid(theId)) {
203 				return null;
204 			}
205 
206 			ForcedId fid = new ForcedId();
207 			fid.setResourceType(theEntity.getResourceType());
208 			fid.setForcedId(theId.getIdPart());
209 			fid.setResource(theEntity);
210 			theEntity.setForcedId(fid);
211 			return fid;
212 		}
213 
214 		return null;
215 	}
216 
217 	protected ExpungeOutcome doExpunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions) {
218 		TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
219 		txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
220 		ourLog.info("Expunge: ResourceName[{}] Id[{}] Version[{}] Options[{}]", theResourceName, theResourceId, theVersion, theExpungeOptions);
221 
222 		if (!getConfig().isExpungeEnabled()) {
223 			throw new MethodNotAllowedException("$expunge is not enabled on this server");
224 		}
225 
226 		AtomicInteger remainingCount = new AtomicInteger(theExpungeOptions.getLimit());
227 
228 		if (theResourceName == null && theResourceId == null && theVersion == null) {
229 			if (theExpungeOptions.isExpungeEverything()) {
230 				doExpungeEverything();
231 			}
232 		}
233 
234 		if (theExpungeOptions.isExpungeDeletedResources() && theVersion == null) {
235 
236 			/*
237 			 * Delete historical versions of deleted resources
238 			 */
239 			Pageable page = PageRequest.of(0, remainingCount.get());
240 			Slice<Long> resourceIds = txTemplate.execute(t -> {
241 				if (theResourceId != null) {
242 					Slice<Long> ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceId, theResourceName);
243 					ourLog.info("Expunging {} deleted resources of type[{}] and ID[{}]", ids.getNumberOfElements(), theResourceName, theResourceId);
244 					return ids;
245 				} else {
246 					if (theResourceName != null) {
247 						Slice<Long> ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceName);
248 						ourLog.info("Expunging {} deleted resources of type[{}]", ids.getNumberOfElements(), theResourceName);
249 						return ids;
250 					} else {
251 						Slice<Long> ids = myResourceTableDao.findIdsOfDeletedResources(page);
252 						ourLog.info("Expunging {} deleted resources (all types)", ids.getNumberOfElements(), theResourceName);
253 						return ids;
254 					}
255 				}
256 			});
257 
258 			/*
259 			 * Delete any search result cache entries pointing to the given resource. We do
260 			 * this in batches to avoid sending giant batches of parameters to the DB
261 			 */
262 			List<List<Long>> partitions = Lists.partition(resourceIds.getContent(), 800);
263 			for (List<Long> nextPartition : partitions) {
264 				ourLog.info("Expunging any search results pointing to {} resources", nextPartition.size());
265 				txTemplate.execute(t -> {
266 					mySearchResultDao.deleteByResourceIds(nextPartition);
267 					return null;
268 				});
269 			}
270 
271 			/*
272 			 * Delete historical versions
273 			 */
274 			for (Long next : resourceIds) {
275 				txTemplate.execute(t -> {
276 					expungeHistoricalVersionsOfId(next, remainingCount);
277 					if (remainingCount.get() <= 0) {
278 						ourLog.debug("Expunge limit has been hit - Stopping operation");
279 						return toExpungeOutcome(theExpungeOptions, remainingCount);
280 					}
281 					return null;
282 				});
283 			}
284 
285 			/*
286 			 * Delete current versions of deleted resources
287 			 */
288 			for (Long next : resourceIds) {
289 				txTemplate.execute(t -> {
290 					expungeCurrentVersionOfResource(next, remainingCount);
291 					return null;
292 				});
293 			}
294 
295 		}
296 
297 		if (theExpungeOptions.isExpungeOldVersions()) {
298 
299 			/*
300 			 * Delete historical versions of non-deleted resources
301 			 */
302 			Pageable page = PageRequest.of(0, remainingCount.get());
303 			Slice<Long> historicalIds = txTemplate.execute(t -> {
304 				if (theResourceId != null && theVersion != null) {
305 					return toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion));
306 				} else {
307 					if (theResourceName != null) {
308 						return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName);
309 					} else {
310 						return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page);
311 					}
312 				}
313 			});
314 
315 			for (Long next : historicalIds) {
316 				txTemplate.execute(t -> {
317 					expungeHistoricalVersion(next);
318 					if (remainingCount.decrementAndGet() <= 0) {
319 						return toExpungeOutcome(theExpungeOptions, remainingCount);
320 					}
321 					return null;
322 				});
323 			}
324 
325 		}
326 		return toExpungeOutcome(theExpungeOptions, remainingCount);
327 	}
328 
329 	private void doExpungeEverything() {
330 
331 		final AtomicInteger counter = new AtomicInteger();
332 
333 		ourLog.info("BEGINNING GLOBAL $expunge");
334 		TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
335 		txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
336 		txTemplate.execute(t -> {
337 			counter.addAndGet(doExpungeEverythingQuery("UPDATE " + ResourceHistoryTable.class.getSimpleName() + " d SET d.myForcedId = null"));
338 			counter.addAndGet(doExpungeEverythingQuery("UPDATE " + ResourceTable.class.getSimpleName() + " d SET d.myForcedId = null"));
339 			counter.addAndGet(doExpungeEverythingQuery("UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null"));
340 			return null;
341 		});
342 		txTemplate.execute(t -> {
343 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + SearchParamPresent.class.getSimpleName() + " d"));
344 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ForcedId.class.getSimpleName() + " d"));
345 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamDate.class.getSimpleName() + " d"));
346 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamNumber.class.getSimpleName() + " d"));
347 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamQuantity.class.getSimpleName() + " d"));
348 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamString.class.getSimpleName() + " d"));
349 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamToken.class.getSimpleName() + " d"));
350 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamUri.class.getSimpleName() + " d"));
351 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamCoords.class.getSimpleName() + " d"));
352 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceIndexedCompositeStringUnique.class.getSimpleName() + " d"));
353 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceLink.class.getSimpleName() + " d"));
354 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + SearchResult.class.getSimpleName() + " d"));
355 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + SearchInclude.class.getSimpleName() + " d"));
356 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConceptParentChildLink.class.getSimpleName() + " d"));
357 			return null;
358 		});
359 		txTemplate.execute(t -> {
360 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConceptMapGroupElementTarget.class.getSimpleName() + " d"));
361 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConceptMapGroupElement.class.getSimpleName() + " d"));
362 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConceptMapGroup.class.getSimpleName() + " d"));
363 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConceptMap.class.getSimpleName() + " d"));
364 			return null;
365 		});
366 		txTemplate.execute(t -> {
367 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConceptProperty.class.getSimpleName() + " d"));
368 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConceptDesignation.class.getSimpleName() + " d"));
369 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermConcept.class.getSimpleName() + " d"));
370 			for (TermCodeSystem next : myEntityManager.createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class).getResultList()) {
371 				next.setCurrentVersion(null);
372 				myEntityManager.merge(next);
373 			}
374 			return null;
375 		});
376 		txTemplate.execute(t -> {
377 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermCodeSystemVersion.class.getSimpleName() + " d"));
378 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TermCodeSystem.class.getSimpleName() + " d"));
379 			return null;
380 		});
381 		txTemplate.execute(t -> {
382 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + SubscriptionTable.class.getSimpleName() + " d"));
383 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceHistoryTag.class.getSimpleName() + " d"));
384 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceTag.class.getSimpleName() + " d"));
385 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + TagDefinition.class.getSimpleName() + " d"));
386 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceHistoryTable.class.getSimpleName() + " d"));
387 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + ResourceTable.class.getSimpleName() + " d"));
388 			counter.addAndGet(doExpungeEverythingQuery("DELETE from " + org.hibernate.search.jpa.Search.class.getSimpleName() + " d"));
389 			return null;
390 		});
391 
392 		ourLog.info("COMPLETED GLOBAL $expunge - Deleted {} rows", counter.get());
393 	}
394 
395 	private int doExpungeEverythingQuery(String theQuery) {
396 		StopWatch sw = new StopWatch();
397 		int outcome = myEntityManager.createQuery(theQuery).executeUpdate();
398 		if (outcome > 0) {
399 			ourLog.debug("Query affected {} rows in {}: {}", outcome, sw.toString(), theQuery);
400 		} else {
401 			ourLog.debug("Query affected {} rows in {}: {}", outcome, sw.toString(), theQuery);
402 		}
403 		return outcome;
404 	}
405 
406 	private void expungeCurrentVersionOfResource(Long theResourceId, AtomicInteger theRemainingCount) {
407 		ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new);
408 
409 		ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion());
410 		if (currentVersion != null) {
411 			expungeHistoricalVersion(currentVersion.getId());
412 		}
413 
414 		ourLog.info("Expunging current version of resource {}", resource.getIdDt().getValue());
415 
416 		myResourceIndexedSearchParamUriDao.deleteAll(resource.getParamsUri());
417 		myResourceIndexedSearchParamCoordsDao.deleteAll(resource.getParamsCoords());
418 		myResourceIndexedSearchParamDateDao.deleteAll(resource.getParamsDate());
419 		myResourceIndexedSearchParamNumberDao.deleteAll(resource.getParamsNumber());
420 		myResourceIndexedSearchParamQuantityDao.deleteAll(resource.getParamsQuantity());
421 		myResourceIndexedSearchParamStringDao.deleteAll(resource.getParamsString());
422 		myResourceIndexedSearchParamTokenDao.deleteAll(resource.getParamsToken());
423 		myResourceLinkDao.deleteAll(resource.getResourceLinks());
424 		myResourceLinkDao.deleteAll(resource.getResourceLinksAsTarget());
425 
426 		myResourceTagDao.deleteAll(resource.getTags());
427 		resource.getTags().clear();
428 
429 		if (resource.getForcedId() != null) {
430 			ForcedId forcedId = resource.getForcedId();
431 			resource.setForcedId(null);
432 			myResourceTableDao.saveAndFlush(resource);
433 			myIdHelperService.delete(forcedId);
434 		}
435 
436 		myResourceTableDao.delete(resource);
437 
438 		theRemainingCount.decrementAndGet();
439 	}
440 
441 	protected void expungeHistoricalVersion(Long theNextVersionId) {
442 		ResourceHistoryTable version = myResourceHistoryTableDao.findById(theNextVersionId).orElseThrow(IllegalArgumentException::new);
443 		ourLog.info("Deleting resource version {}", version.getIdDt().getValue());
444 
445 		myResourceHistoryTagDao.deleteAll(version.getTags());
446 		myResourceHistoryTableDao.delete(version);
447 	}
448 
449 	protected void expungeHistoricalVersionsOfId(Long theResourceId, AtomicInteger theRemainingCount) {
450 		ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalArgumentException::new);
451 
452 		Pageable page = PageRequest.of(0, theRemainingCount.get());
453 
454 		Slice<Long> versionIds = myResourceHistoryTableDao.findForResourceId(page, resource.getId(), resource.getVersion());
455 		ourLog.debug("Found {} versions of resource {} to expunge", versionIds.getNumberOfElements(), resource.getIdDt().getValue());
456 		for (Long nextVersionId : versionIds) {
457 			expungeHistoricalVersion(nextVersionId);
458 			if (theRemainingCount.decrementAndGet() <= 0) {
459 				return;
460 			}
461 		}
462 	}
463 
464 	private void extractTagsHapi(IResource theResource, ResourceTable theEntity, Set<ResourceTag> allDefs) {
465 		TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource);
466 		if (tagList != null) {
467 			for (Tag next : tagList) {
468 				TagDefinition def = getTagOrNull(TagTypeEnum.TAG, next.getScheme(), next.getTerm(), next.getLabel());
469 				if (def != null) {
470 					ResourceTag tag = theEntity.addTag(def);
471 					allDefs.add(tag);
472 					theEntity.setHasTags(true);
473 				}
474 			}
475 		}
476 
477 		List<BaseCodingDt> securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource);
478 		if (securityLabels != null) {
479 			for (BaseCodingDt next : securityLabels) {
480 				TagDefinition def = getTagOrNull(TagTypeEnum.SECURITY_LABEL, next.getSystemElement().getValue(), next.getCodeElement().getValue(), next.getDisplayElement().getValue());
481 				if (def != null) {
482 					ResourceTag tag = theEntity.addTag(def);
483 					allDefs.add(tag);
484 					theEntity.setHasTags(true);
485 				}
486 			}
487 		}
488 
489 		List<IdDt> profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource);
490 		if (profiles != null) {
491 			for (IIdType next : profiles) {
492 				TagDefinition def = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
493 				if (def != null) {
494 					ResourceTag tag = theEntity.addTag(def);
495 					allDefs.add(tag);
496 					theEntity.setHasTags(true);
497 				}
498 			}
499 		}
500 	}
501 
502 	private void extractTagsRi(IAnyResource theResource, ResourceTable theEntity, Set<ResourceTag> theAllTags) {
503 		List<? extends IBaseCoding> tagList = theResource.getMeta().getTag();
504 		if (tagList != null) {
505 			for (IBaseCoding next : tagList) {
506 				TagDefinition def = getTagOrNull(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay());
507 				if (def != null) {
508 					ResourceTag tag = theEntity.addTag(def);
509 					theAllTags.add(tag);
510 					theEntity.setHasTags(true);
511 				}
512 			}
513 		}
514 
515 		List<? extends IBaseCoding> securityLabels = theResource.getMeta().getSecurity();
516 		if (securityLabels != null) {
517 			for (IBaseCoding next : securityLabels) {
518 				TagDefinition def = getTagOrNull(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay());
519 				if (def != null) {
520 					ResourceTag tag = theEntity.addTag(def);
521 					theAllTags.add(tag);
522 					theEntity.setHasTags(true);
523 				}
524 			}
525 		}
526 
527 		List<? extends IPrimitiveType<String>> profiles = theResource.getMeta().getProfile();
528 		if (profiles != null) {
529 			for (IPrimitiveType<String> next : profiles) {
530 				TagDefinition def = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
531 				if (def != null) {
532 					ResourceTag tag = theEntity.addTag(def);
533 					theAllTags.add(tag);
534 					theEntity.setHasTags(true);
535 				}
536 			}
537 		}
538 	}
539 
540 	private void findMatchingTagIds(String theResourceName, IIdType theResourceId, Set<Long> tagIds, Class<? extends BaseTag> entityClass) {
541 		{
542 			CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
543 			CriteriaQuery<Tuple> cq = builder.createTupleQuery();
544 			Root<? extends BaseTag> from = cq.from(entityClass);
545 			cq.multiselect(from.get("myTagId").as(Long.class)).distinct(true);
546 
547 			if (theResourceName != null) {
548 				Predicate typePredicate = builder.equal(from.get("myResourceType"), theResourceName);
549 				if (theResourceId != null) {
550 					cq.where(typePredicate, builder.equal(from.get("myResourceId"), myIdHelperService.translateForcedIdToPid(theResourceName, theResourceId.getIdPart())));
551 				} else {
552 					cq.where(typePredicate);
553 				}
554 			}
555 
556 			TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
557 			for (Tuple next : query.getResultList()) {
558 				tagIds.add(next.get(0, Long.class));
559 			}
560 		}
561 	}
562 
563 	protected void flushJpaSession() {
564 		SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class);
565 		int insertionCount = session.getActionQueue().numberOfInsertions();
566 		int updateCount = session.getActionQueue().numberOfUpdates();
567 
568 		StopWatch sw = new StopWatch();
569 		myEntityManager.flush();
570 		ourLog.debug("Session flush took {}ms for {} inserts and {} updates", sw.getMillis(), insertionCount, updateCount);
571 	}
572 
573 	private Set<ResourceTag> getAllTagDefinitions(ResourceTable theEntity) {
574 		HashSet<ResourceTag> retVal = Sets.newHashSet();
575 		if (theEntity.isHasTags()) {
576 			for (ResourceTag next : theEntity.getTags()) {
577 				retVal.add(next);
578 			}
579 		}
580 		return retVal;
581 	}
582 
583 	protected DaoConfig getConfig() {
584 		return myConfig;
585 	}
586 
587 	public void setConfig(DaoConfig theConfig) {
588 		myConfig = theConfig;
589 	}
590 
591 	@Override
592 	public FhirContext getContext() {
593 		return myContext;
594 	}
595 
596 	@Autowired
597 	public void setContext(FhirContext theContext) {
598 		myContext = theContext;
599 	}
600 
601 	public FhirContext getContext(FhirVersionEnum theVersion) {
602 		Validate.notNull(theVersion, "theVersion must not be null");
603 		synchronized (ourRetrievalContexts) {
604 			FhirContext retVal = ourRetrievalContexts.get(theVersion);
605 			if (retVal == null) {
606 				retVal = new FhirContext(theVersion);
607 				ourRetrievalContexts.put(theVersion, retVal);
608 			}
609 			return retVal;
610 		}
611 	}
612 
613 	@SuppressWarnings("unchecked")
614 	public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
615 		return myDaoRegistry.getResourceDaoIfExists(theType);
616 	}
617 
618 	protected IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
619 		return myDaoRegistry.getDaoOrThrowException(theClass);
620 	}
621 
622 	protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
623 		if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) {
624 			return null;
625 		}
626 
627 		CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
628 		CriteriaQuery<TagDefinition> cq = builder.createQuery(TagDefinition.class);
629 		Root<TagDefinition> from = cq.from(TagDefinition.class);
630 
631 		if (isNotBlank(theScheme)) {
632 			cq.where(
633 				builder.and(
634 					builder.equal(from.get("myTagType"), theTagType),
635 					builder.equal(from.get("mySystem"), theScheme),
636 					builder.equal(from.get("myCode"), theTerm)));
637 		} else {
638 			cq.where(
639 				builder.and(
640 					builder.equal(from.get("myTagType"), theTagType),
641 					builder.isNull(from.get("mySystem")),
642 					builder.equal(from.get("myCode"), theTerm)));
643 		}
644 
645 		TypedQuery<TagDefinition> q = myEntityManager.createQuery(cq);
646 		try {
647 			return q.getSingleResult();
648 		} catch (NoResultException e) {
649 			TagDefinition retVal = new TagDefinition(theTagType, theScheme, theTerm, theLabel);
650 			myEntityManager.persist(retVal);
651 			return retVal;
652 		}
653 	}
654 
655 	protected TagList getTags(Class<? extends IBaseResource> theResourceType, IIdType theResourceId) {
656 		String resourceName = null;
657 		if (theResourceType != null) {
658 			resourceName = toResourceName(theResourceType);
659 			if (theResourceId != null && theResourceId.hasVersionIdPart()) {
660 				IFhirResourceDao<? extends IBaseResource> dao = getDao(theResourceType);
661 				BaseHasResource entity = dao.readEntity(theResourceId);
662 				TagList retVal = new TagList();
663 				for (BaseTag next : entity.getTags()) {
664 					retVal.add(next.getTag().toTag());
665 				}
666 				return retVal;
667 			}
668 		}
669 
670 		Set<Long> tagIds = new HashSet<>();
671 		findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceTag.class);
672 		findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceHistoryTag.class);
673 		if (tagIds.isEmpty()) {
674 			return new TagList();
675 		}
676 		{
677 			CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
678 			CriteriaQuery<TagDefinition> cq = builder.createQuery(TagDefinition.class);
679 			Root<TagDefinition> from = cq.from(TagDefinition.class);
680 			cq.where(from.get("myId").in(tagIds));
681 			cq.orderBy(builder.asc(from.get("mySystem")), builder.asc(from.get("myCode")));
682 			TypedQuery<TagDefinition> q = myEntityManager.createQuery(cq);
683 			q.setMaxResults(getConfig().getHardTagListLimit());
684 
685 			TagList retVal = new TagList();
686 			for (TagDefinition next : q.getResultList()) {
687 				retVal.add(next.toTag());
688 			}
689 
690 			return retVal;
691 		}
692 	}
693 
694 	protected IBundleProvider history(String theResourceName, Long theId, Date theSince, Date theUntil) {
695 
696 		String resourceName = defaultIfBlank(theResourceName, null);
697 
698 		Search search = new Search();
699 		search.setDeleted(false);
700 		search.setCreated(new Date());
701 		search.setSearchLastReturned(new Date());
702 		search.setLastUpdated(theSince, theUntil);
703 		search.setUuid(UUID.randomUUID().toString());
704 		search.setResourceType(resourceName);
705 		search.setResourceId(theId);
706 		search.setSearchType(SearchTypeEnum.HISTORY);
707 		search.setStatus(SearchStatusEnum.FINISHED);
708 
709 		if (theSince != null) {
710 			if (resourceName == null) {
711 				search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes(theSince));
712 			} else if (theId == null) {
713 				search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName, theSince));
714 			} else {
715 				search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId, theSince));
716 			}
717 		} else {
718 			if (resourceName == null) {
719 				search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes());
720 			} else if (theId == null) {
721 				search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName));
722 			} else {
723 				search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId));
724 			}
725 		}
726 
727 		search = mySearchDao.save(search);
728 
729 		return new PersistedJpaBundleProvider(search.getUuid(), this);
730 	}
731 
732 	void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) {
733 		String newVersion;
734 		long newVersionLong;
735 		if (theResourceId == null || theResourceId.getVersionIdPart() == null) {
736 			newVersion = "1";
737 			newVersionLong = 1;
738 		} else {
739 			newVersionLong = theResourceId.getVersionIdPartAsLong() + 1;
740 			newVersion = Long.toString(newVersionLong);
741 		}
742 
743 		IIdType newId = theResourceId.withVersion(newVersion);
744 		theResource.getIdElement().setValue(newId.getValue());
745 		theSavedEntity.setVersion(newVersionLong);
746 	}
747 
748 	@Override
749 	public void injectDependenciesIntoBundleProvider(PersistedJpaBundleProvider theProvider) {
750 		theProvider.setContext(getContext());
751 		theProvider.setEntityManager(myEntityManager);
752 		theProvider.setPlatformTransactionManager(myPlatformTransactionManager);
753 		theProvider.setSearchDao(mySearchDao);
754 		theProvider.setSearchCoordinatorSvc(mySearchCoordinatorSvc);
755 	}
756 
757 	public boolean isLogicalReference(IIdType theId) {
758 		return LogicalReferenceHelper.isLogicalReference(myConfig.getModelConfig(), theId);
759 	}
760 
761 	// TODO KHS inject a searchBuilderFactory into callers of this method and delete this method
762 	@Override
763 	public SearchBuilder newSearchBuilder() {
764 		return mySearchBuilderFactory.newSearchBuilder(this);
765 	}
766 
767 	public void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) {
768 		if (theRequestDetails.getId() != null && theRequestDetails.getId().hasResourceType() && isNotBlank(theRequestDetails.getResourceType())) {
769 			if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) {
770 				throw new InternalErrorException(
771 					"Inconsistent server state - Resource types don't match: " + theRequestDetails.getId().getResourceType() + " / " + theRequestDetails.getResourceType());
772 			}
773 		}
774 
775 		if (theRequestDetails.getUserData().get(PROCESSING_SUB_REQUEST) == Boolean.TRUE) {
776 			theRequestDetails.notifyIncomingRequestPreHandled(theOperationType);
777 		}
778 		List<IServerInterceptor> interceptors = getConfig().getInterceptors();
779 		for (IServerInterceptor next : interceptors) {
780 			next.incomingRequestPreHandled(theOperationType, theRequestDetails);
781 		}
782 	}
783 
784 	/**
785 	 * Returns true if the resource has changed (either the contents or the tags)
786 	 */
787 	protected EncodedResource populateResourceIntoEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, boolean theUpdateHash) {
788 		if (theEntity.getResourceType() == null) {
789 			theEntity.setResourceType(toResourceName(theResource));
790 		}
791 
792 		if (theResource != null) {
793 			List<BaseResourceReferenceDt> refs = myContext.newTerser().getAllPopulatedChildElementsOfType(theResource, BaseResourceReferenceDt.class);
794 			for (BaseResourceReferenceDt nextRef : refs) {
795 				if (nextRef.getReference().isEmpty() == false) {
796 					if (nextRef.getReference().hasVersionIdPart()) {
797 						nextRef.setReference(nextRef.getReference().toUnqualifiedVersionless());
798 					}
799 				}
800 			}
801 		}
802 
803 		byte[] bytes;
804 		ResourceEncodingEnum encoding;
805 		boolean changed = false;
806 
807 		if (theEntity.getDeleted() == null) {
808 
809 			encoding = myConfig.getResourceEncoding();
810 			Set<String> excludeElements = ResourceMetaParams.EXCLUDE_ELEMENTS_IN_ENCODED;
811 			theEntity.setFhirVersion(myContext.getVersion().getVersion());
812 
813 			bytes = encodeResource(theResource, encoding, excludeElements, myContext);
814 
815 			if (theUpdateHash) {
816 				HashFunction sha256 = Hashing.sha256();
817 				String hashSha256 = sha256.hashBytes(bytes).toString();
818 				if (hashSha256.equals(theEntity.getHashSha256()) == false) {
819 					changed = true;
820 				}
821 				theEntity.setHashSha256(hashSha256);
822 			}
823 
824 			Set<ResourceTag> allDefs = new HashSet<>();
825 			Set<ResourceTag> allTagsOld = getAllTagDefinitions(theEntity);
826 
827 			if (theResource instanceof IResource) {
828 				extractTagsHapi((IResource) theResource, theEntity, allDefs);
829 			} else {
830 				extractTagsRi((IAnyResource) theResource, theEntity, allDefs);
831 			}
832 
833 			RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
834 			if (def.isStandardType() == false) {
835 				String profile = def.getResourceProfile("");
836 				if (isNotBlank(profile)) {
837 					TagDefinition profileDef = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, profile, null);
838 					if (def != null) {
839 						ResourceTag tag = theEntity.addTag(profileDef);
840 						allDefs.add(tag);
841 						theEntity.setHasTags(true);
842 					}
843 				}
844 			}
845 
846 			Set<ResourceTag> allTagsNew = getAllTagDefinitions(theEntity);
847 			Set<TagDefinition> allDefsPresent = new HashSet<>();
848 			allTagsNew.forEach(tag -> {
849 
850 				// Don't keep duplicate tags
851 				if (!allDefsPresent.add(tag.getTag())) {
852 					theEntity.getTags().remove(tag);
853 				}
854 
855 				// Drop any tags that have been removed
856 				if (!allDefs.contains(tag)) {
857 					if (shouldDroppedTagBeRemovedOnUpdate(theRequest, tag)) {
858 						theEntity.getTags().remove(tag);
859 					}
860 				}
861 
862 			});
863 
864 			if (!allTagsOld.equals(allTagsNew)) {
865 				changed = true;
866 			}
867 			theEntity.setHasTags(!allTagsNew.isEmpty());
868 
869 		} else {
870 			theEntity.setHashSha256(null);
871 			bytes = null;
872 			encoding = ResourceEncodingEnum.DEL;
873 		}
874 
875 		if (changed == false) {
876 			if (theEntity.getId() == null) {
877 				changed = true;
878 			} else {
879 				ResourceHistoryTable currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion());
880 				if (currentHistoryVersion == null || currentHistoryVersion.getResource() == null) {
881 					changed = true;
882 				} else {
883 					changed = !Arrays.equals(currentHistoryVersion.getResource(), bytes);
884 				}
885 			}
886 		}
887 
888 		EncodedResource retVal = new EncodedResource();
889 		retVal.setEncoding(encoding);
890 		retVal.setResource(bytes);
891 		retVal.setChanged(changed);
892 
893 		return retVal;
894 	}
895 
896 	@SuppressWarnings("unchecked")
897 	private <R extends IBaseResource> R populateResourceMetadataHapi(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<? extends BaseTag> theTagList, boolean theForHistoryOperation, IResource res, Long theVersion) {
898 		R retVal = (R) res;
899 		if (theEntity.getDeleted() != null) {
900 			res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance();
901 			retVal = (R) res;
902 			ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
903 			if (theForHistoryOperation) {
904 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE);
905 			}
906 		} else if (theForHistoryOperation) {
907 			/*
908 			 * 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.
909 			 */
910 			Date published = theEntity.getPublished().getValue();
911 			Date updated = theEntity.getUpdated().getValue();
912 			if (published.equals(updated)) {
913 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST);
914 			} else {
915 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT);
916 			}
917 		}
918 
919 		res.setId(theEntity.getIdDt().withVersion(theVersion.toString()));
920 
921 		ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion()));
922 		ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished());
923 		ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated());
924 		IDao.RESOURCE_PID.put(res, theEntity.getId());
925 
926 		Collection<? extends BaseTag> tags = theTagList;
927 		if (theEntity.isHasTags()) {
928 			TagList tagList = new TagList();
929 			List<IBaseCoding> securityLabels = new ArrayList<>();
930 			List<IdDt> profiles = new ArrayList<>();
931 			for (BaseTag next : tags) {
932 				switch (next.getTag().getTagType()) {
933 					case PROFILE:
934 						profiles.add(new IdDt(next.getTag().getCode()));
935 						break;
936 					case SECURITY_LABEL:
937 						IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt();
938 						secLabel.setSystem(next.getTag().getSystem());
939 						secLabel.setCode(next.getTag().getCode());
940 						secLabel.setDisplay(next.getTag().getDisplay());
941 						securityLabels.add(secLabel);
942 						break;
943 					case TAG:
944 						tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay()));
945 						break;
946 				}
947 			}
948 			if (tagList.size() > 0) {
949 				ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList);
950 			}
951 			if (securityLabels.size() > 0) {
952 				ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels));
953 			}
954 			if (profiles.size() > 0) {
955 				ResourceMetadataKeyEnum.PROFILES.put(res, profiles);
956 			}
957 		}
958 
959 		return retVal;
960 	}
961 
962 	@SuppressWarnings("unchecked")
963 	private <R extends IBaseResource> R populateResourceMetadataRi(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<? extends BaseTag> theTagList, boolean theForHistoryOperation, IAnyResource res, Long theVersion) {
964 		R retVal = (R) res;
965 		if (theEntity.getDeleted() != null) {
966 			res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance();
967 			retVal = (R) res;
968 			ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
969 			if (theForHistoryOperation) {
970 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.DELETE.toCode());
971 			}
972 		} else if (theForHistoryOperation) {
973 			/*
974 			 * 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.
975 			 */
976 			Date published = theEntity.getPublished().getValue();
977 			Date updated = theEntity.getUpdated().getValue();
978 			if (published.equals(updated)) {
979 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.POST.toCode());
980 			} else {
981 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.PUT.toCode());
982 			}
983 		}
984 
985 		res.getMeta().getTag().clear();
986 		res.getMeta().getProfile().clear();
987 		res.getMeta().getSecurity().clear();
988 		res.getMeta().setLastUpdated(null);
989 		res.getMeta().setVersionId(null);
990 
991 		updateResourceMetadata(theEntity, res);
992 		res.setId(res.getIdElement().withVersion(theVersion.toString()));
993 
994 		res.getMeta().setLastUpdated(theEntity.getUpdatedDate());
995 		IDao.RESOURCE_PID.put(res, theEntity.getId());
996 
997 		Collection<? extends BaseTag> tags = theTagList;
998 
999 		if (theEntity.isHasTags()) {
1000 			for (BaseTag next : tags) {
1001 				switch (next.getTag().getTagType()) {
1002 					case PROFILE:
1003 						res.getMeta().addProfile(next.getTag().getCode());
1004 						break;
1005 					case SECURITY_LABEL:
1006 						IBaseCoding sec = res.getMeta().addSecurity();
1007 						sec.setSystem(next.getTag().getSystem());
1008 						sec.setCode(next.getTag().getCode());
1009 						sec.setDisplay(next.getTag().getDisplay());
1010 						break;
1011 					case TAG:
1012 						IBaseCoding tag = res.getMeta().addTag();
1013 						tag.setSystem(next.getTag().getSystem());
1014 						tag.setCode(next.getTag().getCode());
1015 						tag.setDisplay(next.getTag().getDisplay());
1016 						break;
1017 				}
1018 			}
1019 		}
1020 		return retVal;
1021 	}
1022 
1023 	/**
1024 	 * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
1025 	 *
1026 	 * @param theEntity The resource
1027 	 */
1028 	protected void postDelete(ResourceTable theEntity) {
1029 		// nothing
1030 	}
1031 
1032 	/**
1033 	 * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time.
1034 	 *
1035 	 * @param theEntity   The entity being updated (Do not modify the entity! Undefined behaviour will occur!)
1036 	 * @param theResource The resource being persisted
1037 	 */
1038 	protected void postPersist(ResourceTable theEntity, T theResource) {
1039 		// nothing
1040 	}
1041 
1042 	/**
1043 	 * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
1044 	 *
1045 	 * @param theEntity   The resource
1046 	 * @param theResource The resource being persisted
1047 	 */
1048 	protected void postUpdate(ResourceTable theEntity, T theResource) {
1049 		// nothing
1050 	}
1051 
1052 	@CoverageIgnore
1053 	public BaseHasResource readEntity(IIdType theValueId) {
1054 		throw new NotImplementedException("");
1055 	}
1056 
1057 	@Override
1058 	public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
1059 		/*
1060 		 * We do a null check here because Smile's module system tries to
1061 		 * initialize the application context twice if two modules depend on
1062 		 * the persistence module. The second time sets the dependency's appctx.
1063 		 */
1064 		if (myApplicationContext == null) {
1065 			myApplicationContext = theApplicationContext;
1066 		}
1067 	}
1068 
1069 	/**
1070 	 * 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.
1071 	 * <p>
1072 	 * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour.
1073 	 * </p>
1074 	 * <p>
1075 	 * 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.
1076 	 * </p>
1077 	 *
1078 	 * @param theTag The tag
1079 	 * @return Returns <code>true</code> if the tag should be removed
1080 	 */
1081 	protected boolean shouldDroppedTagBeRemovedOnUpdate(RequestDetails theRequest, ResourceTag theTag) {
1082 
1083 		Set<TagTypeEnum> metaSnapshotModeTokens = null;
1084 
1085 		if (theRequest != null) {
1086 			List<String> metaSnapshotMode = theRequest.getHeaders(JpaConstants.HEADER_META_SNAPSHOT_MODE);
1087 			if (metaSnapshotMode != null && !metaSnapshotMode.isEmpty()) {
1088 				metaSnapshotModeTokens = new HashSet<>();
1089 				for (String nextHeaderValue : metaSnapshotMode) {
1090 					StringTokenizer tok = new StringTokenizer(nextHeaderValue, ",");
1091 					while (tok.hasMoreTokens()) {
1092 						switch (trim(tok.nextToken())) {
1093 							case "TAG":
1094 								metaSnapshotModeTokens.add(TagTypeEnum.TAG);
1095 								break;
1096 							case "PROFILE":
1097 								metaSnapshotModeTokens.add(TagTypeEnum.PROFILE);
1098 								break;
1099 							case "SECURITY_LABEL":
1100 								metaSnapshotModeTokens.add(TagTypeEnum.SECURITY_LABEL);
1101 								break;
1102 						}
1103 					}
1104 				}
1105 			}
1106 		}
1107 
1108 		if (metaSnapshotModeTokens == null) {
1109 			metaSnapshotModeTokens = Collections.singleton(TagTypeEnum.PROFILE);
1110 		}
1111 
1112 		if (metaSnapshotModeTokens.contains(theTag.getTag().getTagType())) {
1113 			return true;
1114 		}
1115 
1116 		return false;
1117 	}
1118 
1119 	private ExpungeOutcome toExpungeOutcome(ExpungeOptions theExpungeOptions, AtomicInteger theRemainingCount) {
1120 		return new ExpungeOutcome()
1121 			.setDeletedCount(theExpungeOptions.getLimit() - theRemainingCount.get());
1122 	}
1123 
1124 	@Override
1125 	public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) {
1126 		RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType());
1127 		Class<? extends IBaseResource> resourceType = type.getImplementingClass();
1128 		return toResource(resourceType, theEntity, null, theForHistoryOperation);
1129 	}
1130 
1131 	@SuppressWarnings("unchecked")
1132 	@Override
1133 	public <R extends IBaseResource> R toResource(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<ResourceTag> theTagList, boolean theForHistoryOperation) {
1134 
1135 		// 1. get resource, it's encoding and the tags if any
1136 		byte[] resourceBytes = null;
1137 		ResourceEncodingEnum resourceEncoding = null;
1138 		Collection<? extends BaseTag> myTagList = null;
1139 		Long version = null;
1140 
1141 		if (theEntity instanceof ResourceHistoryTable) {
1142 			ResourceHistoryTable history = (ResourceHistoryTable) theEntity;
1143 			resourceBytes = history.getResource();
1144 			resourceEncoding = history.getEncoding();
1145 			myTagList = history.getTags();
1146 			version = history.getVersion();
1147 		} else if (theEntity instanceof ResourceTable) {
1148 			ResourceTable resource = (ResourceTable) theEntity;
1149 			version = theEntity.getVersion();
1150 			ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version);
1151 			while (history == null) {
1152 				if (version > 1L) {
1153 					version--;
1154 					history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version);
1155 				} else {
1156 					return null;
1157 				}
1158 			}
1159 			resourceBytes = history.getResource();
1160 			resourceEncoding = history.getEncoding();
1161 			myTagList = resource.getTags();
1162 			version = history.getVersion();
1163 		} else if (theEntity instanceof ResourceSearchView) {
1164 			// This is the search View
1165 			ResourceSearchView myView = (ResourceSearchView) theEntity;
1166 			resourceBytes = myView.getResource();
1167 			resourceEncoding = myView.getEncoding();
1168 			version = myView.getVersion();
1169 			if (theTagList == null)
1170 				myTagList = new HashSet<>();
1171 			else
1172 				myTagList = theTagList;
1173 		} else {
1174 			// something wrong
1175 			return null;
1176 		}
1177 
1178 		// 2. get The text
1179 		String resourceText = decodeResource(resourceBytes, resourceEncoding);
1180 
1181 		// 3. Use the appropriate custom type if one is specified in the context
1182 		Class<R> resourceType = theResourceType;
1183 		if (myContext.hasDefaultTypeForProfile()) {
1184 			for (BaseTag nextTag : myTagList) {
1185 				if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) {
1186 					String profile = nextTag.getTag().getCode();
1187 					if (isNotBlank(profile)) {
1188 						Class<? extends IBaseResource> newType = myContext.getDefaultTypeForProfile(profile);
1189 						if (newType != null && theResourceType.isAssignableFrom(newType)) {
1190 							ourLog.debug("Using custom type {} for profile: {}", newType.getName(), profile);
1191 							resourceType = (Class<R>) newType;
1192 							break;
1193 						}
1194 					}
1195 				}
1196 			}
1197 		}
1198 
1199 		// 4. parse the text to FHIR
1200 		R retVal;
1201 		if (resourceEncoding != ResourceEncodingEnum.DEL) {
1202 			IParser parser = resourceEncoding.newParser(getContext(theEntity.getFhirVersion()));
1203 			parser.setParserErrorHandler(new LenientErrorHandler(false).setErrorOnInvalidValue(false));
1204 
1205 			try {
1206 				retVal = parser.parseResource(resourceType, resourceText);
1207 			} catch (Exception e) {
1208 				StringBuilder b = new StringBuilder();
1209 				b.append("Failed to parse database resource[");
1210 				b.append(resourceType);
1211 				b.append("/");
1212 				b.append(theEntity.getIdDt().getIdPart());
1213 				b.append(" (pid ");
1214 				b.append(theEntity.getId());
1215 				b.append(", version ");
1216 				b.append(theEntity.getFhirVersion().name());
1217 				b.append("): ");
1218 				b.append(e.getMessage());
1219 				String msg = b.toString();
1220 				ourLog.error(msg, e);
1221 				throw new DataFormatException(msg, e);
1222 			}
1223 
1224 		} else {
1225 
1226 			retVal = (R) myContext.getResourceDefinition(theEntity.getResourceType()).newInstance();
1227 
1228 		}
1229 
1230 		// 5. fill MetaData
1231 		if (retVal instanceof IResource) {
1232 			IResource res = (IResource) retVal;
1233 			retVal = populateResourceMetadataHapi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version);
1234 		} else {
1235 			IAnyResource res = (IAnyResource) retVal;
1236 			retVal = populateResourceMetadataRi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version);
1237 		}
1238 
1239 		return retVal;
1240 	}
1241 
1242 	public String toResourceName(Class<? extends IBaseResource> theResourceType) {
1243 		return myContext.getResourceDefinition(theResourceType).getName();
1244 	}
1245 
1246 	String toResourceName(IBaseResource theResource) {
1247 		return myContext.getResourceDefinition(theResource).getName();
1248 	}
1249 
1250 	private Slice<Long> toSlice(ResourceHistoryTable theVersion) {
1251 		Validate.notNull(theVersion);
1252 		return new SliceImpl<>(Collections.singletonList(theVersion.getId()));
1253 	}
1254 
1255 	@SuppressWarnings("unchecked")
1256 	protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable
1257 		theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
1258 													 boolean theUpdateVersion, Date theUpdateTime, boolean theForceUpdate, boolean theCreateNewHistoryEntry) {
1259 		Validate.notNull(theEntity);
1260 		Validate.isTrue(theDeletedTimestampOrNull != null || theResource != null, "Must have either a resource[%s] or a deleted timestamp[%s] for resource PID[%s]", theDeletedTimestampOrNull != null, theResource != null, theEntity.getId());
1261 
1262 		ourLog.debug("Starting entity update");
1263 
1264 		/*
1265 		 * This should be the very first thing..
1266 		 */
1267 		if (theResource != null) {
1268 			if (thePerformIndexing) {
1269 				if (!ourValidationDisabledForUnitTest) {
1270 					validateResourceForStorage((T) theResource, theEntity);
1271 				}
1272 			}
1273 			String resourceType = myContext.getResourceDefinition(theResource).getName();
1274 			if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) {
1275 				throw new UnprocessableEntityException(
1276 					"Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]");
1277 			}
1278 		}
1279 
1280 		if (theEntity.getPublished() == null) {
1281 			ourLog.debug("Entity has published time: {}", new InstantDt(theUpdateTime));
1282 
1283 			theEntity.setPublished(theUpdateTime);
1284 		}
1285 
1286 		ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(theEntity);
1287 
1288 		ResourceIndexedSearchParams newParams = null;
1289 
1290 		EncodedResource changed;
1291 		if (theDeletedTimestampOrNull != null) {
1292 
1293 			newParams = new ResourceIndexedSearchParams();
1294 
1295 			theEntity.setDeleted(theDeletedTimestampOrNull);
1296 			theEntity.setUpdated(theDeletedTimestampOrNull);
1297 			theEntity.setNarrativeTextParsedIntoWords(null);
1298 			theEntity.setContentTextParsedIntoWords(null);
1299 			theEntity.setHashSha256(null);
1300 			theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
1301 			changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
1302 
1303 		} else {
1304 
1305 			theEntity.setDeleted(null);
1306 
1307 			if (thePerformIndexing) {
1308 
1309 				newParams = new ResourceIndexedSearchParams();
1310 				mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams);
1311 
1312 				changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
1313 
1314 				theEntity.setUpdated(theUpdateTime);
1315 				if (theResource instanceof IResource) {
1316 					theEntity.setLanguage(((IResource) theResource).getLanguage().getValue());
1317 				} else {
1318 					theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue());
1319 				}
1320 
1321 				newParams.setParamsOn(theEntity);
1322 				theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
1323 				populateFullTextFields(myContext, theResource, theEntity);
1324 			} else {
1325 
1326 				changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false);
1327 
1328 				theEntity.setUpdated(theUpdateTime);
1329 				theEntity.setIndexStatus(null);
1330 
1331 			}
1332 
1333 		}
1334 
1335 		if (!changed.isChanged() && !theForceUpdate && myConfig.isSuppressUpdatesWithNoChange()) {
1336 			ourLog.debug("Resource {} has not changed", theEntity.getIdDt().toUnqualified().getValue());
1337 			if (theResource != null) {
1338 				updateResourceMetadata(theEntity, theResource);
1339 			}
1340 			theEntity.setUnchangedInCurrentOperation(true);
1341 			return theEntity;
1342 		}
1343 
1344 		if (theUpdateVersion) {
1345 			theEntity.setVersion(theEntity.getVersion() + 1);
1346 		}
1347 
1348 		/*
1349 		 * Save the resource itself
1350 		 */
1351 		if (theEntity.getId() == null) {
1352 			myEntityManager.persist(theEntity);
1353 
1354 			if (theEntity.getForcedId() != null) {
1355 				myEntityManager.persist(theEntity.getForcedId());
1356 			}
1357 
1358 			postPersist(theEntity, (T) theResource);
1359 
1360 		} else if (theEntity.getDeleted() != null) {
1361 			theEntity = myEntityManager.merge(theEntity);
1362 
1363 			postDelete(theEntity);
1364 
1365 		} else {
1366 			theEntity = myEntityManager.merge(theEntity);
1367 
1368 			postUpdate(theEntity, (T) theResource);
1369 		}
1370 
1371 		/*
1372 		 * Create history entry
1373 		 */
1374 		if (theCreateNewHistoryEntry) {
1375 			final ResourceHistoryTable historyEntry = theEntity.toHistory();
1376 			historyEntry.setEncoding(changed.getEncoding());
1377 			historyEntry.setResource(changed.getResource());
1378 
1379 			ourLog.debug("Saving history entry {}", historyEntry.getIdDt());
1380 			myResourceHistoryTableDao.save(historyEntry);
1381 		}
1382 
1383 		/*
1384 		 * Update the "search param present" table which is used for the
1385 		 * ?foo:missing=true queries
1386 		 *
1387 		 * Note that we're only populating this for reference params
1388 		 * because the index tables for all other types have a MISSING column
1389 		 * right on them for handling the :missing queries. We can't use the
1390 		 * index table for resource links (reference indexes) because we index
1391 		 * those by path and not by parameter name.
1392 		 */
1393 		if (thePerformIndexing) {
1394 			Map<String, Boolean> presentSearchParams = new HashMap<>();
1395 			for (String nextKey : newParams.getPopulatedResourceLinkParameters()) {
1396 				presentSearchParams.put(nextKey, Boolean.TRUE);
1397 			}
1398 			Set<Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
1399 			for (Entry<String, RuntimeSearchParam> nextSpEntry : activeSearchParams) {
1400 				if (nextSpEntry.getValue().getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
1401 					if (!presentSearchParams.containsKey(nextSpEntry.getKey())) {
1402 						presentSearchParams.put(nextSpEntry.getKey(), Boolean.FALSE);
1403 					}
1404 				}
1405 			}
1406 			mySearchParamPresenceSvc.updatePresence(theEntity, presentSearchParams);
1407 		}
1408 
1409 		/*
1410 		 * Indexing
1411 		 */
1412 		if (thePerformIndexing) {
1413 			myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams);
1414 			mySearchParamWithInlineReferencesExtractor.storeCompositeStringUniques(newParams, theEntity, existingParams);
1415 		}
1416 
1417 		if (theResource != null) {
1418 			updateResourceMetadata(theEntity, theResource);
1419 		}
1420 
1421 
1422 		return theEntity;
1423 	}
1424 
1425 	protected ResourceTable updateEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable
1426 		entity, Date theDeletedTimestampOrNull, Date theUpdateTime) {
1427 		return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true);
1428 	}
1429 
1430 	public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion,
1431 													ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) {
1432 		// Notify interceptors
1433 		ActionRequestDetails requestDetails;
1434 		if (theRequestDetails != null) {
1435 			requestDetails = new ActionRequestDetails(theRequestDetails, theResource, theResourceId.getResourceType(), theResourceId);
1436 			notifyInterceptors(RestOperationTypeEnum.UPDATE, requestDetails);
1437 		}
1438 
1439 		// Notify IServerOperationInterceptors about pre-action call
1440 		if (theRequestDetails != null) {
1441 			theRequestDetails.getRequestOperationCallback().resourcePreUpdate(theOldResource, theResource);
1442 		}
1443 		for (IServerInterceptor next : getConfig().getInterceptors()) {
1444 			if (next instanceof IServerOperationInterceptor) {
1445 				((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource);
1446 			}
1447 		}
1448 		HookParams hookParams = new HookParams()
1449 			.add(IBaseResource.class, theOldResource)
1450 			.add(IBaseResource.class, theResource);
1451 		myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, hookParams);
1452 
1453 		// Perform update
1454 		ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing);
1455 
1456 		/*
1457 		 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
1458 		 * we'll manually increase the version. This is important because we want the updated version number
1459 		 * to be reflected in the resource shared with interceptors
1460 		 */
1461 		if (!thePerformIndexing && !savedEntity.isUnchangedInCurrentOperation() && !ourDisableIncrementOnUpdateForUnitTest) {
1462 			if (theResourceId.hasVersionIdPart() == false) {
1463 				theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion()));
1464 			}
1465 			incrementId(theResource, savedEntity, theResourceId);
1466 		}
1467 
1468 		// Update version/lastUpdated so that interceptors see the correct version
1469 		updateResourceMetadata(savedEntity, theResource);
1470 
1471 		// Notify interceptors
1472 		if (!savedEntity.isUnchangedInCurrentOperation()) {
1473 			if (theRequestDetails != null) {
1474 				theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
1475 				theRequestDetails.getRequestOperationCallback().resourceUpdated(theOldResource, theResource);
1476 			}
1477 			for (IServerInterceptor next : getConfig().getInterceptors()) {
1478 				if (next instanceof IServerOperationInterceptor) {
1479 					((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource);
1480 					((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource);
1481 				}
1482 			}
1483 			TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
1484 				@Override
1485 				public void beforeCommit(boolean readOnly) {
1486 					HookParams hookParams = new HookParams()
1487 						.add(IBaseResource.class, theOldResource)
1488 						.add(IBaseResource.class, theResource);
1489 					myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, hookParams);
1490 				}
1491 			});
1492 		}
1493 
1494 		return savedEntity;
1495 	}
1496 
1497 	protected void updateResourceMetadata(IBaseResourceEntity theEntity, IBaseResource theResource) {
1498 		IIdType id = theEntity.getIdDt();
1499 		if (getContext().getVersion().getVersion().isRi()) {
1500 			id = getContext().getVersion().newIdType().setValue(id.getValue());
1501 		}
1502 
1503 		if (id.hasResourceType() == false) {
1504 			id = id.withResourceType(theEntity.getResourceType());
1505 		}
1506 
1507 		theResource.setId(id);
1508 		if (theResource instanceof IResource) {
1509 			ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, id.getVersionIdPart());
1510 			ResourceMetadataKeyEnum.UPDATED.put((IResource) theResource, theEntity.getUpdated());
1511 		} else {
1512 			IBaseMetaType meta = theResource.getMeta();
1513 			meta.setVersionId(id.getVersionIdPart());
1514 			meta.setLastUpdated(theEntity.getUpdatedDate());
1515 		}
1516 	}
1517 
1518 	private void validateChildReferences(IBase theElement, String thePath) {
1519 		if (theElement == null) {
1520 			return;
1521 		}
1522 		BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
1523 		if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
1524 			return;
1525 		}
1526 
1527 		BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def;
1528 		for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) {
1529 
1530 			List<IBase> values = nextChildDef.getAccessor().getValues(theElement);
1531 			if (values == null || values.isEmpty()) {
1532 				continue;
1533 			}
1534 
1535 			String newPath = thePath + "." + nextChildDef.getElementName();
1536 
1537 			for (IBase nextChild : values) {
1538 				validateChildReferences(nextChild, newPath);
1539 			}
1540 
1541 			if (nextChildDef instanceof RuntimeChildResourceDefinition) {
1542 				RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef;
1543 				Set<String> validTypes = new HashSet<>();
1544 				boolean allowAny = false;
1545 				for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) {
1546 					if (nextValidType.isInterface()) {
1547 						allowAny = true;
1548 						break;
1549 					}
1550 					validTypes.add(getContext().getResourceDefinition(nextValidType).getName());
1551 				}
1552 
1553 				if (allowAny) {
1554 					continue;
1555 				}
1556 
1557 				for (IBase nextChild : values) {
1558 					IBaseReference nextRef = (IBaseReference) nextChild;
1559 					IIdType referencedId = nextRef.getReferenceElement();
1560 					if (!isBlank(referencedId.getResourceType())) {
1561 						if (!isLogicalReference(referencedId)) {
1562 							if (!referencedId.getValue().contains("?")) {
1563 								if (!validTypes.contains(referencedId.getResourceType())) {
1564 									throw new UnprocessableEntityException(
1565 										"Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path");
1566 								}
1567 							}
1568 						}
1569 					}
1570 				}
1571 
1572 			}
1573 		}
1574 	}
1575 
1576 	public void validateDeleteConflictsEmptyOrThrowException(List<DeleteConflict> theDeleteConflicts) {
1577 		if (theDeleteConflicts.isEmpty()) {
1578 			return;
1579 		}
1580 
1581 		IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
1582 		String firstMsg = null;
1583 		for (DeleteConflict next : theDeleteConflicts) {
1584 			StringBuilder b = new StringBuilder();
1585 			b.append("Unable to delete ");
1586 			b.append(next.getTargetId().toUnqualifiedVersionless().getValue());
1587 			b.append(" because at least one resource has a reference to this resource. First reference found was resource ");
1588 			b.append(next.getTargetId().toUnqualifiedVersionless().getValue());
1589 			b.append(" in path ");
1590 			b.append(next.getSourcePath());
1591 			String msg = b.toString();
1592 			if (firstMsg == null) {
1593 				firstMsg = msg;
1594 			}
1595 			OperationOutcomeUtil.addIssue(getContext(), oo, OO_SEVERITY_ERROR, msg, null, "processing");
1596 		}
1597 
1598 		throw new ResourceVersionConflictException(firstMsg, oo);
1599 	}
1600 
1601 	protected void validateMetaCount(int theMetaCount) {
1602 		if (myConfig.getResourceMetaCountHardLimit() != null) {
1603 			if (theMetaCount > myConfig.getResourceMetaCountHardLimit()) {
1604 				throw new UnprocessableEntityException("Resource contains " + theMetaCount + " meta entries (tag/profile/security label), maximum is " + myConfig.getResourceMetaCountHardLimit());
1605 			}
1606 		}
1607 	}
1608 
1609 	/**
1610 	 * 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
1611 	 * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check.
1612 	 *
1613 	 * @param theResource     The resource that is about to be persisted
1614 	 * @param theEntityToSave TODO
1615 	 */
1616 	protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) {
1617 		Object tag = null;
1618 
1619 		int totalMetaCount = 0;
1620 
1621 		if (theResource instanceof IResource) {
1622 			IResource res = (IResource) theResource;
1623 			TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res);
1624 			if (tagList != null) {
1625 				tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1626 				totalMetaCount += tagList.size();
1627 			}
1628 			List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res);
1629 			if (profileList != null) {
1630 				totalMetaCount += profileList.size();
1631 			}
1632 		} else {
1633 			IAnyResource res = (IAnyResource) theResource;
1634 			tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1635 			totalMetaCount += res.getMeta().getTag().size();
1636 			totalMetaCount += res.getMeta().getProfile().size();
1637 			totalMetaCount += res.getMeta().getSecurity().size();
1638 		}
1639 
1640 		if (tag != null) {
1641 			throw new UnprocessableEntityException("Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data");
1642 		}
1643 
1644 		String resName = getContext().getResourceDefinition(theResource).getName();
1645 		validateChildReferences(theResource, resName);
1646 
1647 		validateMetaCount(totalMetaCount);
1648 
1649 	}
1650 
1651 	@Override
1652 	public ISearchParamRegistry getSearchParamRegistry() {
1653 		return mySearchParamRegistry;
1654 	}
1655 
1656 	public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
1657 		if (theRequestDetails != null) {
1658 			theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
1659 		}
1660 	}
1661 
1662 	public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
1663 		if (theRequestDetails != null) {
1664 			theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE);
1665 		}
1666 	}
1667 
1668 	public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) {
1669 		StringBuilder retVal = new StringBuilder();
1670 		@SuppressWarnings("rawtypes")
1671 		List<IPrimitiveType> childElements = theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class);
1672 		for (@SuppressWarnings("rawtypes")
1673 			IPrimitiveType nextType : childElements) {
1674 			if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) {
1675 				String nextValue = nextType.getValueAsString();
1676 				if (isNotBlank(nextValue)) {
1677 					retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
1678 					retVal.append("\n");
1679 				}
1680 			}
1681 		}
1682 		return retVal.toString();
1683 	}
1684 
1685 	public static void populateFullTextFields(final FhirContext theContext, final IBaseResource theResource, ResourceTable theEntity) {
1686 		if (theEntity.getDeleted() != null) {
1687 			theEntity.setNarrativeTextParsedIntoWords(null);
1688 			theEntity.setContentTextParsedIntoWords(null);
1689 		} else {
1690 			theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource));
1691 			theEntity.setContentTextParsedIntoWords(parseContentTextIntoWords(theContext, theResource));
1692 		}
1693 	}
1694 
1695 	public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
1696 		String resourceText = null;
1697 		switch (theResourceEncoding) {
1698 			case JSON:
1699 				resourceText = new String(theResourceBytes, Charsets.UTF_8);
1700 				break;
1701 			case JSONC:
1702 				resourceText = GZipUtil.decompress(theResourceBytes);
1703 				break;
1704 			case DEL:
1705 				break;
1706 		}
1707 		return resourceText;
1708 	}
1709 
1710 	public static byte[] encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, Set<String> theExcludeElements, FhirContext theContext) {
1711 		byte[] bytes;
1712 		IParser parser = theEncoding.newParser(theContext);
1713 		parser.setDontEncodeElements(theExcludeElements);
1714 		String encoded = parser.encodeResourceToString(theResource);
1715 
1716 
1717 		switch (theEncoding) {
1718 			case JSON:
1719 				bytes = encoded.getBytes(Charsets.UTF_8);
1720 				break;
1721 			case JSONC:
1722 				bytes = GZipUtil.compress(encoded);
1723 				break;
1724 			default:
1725 			case DEL:
1726 				bytes = new byte[0];
1727 				break;
1728 		}
1729 
1730 		ourLog.debug("Encoded {} chars of resource body as {} bytes", encoded.length(), bytes.length);
1731 		return bytes;
1732 	}
1733 
1734 	private static String parseNarrativeTextIntoWords(IBaseResource theResource) {
1735 
1736 		StringBuilder b = new StringBuilder();
1737 		if (theResource instanceof IResource) {
1738 			IResource resource = (IResource) theResource;
1739 			List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
1740 			if (xmlEvents != null) {
1741 				for (XMLEvent next : xmlEvents) {
1742 					if (next.isCharacters()) {
1743 						Characters characters = next.asCharacters();
1744 						b.append(characters.getData()).append(" ");
1745 					}
1746 				}
1747 			}
1748 		} else if (theResource instanceof IDomainResource) {
1749 			IDomainResource resource = (IDomainResource) theResource;
1750 			try {
1751 				String divAsString = resource.getText().getDivAsString();
1752 				List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
1753 				if (xmlEvents != null) {
1754 					for (XMLEvent next : xmlEvents) {
1755 						if (next.isCharacters()) {
1756 							Characters characters = next.asCharacters();
1757 							b.append(characters.getData()).append(" ");
1758 						}
1759 					}
1760 				}
1761 			} catch (Exception e) {
1762 				throw new DataFormatException("Unable to convert DIV to string", e);
1763 			}
1764 
1765 		}
1766 		return b.toString();
1767 	}
1768 
1769 	@VisibleForTesting
1770 	public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) {
1771 		ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest;
1772 	}
1773 
1774 	/**
1775 	 * Do not call this method outside of unit tests
1776 	 */
1777 	@VisibleForTesting
1778 	public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
1779 		ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
1780 	}
1781 
1782 	private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) {
1783 		ArrayList<BaseCodingDt> retVal = new ArrayList<>(theSecurityLabels.size());
1784 		for (IBaseCoding next : theSecurityLabels) {
1785 			retVal.add((BaseCodingDt) next);
1786 		}
1787 		return retVal;
1788 	}
1789 
1790 	public static void validateResourceType(BaseHasResource theEntity, String theResourceName) {
1791 		if (!theResourceName.equals(theEntity.getResourceType())) {
1792 			throw new ResourceNotFoundException(
1793 				"Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType());
1794 		}
1795 	}
1796 }