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 		ourLog.debug("Query affected {} rows in {}: {}", outcome, sw.toString(), theQuery);
399 		return outcome;
400 	}
401 
402 	private void expungeCurrentVersionOfResource(Long theResourceId, AtomicInteger theRemainingCount) {
403 		ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new);
404 
405 		ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion());
406 		if (currentVersion != null) {
407 			expungeHistoricalVersion(currentVersion.getId());
408 		}
409 
410 		ourLog.info("Expunging current version of resource {}", resource.getIdDt().getValue());
411 
412 		myResourceIndexedSearchParamUriDao.deleteAll(resource.getParamsUri());
413 		myResourceIndexedSearchParamCoordsDao.deleteAll(resource.getParamsCoords());
414 		myResourceIndexedSearchParamDateDao.deleteAll(resource.getParamsDate());
415 		myResourceIndexedSearchParamNumberDao.deleteAll(resource.getParamsNumber());
416 		myResourceIndexedSearchParamQuantityDao.deleteAll(resource.getParamsQuantity());
417 		myResourceIndexedSearchParamStringDao.deleteAll(resource.getParamsString());
418 		myResourceIndexedSearchParamTokenDao.deleteAll(resource.getParamsToken());
419 		myResourceLinkDao.deleteAll(resource.getResourceLinks());
420 		myResourceLinkDao.deleteAll(resource.getResourceLinksAsTarget());
421 
422 		myResourceTagDao.deleteAll(resource.getTags());
423 		resource.getTags().clear();
424 
425 		if (resource.getForcedId() != null) {
426 			ForcedId forcedId = resource.getForcedId();
427 			resource.setForcedId(null);
428 			myResourceTableDao.saveAndFlush(resource);
429 			myIdHelperService.delete(forcedId);
430 		}
431 
432 		myResourceTableDao.delete(resource);
433 
434 		theRemainingCount.decrementAndGet();
435 	}
436 
437 	protected void expungeHistoricalVersion(Long theNextVersionId) {
438 		ResourceHistoryTable version = myResourceHistoryTableDao.findById(theNextVersionId).orElseThrow(IllegalArgumentException::new);
439 		ourLog.info("Deleting resource version {}", version.getIdDt().getValue());
440 
441 		myResourceHistoryTagDao.deleteAll(version.getTags());
442 		myResourceHistoryTableDao.delete(version);
443 	}
444 
445 	protected void expungeHistoricalVersionsOfId(Long theResourceId, AtomicInteger theRemainingCount) {
446 		ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalArgumentException::new);
447 
448 		Pageable page = PageRequest.of(0, theRemainingCount.get());
449 
450 		Slice<Long> versionIds = myResourceHistoryTableDao.findForResourceId(page, resource.getId(), resource.getVersion());
451 		ourLog.debug("Found {} versions of resource {} to expunge", versionIds.getNumberOfElements(), resource.getIdDt().getValue());
452 		for (Long nextVersionId : versionIds) {
453 			expungeHistoricalVersion(nextVersionId);
454 			if (theRemainingCount.decrementAndGet() <= 0) {
455 				return;
456 			}
457 		}
458 	}
459 
460 	private void extractTagsHapi(IResource theResource, ResourceTable theEntity, Set<ResourceTag> allDefs) {
461 		TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource);
462 		if (tagList != null) {
463 			for (Tag next : tagList) {
464 				TagDefinition def = getTagOrNull(TagTypeEnum.TAG, next.getScheme(), next.getTerm(), next.getLabel());
465 				if (def != null) {
466 					ResourceTag tag = theEntity.addTag(def);
467 					allDefs.add(tag);
468 					theEntity.setHasTags(true);
469 				}
470 			}
471 		}
472 
473 		List<BaseCodingDt> securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource);
474 		if (securityLabels != null) {
475 			for (BaseCodingDt next : securityLabels) {
476 				TagDefinition def = getTagOrNull(TagTypeEnum.SECURITY_LABEL, next.getSystemElement().getValue(), next.getCodeElement().getValue(), next.getDisplayElement().getValue());
477 				if (def != null) {
478 					ResourceTag tag = theEntity.addTag(def);
479 					allDefs.add(tag);
480 					theEntity.setHasTags(true);
481 				}
482 			}
483 		}
484 
485 		List<IdDt> profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource);
486 		if (profiles != null) {
487 			for (IIdType next : profiles) {
488 				TagDefinition def = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
489 				if (def != null) {
490 					ResourceTag tag = theEntity.addTag(def);
491 					allDefs.add(tag);
492 					theEntity.setHasTags(true);
493 				}
494 			}
495 		}
496 	}
497 
498 	private void extractTagsRi(IAnyResource theResource, ResourceTable theEntity, Set<ResourceTag> theAllTags) {
499 		List<? extends IBaseCoding> tagList = theResource.getMeta().getTag();
500 		if (tagList != null) {
501 			for (IBaseCoding next : tagList) {
502 				TagDefinition def = getTagOrNull(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay());
503 				if (def != null) {
504 					ResourceTag tag = theEntity.addTag(def);
505 					theAllTags.add(tag);
506 					theEntity.setHasTags(true);
507 				}
508 			}
509 		}
510 
511 		List<? extends IBaseCoding> securityLabels = theResource.getMeta().getSecurity();
512 		if (securityLabels != null) {
513 			for (IBaseCoding next : securityLabels) {
514 				TagDefinition def = getTagOrNull(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay());
515 				if (def != null) {
516 					ResourceTag tag = theEntity.addTag(def);
517 					theAllTags.add(tag);
518 					theEntity.setHasTags(true);
519 				}
520 			}
521 		}
522 
523 		List<? extends IPrimitiveType<String>> profiles = theResource.getMeta().getProfile();
524 		if (profiles != null) {
525 			for (IPrimitiveType<String> next : profiles) {
526 				TagDefinition def = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
527 				if (def != null) {
528 					ResourceTag tag = theEntity.addTag(def);
529 					theAllTags.add(tag);
530 					theEntity.setHasTags(true);
531 				}
532 			}
533 		}
534 	}
535 
536 	private void findMatchingTagIds(String theResourceName, IIdType theResourceId, Set<Long> tagIds, Class<? extends BaseTag> entityClass) {
537 		{
538 			CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
539 			CriteriaQuery<Tuple> cq = builder.createTupleQuery();
540 			Root<? extends BaseTag> from = cq.from(entityClass);
541 			cq.multiselect(from.get("myTagId").as(Long.class)).distinct(true);
542 
543 			if (theResourceName != null) {
544 				Predicate typePredicate = builder.equal(from.get("myResourceType"), theResourceName);
545 				if (theResourceId != null) {
546 					cq.where(typePredicate, builder.equal(from.get("myResourceId"), myIdHelperService.translateForcedIdToPid(theResourceName, theResourceId.getIdPart())));
547 				} else {
548 					cq.where(typePredicate);
549 				}
550 			}
551 
552 			TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
553 			for (Tuple next : query.getResultList()) {
554 				tagIds.add(next.get(0, Long.class));
555 			}
556 		}
557 	}
558 
559 	protected void flushJpaSession() {
560 		SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class);
561 		int insertionCount = session.getActionQueue().numberOfInsertions();
562 		int updateCount = session.getActionQueue().numberOfUpdates();
563 
564 		StopWatch sw = new StopWatch();
565 		myEntityManager.flush();
566 		ourLog.debug("Session flush took {}ms for {} inserts and {} updates", sw.getMillis(), insertionCount, updateCount);
567 	}
568 
569 	private Set<ResourceTag> getAllTagDefinitions(ResourceTable theEntity) {
570 		HashSet<ResourceTag> retVal = Sets.newHashSet();
571 		if (theEntity.isHasTags()) {
572 			for (ResourceTag next : theEntity.getTags()) {
573 				retVal.add(next);
574 			}
575 		}
576 		return retVal;
577 	}
578 
579 	protected DaoConfig getConfig() {
580 		return myConfig;
581 	}
582 
583 	public void setConfig(DaoConfig theConfig) {
584 		myConfig = theConfig;
585 	}
586 
587 	@Override
588 	public FhirContext getContext() {
589 		return myContext;
590 	}
591 
592 	@Autowired
593 	public void setContext(FhirContext theContext) {
594 		myContext = theContext;
595 	}
596 
597 	public FhirContext getContext(FhirVersionEnum theVersion) {
598 		Validate.notNull(theVersion, "theVersion must not be null");
599 		synchronized (ourRetrievalContexts) {
600 			FhirContext retVal = ourRetrievalContexts.get(theVersion);
601 			if (retVal == null) {
602 				retVal = new FhirContext(theVersion);
603 				ourRetrievalContexts.put(theVersion, retVal);
604 			}
605 			return retVal;
606 		}
607 	}
608 
609 	@SuppressWarnings("unchecked")
610 	public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
611 		return myDaoRegistry.getResourceDaoIfExists(theType);
612 	}
613 
614 	protected IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
615 		return myDaoRegistry.getDaoOrThrowException(theClass);
616 	}
617 
618 	protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
619 		if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) {
620 			return null;
621 		}
622 
623 		CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
624 		CriteriaQuery<TagDefinition> cq = builder.createQuery(TagDefinition.class);
625 		Root<TagDefinition> from = cq.from(TagDefinition.class);
626 
627 		if (isNotBlank(theScheme)) {
628 			cq.where(
629 				builder.and(
630 					builder.equal(from.get("myTagType"), theTagType),
631 					builder.equal(from.get("mySystem"), theScheme),
632 					builder.equal(from.get("myCode"), theTerm)));
633 		} else {
634 			cq.where(
635 				builder.and(
636 					builder.equal(from.get("myTagType"), theTagType),
637 					builder.isNull(from.get("mySystem")),
638 					builder.equal(from.get("myCode"), theTerm)));
639 		}
640 
641 		TypedQuery<TagDefinition> q = myEntityManager.createQuery(cq);
642 		try {
643 			return q.getSingleResult();
644 		} catch (NoResultException e) {
645 			TagDefinition retVal = new TagDefinition(theTagType, theScheme, theTerm, theLabel);
646 			myEntityManager.persist(retVal);
647 			return retVal;
648 		}
649 	}
650 
651 	protected TagList getTags(Class<? extends IBaseResource> theResourceType, IIdType theResourceId) {
652 		String resourceName = null;
653 		if (theResourceType != null) {
654 			resourceName = toResourceName(theResourceType);
655 			if (theResourceId != null && theResourceId.hasVersionIdPart()) {
656 				IFhirResourceDao<? extends IBaseResource> dao = getDao(theResourceType);
657 				BaseHasResource entity = dao.readEntity(theResourceId);
658 				TagList retVal = new TagList();
659 				for (BaseTag next : entity.getTags()) {
660 					retVal.add(next.getTag().toTag());
661 				}
662 				return retVal;
663 			}
664 		}
665 
666 		Set<Long> tagIds = new HashSet<>();
667 		findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceTag.class);
668 		findMatchingTagIds(resourceName, theResourceId, tagIds, ResourceHistoryTag.class);
669 		if (tagIds.isEmpty()) {
670 			return new TagList();
671 		}
672 		{
673 			CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
674 			CriteriaQuery<TagDefinition> cq = builder.createQuery(TagDefinition.class);
675 			Root<TagDefinition> from = cq.from(TagDefinition.class);
676 			cq.where(from.get("myId").in(tagIds));
677 			cq.orderBy(builder.asc(from.get("mySystem")), builder.asc(from.get("myCode")));
678 			TypedQuery<TagDefinition> q = myEntityManager.createQuery(cq);
679 			q.setMaxResults(getConfig().getHardTagListLimit());
680 
681 			TagList retVal = new TagList();
682 			for (TagDefinition next : q.getResultList()) {
683 				retVal.add(next.toTag());
684 			}
685 
686 			return retVal;
687 		}
688 	}
689 
690 	protected IBundleProvider history(String theResourceName, Long theId, Date theSince, Date theUntil) {
691 
692 		String resourceName = defaultIfBlank(theResourceName, null);
693 
694 		Search search = new Search();
695 		search.setDeleted(false);
696 		search.setCreated(new Date());
697 		search.setSearchLastReturned(new Date());
698 		search.setLastUpdated(theSince, theUntil);
699 		search.setUuid(UUID.randomUUID().toString());
700 		search.setResourceType(resourceName);
701 		search.setResourceId(theId);
702 		search.setSearchType(SearchTypeEnum.HISTORY);
703 		search.setStatus(SearchStatusEnum.FINISHED);
704 
705 		if (theSince != null) {
706 			if (resourceName == null) {
707 				search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes(theSince));
708 			} else if (theId == null) {
709 				search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName, theSince));
710 			} else {
711 				search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId, theSince));
712 			}
713 		} else {
714 			if (resourceName == null) {
715 				search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes());
716 			} else if (theId == null) {
717 				search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName));
718 			} else {
719 				search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId));
720 			}
721 		}
722 
723 		search = mySearchDao.save(search);
724 
725 		return new PersistedJpaBundleProvider(search.getUuid(), this);
726 	}
727 
728 	void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) {
729 		String newVersion;
730 		long newVersionLong;
731 		if (theResourceId == null || theResourceId.getVersionIdPart() == null) {
732 			newVersion = "1";
733 			newVersionLong = 1;
734 		} else {
735 			newVersionLong = theResourceId.getVersionIdPartAsLong() + 1;
736 			newVersion = Long.toString(newVersionLong);
737 		}
738 
739 		IIdType newId = theResourceId.withVersion(newVersion);
740 		theResource.getIdElement().setValue(newId.getValue());
741 		theSavedEntity.setVersion(newVersionLong);
742 	}
743 
744 	@Override
745 	public void injectDependenciesIntoBundleProvider(PersistedJpaBundleProvider theProvider) {
746 		theProvider.setContext(getContext());
747 		theProvider.setEntityManager(myEntityManager);
748 		theProvider.setPlatformTransactionManager(myPlatformTransactionManager);
749 		theProvider.setSearchDao(mySearchDao);
750 		theProvider.setSearchCoordinatorSvc(mySearchCoordinatorSvc);
751 	}
752 
753 	public boolean isLogicalReference(IIdType theId) {
754 		return LogicalReferenceHelper.isLogicalReference(myConfig.getModelConfig(), theId);
755 	}
756 
757 	// TODO KHS inject a searchBuilderFactory into callers of this method and delete this method
758 	@Override
759 	public SearchBuilder newSearchBuilder() {
760 		return mySearchBuilderFactory.newSearchBuilder(this);
761 	}
762 
763 	public void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) {
764 		if (theRequestDetails.getId() != null && theRequestDetails.getId().hasResourceType() && isNotBlank(theRequestDetails.getResourceType())) {
765 			if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) {
766 				throw new InternalErrorException(
767 					"Inconsistent server state - Resource types don't match: " + theRequestDetails.getId().getResourceType() + " / " + theRequestDetails.getResourceType());
768 			}
769 		}
770 
771 		if (theRequestDetails.getUserData().get(PROCESSING_SUB_REQUEST) == Boolean.TRUE) {
772 			theRequestDetails.notifyIncomingRequestPreHandled(theOperationType);
773 		}
774 		List<IServerInterceptor> interceptors = getConfig().getInterceptors();
775 		for (IServerInterceptor next : interceptors) {
776 			next.incomingRequestPreHandled(theOperationType, theRequestDetails);
777 		}
778 	}
779 
780 	/**
781 	 * Returns true if the resource has changed (either the contents or the tags)
782 	 */
783 	protected EncodedResource populateResourceIntoEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, boolean theUpdateHash) {
784 		if (theEntity.getResourceType() == null) {
785 			theEntity.setResourceType(toResourceName(theResource));
786 		}
787 
788 		if (theResource != null) {
789 			List<BaseResourceReferenceDt> refs = myContext.newTerser().getAllPopulatedChildElementsOfType(theResource, BaseResourceReferenceDt.class);
790 			for (BaseResourceReferenceDt nextRef : refs) {
791 				if (nextRef.getReference().isEmpty() == false) {
792 					if (nextRef.getReference().hasVersionIdPart()) {
793 						nextRef.setReference(nextRef.getReference().toUnqualifiedVersionless());
794 					}
795 				}
796 			}
797 		}
798 
799 		byte[] bytes;
800 		ResourceEncodingEnum encoding;
801 		boolean changed = false;
802 
803 		if (theEntity.getDeleted() == null) {
804 
805 			encoding = myConfig.getResourceEncoding();
806 			Set<String> excludeElements = ResourceMetaParams.EXCLUDE_ELEMENTS_IN_ENCODED;
807 			theEntity.setFhirVersion(myContext.getVersion().getVersion());
808 
809 			bytes = encodeResource(theResource, encoding, excludeElements, myContext);
810 
811 			if (theUpdateHash) {
812 				HashFunction sha256 = Hashing.sha256();
813 				String hashSha256 = sha256.hashBytes(bytes).toString();
814 				if (hashSha256.equals(theEntity.getHashSha256()) == false) {
815 					changed = true;
816 				}
817 				theEntity.setHashSha256(hashSha256);
818 			}
819 
820 			Set<ResourceTag> allDefs = new HashSet<>();
821 			Set<ResourceTag> allTagsOld = getAllTagDefinitions(theEntity);
822 
823 			if (theResource instanceof IResource) {
824 				extractTagsHapi((IResource) theResource, theEntity, allDefs);
825 			} else {
826 				extractTagsRi((IAnyResource) theResource, theEntity, allDefs);
827 			}
828 
829 			RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
830 			if (def.isStandardType() == false) {
831 				String profile = def.getResourceProfile("");
832 				if (isNotBlank(profile)) {
833 					TagDefinition profileDef = getTagOrNull(TagTypeEnum.PROFILE, NS_JPA_PROFILE, profile, null);
834 					if (def != null) {
835 						ResourceTag tag = theEntity.addTag(profileDef);
836 						allDefs.add(tag);
837 						theEntity.setHasTags(true);
838 					}
839 				}
840 			}
841 
842 			Set<ResourceTag> allTagsNew = getAllTagDefinitions(theEntity);
843 			Set<TagDefinition> allDefsPresent = new HashSet<>();
844 			allTagsNew.forEach(tag -> {
845 
846 				// Don't keep duplicate tags
847 				if (!allDefsPresent.add(tag.getTag())) {
848 					theEntity.getTags().remove(tag);
849 				}
850 
851 				// Drop any tags that have been removed
852 				if (!allDefs.contains(tag)) {
853 					if (shouldDroppedTagBeRemovedOnUpdate(theRequest, tag)) {
854 						theEntity.getTags().remove(tag);
855 					}
856 				}
857 
858 			});
859 
860 			if (!allTagsOld.equals(allTagsNew)) {
861 				changed = true;
862 			}
863 			theEntity.setHasTags(!allTagsNew.isEmpty());
864 
865 		} else {
866 			theEntity.setHashSha256(null);
867 			bytes = null;
868 			encoding = ResourceEncodingEnum.DEL;
869 		}
870 
871 		if (changed == false) {
872 			if (theEntity.getId() == null) {
873 				changed = true;
874 			} else {
875 				ResourceHistoryTable currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion());
876 				if (currentHistoryVersion == null || currentHistoryVersion.getResource() == null) {
877 					changed = true;
878 				} else {
879 					changed = !Arrays.equals(currentHistoryVersion.getResource(), bytes);
880 				}
881 			}
882 		}
883 
884 		EncodedResource retVal = new EncodedResource();
885 		retVal.setEncoding(encoding);
886 		retVal.setResource(bytes);
887 		retVal.setChanged(changed);
888 
889 		return retVal;
890 	}
891 
892 	@SuppressWarnings("unchecked")
893 	private <R extends IBaseResource> R populateResourceMetadataHapi(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<? extends BaseTag> theTagList, boolean theForHistoryOperation, IResource res, Long theVersion) {
894 		R retVal = (R) res;
895 		if (theEntity.getDeleted() != null) {
896 			res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance();
897 			retVal = (R) res;
898 			ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
899 			if (theForHistoryOperation) {
900 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE);
901 			}
902 		} else if (theForHistoryOperation) {
903 			/*
904 			 * 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.
905 			 */
906 			Date published = theEntity.getPublished().getValue();
907 			Date updated = theEntity.getUpdated().getValue();
908 			if (published.equals(updated)) {
909 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST);
910 			} else {
911 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT);
912 			}
913 		}
914 
915 		res.setId(theEntity.getIdDt().withVersion(theVersion.toString()));
916 
917 		ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion()));
918 		ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished());
919 		ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated());
920 		IDao.RESOURCE_PID.put(res, theEntity.getId());
921 
922 		Collection<? extends BaseTag> tags = theTagList;
923 		if (theEntity.isHasTags()) {
924 			TagList tagList = new TagList();
925 			List<IBaseCoding> securityLabels = new ArrayList<>();
926 			List<IdDt> profiles = new ArrayList<>();
927 			for (BaseTag next : tags) {
928 				switch (next.getTag().getTagType()) {
929 					case PROFILE:
930 						profiles.add(new IdDt(next.getTag().getCode()));
931 						break;
932 					case SECURITY_LABEL:
933 						IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt();
934 						secLabel.setSystem(next.getTag().getSystem());
935 						secLabel.setCode(next.getTag().getCode());
936 						secLabel.setDisplay(next.getTag().getDisplay());
937 						securityLabels.add(secLabel);
938 						break;
939 					case TAG:
940 						tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay()));
941 						break;
942 				}
943 			}
944 			if (tagList.size() > 0) {
945 				ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList);
946 			}
947 			if (securityLabels.size() > 0) {
948 				ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels));
949 			}
950 			if (profiles.size() > 0) {
951 				ResourceMetadataKeyEnum.PROFILES.put(res, profiles);
952 			}
953 		}
954 
955 		return retVal;
956 	}
957 
958 	@SuppressWarnings("unchecked")
959 	private <R extends IBaseResource> R populateResourceMetadataRi(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<? extends BaseTag> theTagList, boolean theForHistoryOperation, IAnyResource res, Long theVersion) {
960 		R retVal = (R) res;
961 		if (theEntity.getDeleted() != null) {
962 			res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance();
963 			retVal = (R) res;
964 			ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
965 			if (theForHistoryOperation) {
966 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.DELETE.toCode());
967 			}
968 		} else if (theForHistoryOperation) {
969 			/*
970 			 * 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.
971 			 */
972 			Date published = theEntity.getPublished().getValue();
973 			Date updated = theEntity.getUpdated().getValue();
974 			if (published.equals(updated)) {
975 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.POST.toCode());
976 			} else {
977 				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.PUT.toCode());
978 			}
979 		}
980 
981 		res.getMeta().getTag().clear();
982 		res.getMeta().getProfile().clear();
983 		res.getMeta().getSecurity().clear();
984 		res.getMeta().setLastUpdated(null);
985 		res.getMeta().setVersionId(null);
986 
987 		updateResourceMetadata(theEntity, res);
988 		res.setId(res.getIdElement().withVersion(theVersion.toString()));
989 
990 		res.getMeta().setLastUpdated(theEntity.getUpdatedDate());
991 		IDao.RESOURCE_PID.put(res, theEntity.getId());
992 
993 		Collection<? extends BaseTag> tags = theTagList;
994 
995 		if (theEntity.isHasTags()) {
996 			for (BaseTag next : tags) {
997 				switch (next.getTag().getTagType()) {
998 					case PROFILE:
999 						res.getMeta().addProfile(next.getTag().getCode());
1000 						break;
1001 					case SECURITY_LABEL:
1002 						IBaseCoding sec = res.getMeta().addSecurity();
1003 						sec.setSystem(next.getTag().getSystem());
1004 						sec.setCode(next.getTag().getCode());
1005 						sec.setDisplay(next.getTag().getDisplay());
1006 						break;
1007 					case TAG:
1008 						IBaseCoding tag = res.getMeta().addTag();
1009 						tag.setSystem(next.getTag().getSystem());
1010 						tag.setCode(next.getTag().getCode());
1011 						tag.setDisplay(next.getTag().getDisplay());
1012 						break;
1013 				}
1014 			}
1015 		}
1016 		return retVal;
1017 	}
1018 
1019 	/**
1020 	 * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
1021 	 *
1022 	 * @param theEntity The resource
1023 	 */
1024 	protected void postDelete(ResourceTable theEntity) {
1025 		// nothing
1026 	}
1027 
1028 	/**
1029 	 * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time.
1030 	 *
1031 	 * @param theEntity   The entity being updated (Do not modify the entity! Undefined behaviour will occur!)
1032 	 * @param theResource The resource being persisted
1033 	 */
1034 	protected void postPersist(ResourceTable theEntity, T theResource) {
1035 		// nothing
1036 	}
1037 
1038 	/**
1039 	 * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
1040 	 *
1041 	 * @param theEntity   The resource
1042 	 * @param theResource The resource being persisted
1043 	 */
1044 	protected void postUpdate(ResourceTable theEntity, T theResource) {
1045 		// nothing
1046 	}
1047 
1048 	@CoverageIgnore
1049 	public BaseHasResource readEntity(IIdType theValueId) {
1050 		throw new NotImplementedException("");
1051 	}
1052 
1053 	@Override
1054 	public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
1055 		/*
1056 		 * We do a null check here because Smile's module system tries to
1057 		 * initialize the application context twice if two modules depend on
1058 		 * the persistence module. The second time sets the dependency's appctx.
1059 		 */
1060 		if (myApplicationContext == null) {
1061 			myApplicationContext = theApplicationContext;
1062 		}
1063 	}
1064 
1065 	/**
1066 	 * 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.
1067 	 * <p>
1068 	 * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour.
1069 	 * </p>
1070 	 * <p>
1071 	 * 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.
1072 	 * </p>
1073 	 *
1074 	 * @param theTag The tag
1075 	 * @return Returns <code>true</code> if the tag should be removed
1076 	 */
1077 	protected boolean shouldDroppedTagBeRemovedOnUpdate(RequestDetails theRequest, ResourceTag theTag) {
1078 
1079 		Set<TagTypeEnum> metaSnapshotModeTokens = null;
1080 
1081 		if (theRequest != null) {
1082 			List<String> metaSnapshotMode = theRequest.getHeaders(JpaConstants.HEADER_META_SNAPSHOT_MODE);
1083 			if (metaSnapshotMode != null && !metaSnapshotMode.isEmpty()) {
1084 				metaSnapshotModeTokens = new HashSet<>();
1085 				for (String nextHeaderValue : metaSnapshotMode) {
1086 					StringTokenizer tok = new StringTokenizer(nextHeaderValue, ",");
1087 					while (tok.hasMoreTokens()) {
1088 						switch (trim(tok.nextToken())) {
1089 							case "TAG":
1090 								metaSnapshotModeTokens.add(TagTypeEnum.TAG);
1091 								break;
1092 							case "PROFILE":
1093 								metaSnapshotModeTokens.add(TagTypeEnum.PROFILE);
1094 								break;
1095 							case "SECURITY_LABEL":
1096 								metaSnapshotModeTokens.add(TagTypeEnum.SECURITY_LABEL);
1097 								break;
1098 						}
1099 					}
1100 				}
1101 			}
1102 		}
1103 
1104 		if (metaSnapshotModeTokens == null) {
1105 			metaSnapshotModeTokens = Collections.singleton(TagTypeEnum.PROFILE);
1106 		}
1107 
1108 		if (metaSnapshotModeTokens.contains(theTag.getTag().getTagType())) {
1109 			return true;
1110 		}
1111 
1112 		return false;
1113 	}
1114 
1115 	private ExpungeOutcome toExpungeOutcome(ExpungeOptions theExpungeOptions, AtomicInteger theRemainingCount) {
1116 		return new ExpungeOutcome()
1117 			.setDeletedCount(theExpungeOptions.getLimit() - theRemainingCount.get());
1118 	}
1119 
1120 	@Override
1121 	public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) {
1122 		RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType());
1123 		Class<? extends IBaseResource> resourceType = type.getImplementingClass();
1124 		return toResource(resourceType, theEntity, null, theForHistoryOperation);
1125 	}
1126 
1127 	@SuppressWarnings("unchecked")
1128 	@Override
1129 	public <R extends IBaseResource> R toResource(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<ResourceTag> theTagList, boolean theForHistoryOperation) {
1130 
1131 		// 1. get resource, it's encoding and the tags if any
1132 		byte[] resourceBytes = null;
1133 		ResourceEncodingEnum resourceEncoding = null;
1134 		Collection<? extends BaseTag> myTagList = null;
1135 		Long version = null;
1136 
1137 		if (theEntity instanceof ResourceHistoryTable) {
1138 			ResourceHistoryTable history = (ResourceHistoryTable) theEntity;
1139 			resourceBytes = history.getResource();
1140 			resourceEncoding = history.getEncoding();
1141 			myTagList = history.getTags();
1142 			version = history.getVersion();
1143 		} else if (theEntity instanceof ResourceTable) {
1144 			ResourceTable resource = (ResourceTable) theEntity;
1145 			version = theEntity.getVersion();
1146 			ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version);
1147 			while (history == null) {
1148 				if (version > 1L) {
1149 					version--;
1150 					history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version);
1151 				} else {
1152 					return null;
1153 				}
1154 			}
1155 			resourceBytes = history.getResource();
1156 			resourceEncoding = history.getEncoding();
1157 			myTagList = resource.getTags();
1158 			version = history.getVersion();
1159 		} else if (theEntity instanceof ResourceSearchView) {
1160 			// This is the search View
1161 			ResourceSearchView myView = (ResourceSearchView) theEntity;
1162 			resourceBytes = myView.getResource();
1163 			resourceEncoding = myView.getEncoding();
1164 			version = myView.getVersion();
1165 			if (theTagList == null)
1166 				myTagList = new HashSet<>();
1167 			else
1168 				myTagList = theTagList;
1169 		} else {
1170 			// something wrong
1171 			return null;
1172 		}
1173 
1174 		// 2. get The text
1175 		String resourceText = decodeResource(resourceBytes, resourceEncoding);
1176 
1177 		// 3. Use the appropriate custom type if one is specified in the context
1178 		Class<R> resourceType = theResourceType;
1179 		if (myContext.hasDefaultTypeForProfile()) {
1180 			for (BaseTag nextTag : myTagList) {
1181 				if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) {
1182 					String profile = nextTag.getTag().getCode();
1183 					if (isNotBlank(profile)) {
1184 						Class<? extends IBaseResource> newType = myContext.getDefaultTypeForProfile(profile);
1185 						if (newType != null && theResourceType.isAssignableFrom(newType)) {
1186 							ourLog.debug("Using custom type {} for profile: {}", newType.getName(), profile);
1187 							resourceType = (Class<R>) newType;
1188 							break;
1189 						}
1190 					}
1191 				}
1192 			}
1193 		}
1194 
1195 		// 4. parse the text to FHIR
1196 		R retVal;
1197 		if (resourceEncoding != ResourceEncodingEnum.DEL) {
1198 			IParser parser = resourceEncoding.newParser(getContext(theEntity.getFhirVersion()));
1199 			parser.setParserErrorHandler(new LenientErrorHandler(false).setErrorOnInvalidValue(false));
1200 
1201 			try {
1202 				retVal = parser.parseResource(resourceType, resourceText);
1203 			} catch (Exception e) {
1204 				StringBuilder b = new StringBuilder();
1205 				b.append("Failed to parse database resource[");
1206 				b.append(resourceType);
1207 				b.append("/");
1208 				b.append(theEntity.getIdDt().getIdPart());
1209 				b.append(" (pid ");
1210 				b.append(theEntity.getId());
1211 				b.append(", version ");
1212 				b.append(theEntity.getFhirVersion().name());
1213 				b.append("): ");
1214 				b.append(e.getMessage());
1215 				String msg = b.toString();
1216 				ourLog.error(msg, e);
1217 				throw new DataFormatException(msg, e);
1218 			}
1219 
1220 		} else {
1221 
1222 			retVal = (R) myContext.getResourceDefinition(theEntity.getResourceType()).newInstance();
1223 
1224 		}
1225 
1226 		// 5. fill MetaData
1227 		if (retVal instanceof IResource) {
1228 			IResource res = (IResource) retVal;
1229 			retVal = populateResourceMetadataHapi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version);
1230 		} else {
1231 			IAnyResource res = (IAnyResource) retVal;
1232 			retVal = populateResourceMetadataRi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version);
1233 		}
1234 
1235 		return retVal;
1236 	}
1237 
1238 	public String toResourceName(Class<? extends IBaseResource> theResourceType) {
1239 		return myContext.getResourceDefinition(theResourceType).getName();
1240 	}
1241 
1242 	String toResourceName(IBaseResource theResource) {
1243 		return myContext.getResourceDefinition(theResource).getName();
1244 	}
1245 
1246 	private Slice<Long> toSlice(ResourceHistoryTable theVersion) {
1247 		Validate.notNull(theVersion);
1248 		return new SliceImpl<>(Collections.singletonList(theVersion.getId()));
1249 	}
1250 
1251 	@SuppressWarnings("unchecked")
1252 	protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable
1253 		theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
1254 													 boolean theUpdateVersion, Date theUpdateTime, boolean theForceUpdate, boolean theCreateNewHistoryEntry) {
1255 		Validate.notNull(theEntity);
1256 		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());
1257 
1258 		ourLog.debug("Starting entity update");
1259 
1260 		/*
1261 		 * This should be the very first thing..
1262 		 */
1263 		if (theResource != null) {
1264 			if (thePerformIndexing) {
1265 				if (!ourValidationDisabledForUnitTest) {
1266 					validateResourceForStorage((T) theResource, theEntity);
1267 				}
1268 			}
1269 			String resourceType = myContext.getResourceDefinition(theResource).getName();
1270 			if (isNotBlank(theEntity.getResourceType()) && !theEntity.getResourceType().equals(resourceType)) {
1271 				throw new UnprocessableEntityException(
1272 					"Existing resource ID[" + theEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + theEntity.getResourceType() + "] - Cannot update with [" + resourceType + "]");
1273 			}
1274 		}
1275 
1276 		if (theEntity.getPublished() == null) {
1277 			ourLog.debug("Entity has published time: {}", new InstantDt(theUpdateTime));
1278 
1279 			theEntity.setPublished(theUpdateTime);
1280 		}
1281 
1282 		ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(theEntity);
1283 
1284 		ResourceIndexedSearchParams newParams = null;
1285 
1286 		EncodedResource changed;
1287 		if (theDeletedTimestampOrNull != null) {
1288 
1289 			newParams = new ResourceIndexedSearchParams();
1290 
1291 			theEntity.setDeleted(theDeletedTimestampOrNull);
1292 			theEntity.setUpdated(theDeletedTimestampOrNull);
1293 			theEntity.setNarrativeTextParsedIntoWords(null);
1294 			theEntity.setContentTextParsedIntoWords(null);
1295 			theEntity.setHashSha256(null);
1296 			theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
1297 			changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
1298 
1299 		} else {
1300 
1301 			theEntity.setDeleted(null);
1302 
1303 			if (thePerformIndexing) {
1304 
1305 				newParams = new ResourceIndexedSearchParams();
1306 				mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams);
1307 
1308 				changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
1309 
1310 				theEntity.setUpdated(theUpdateTime);
1311 				if (theResource instanceof IResource) {
1312 					theEntity.setLanguage(((IResource) theResource).getLanguage().getValue());
1313 				} else {
1314 					theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue());
1315 				}
1316 
1317 				newParams.setParamsOn(theEntity);
1318 				theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
1319 				populateFullTextFields(myContext, theResource, theEntity);
1320 			} else {
1321 
1322 				changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false);
1323 
1324 				theEntity.setUpdated(theUpdateTime);
1325 				theEntity.setIndexStatus(null);
1326 
1327 			}
1328 
1329 		}
1330 
1331 		if (!changed.isChanged() && !theForceUpdate && myConfig.isSuppressUpdatesWithNoChange()) {
1332 			ourLog.debug("Resource {} has not changed", theEntity.getIdDt().toUnqualified().getValue());
1333 			if (theResource != null) {
1334 				updateResourceMetadata(theEntity, theResource);
1335 			}
1336 			theEntity.setUnchangedInCurrentOperation(true);
1337 			return theEntity;
1338 		}
1339 
1340 		if (theUpdateVersion) {
1341 			theEntity.setVersion(theEntity.getVersion() + 1);
1342 		}
1343 
1344 		/*
1345 		 * Save the resource itself
1346 		 */
1347 		if (theEntity.getId() == null) {
1348 			myEntityManager.persist(theEntity);
1349 
1350 			if (theEntity.getForcedId() != null) {
1351 				myEntityManager.persist(theEntity.getForcedId());
1352 			}
1353 
1354 			postPersist(theEntity, (T) theResource);
1355 
1356 		} else if (theEntity.getDeleted() != null) {
1357 			theEntity = myEntityManager.merge(theEntity);
1358 
1359 			postDelete(theEntity);
1360 
1361 		} else {
1362 			theEntity = myEntityManager.merge(theEntity);
1363 
1364 			postUpdate(theEntity, (T) theResource);
1365 		}
1366 
1367 		/*
1368 		 * Create history entry
1369 		 */
1370 		if (theCreateNewHistoryEntry) {
1371 			final ResourceHistoryTable historyEntry = theEntity.toHistory();
1372 			historyEntry.setEncoding(changed.getEncoding());
1373 			historyEntry.setResource(changed.getResource());
1374 
1375 			ourLog.debug("Saving history entry {}", historyEntry.getIdDt());
1376 			myResourceHistoryTableDao.save(historyEntry);
1377 		}
1378 
1379 		/*
1380 		 * Update the "search param present" table which is used for the
1381 		 * ?foo:missing=true queries
1382 		 *
1383 		 * Note that we're only populating this for reference params
1384 		 * because the index tables for all other types have a MISSING column
1385 		 * right on them for handling the :missing queries. We can't use the
1386 		 * index table for resource links (reference indexes) because we index
1387 		 * those by path and not by parameter name.
1388 		 */
1389 		if (thePerformIndexing) {
1390 			Map<String, Boolean> presentSearchParams = new HashMap<>();
1391 			for (String nextKey : newParams.getPopulatedResourceLinkParameters()) {
1392 				presentSearchParams.put(nextKey, Boolean.TRUE);
1393 			}
1394 			Set<Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
1395 			for (Entry<String, RuntimeSearchParam> nextSpEntry : activeSearchParams) {
1396 				if (nextSpEntry.getValue().getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
1397 					if (!presentSearchParams.containsKey(nextSpEntry.getKey())) {
1398 						presentSearchParams.put(nextSpEntry.getKey(), Boolean.FALSE);
1399 					}
1400 				}
1401 			}
1402 			mySearchParamPresenceSvc.updatePresence(theEntity, presentSearchParams);
1403 		}
1404 
1405 		/*
1406 		 * Indexing
1407 		 */
1408 		if (thePerformIndexing) {
1409 			myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams);
1410 			mySearchParamWithInlineReferencesExtractor.storeCompositeStringUniques(newParams, theEntity, existingParams);
1411 		}
1412 
1413 		if (theResource != null) {
1414 			updateResourceMetadata(theEntity, theResource);
1415 		}
1416 
1417 
1418 		return theEntity;
1419 	}
1420 
1421 	protected ResourceTable updateEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable
1422 		entity, Date theDeletedTimestampOrNull, Date theUpdateTime) {
1423 		return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true);
1424 	}
1425 
1426 	public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion,
1427 													ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) {
1428 		// Notify interceptors
1429 		ActionRequestDetails requestDetails;
1430 		if (theRequestDetails != null) {
1431 			requestDetails = new ActionRequestDetails(theRequestDetails, theResource, theResourceId.getResourceType(), theResourceId);
1432 			notifyInterceptors(RestOperationTypeEnum.UPDATE, requestDetails);
1433 		}
1434 
1435 		// Notify IServerOperationInterceptors about pre-action call
1436 		if (theRequestDetails != null) {
1437 			theRequestDetails.getRequestOperationCallback().resourcePreUpdate(theOldResource, theResource);
1438 		}
1439 		for (IServerInterceptor next : getConfig().getInterceptors()) {
1440 			if (next instanceof IServerOperationInterceptor) {
1441 				((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource);
1442 			}
1443 		}
1444 		HookParams hookParams = new HookParams()
1445 			.add(IBaseResource.class, theOldResource)
1446 			.add(IBaseResource.class, theResource);
1447 		myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, hookParams);
1448 
1449 		// Perform update
1450 		ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing);
1451 
1452 		/*
1453 		 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
1454 		 * we'll manually increase the version. This is important because we want the updated version number
1455 		 * to be reflected in the resource shared with interceptors
1456 		 */
1457 		if (!thePerformIndexing && !savedEntity.isUnchangedInCurrentOperation() && !ourDisableIncrementOnUpdateForUnitTest) {
1458 			if (theResourceId.hasVersionIdPart() == false) {
1459 				theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion()));
1460 			}
1461 			incrementId(theResource, savedEntity, theResourceId);
1462 		}
1463 
1464 		// Update version/lastUpdated so that interceptors see the correct version
1465 		updateResourceMetadata(savedEntity, theResource);
1466 
1467 		// Notify interceptors
1468 		if (!savedEntity.isUnchangedInCurrentOperation()) {
1469 			if (theRequestDetails != null) {
1470 				theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
1471 				theRequestDetails.getRequestOperationCallback().resourceUpdated(theOldResource, theResource);
1472 			}
1473 			for (IServerInterceptor next : getConfig().getInterceptors()) {
1474 				if (next instanceof IServerOperationInterceptor) {
1475 					((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource);
1476 					((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource);
1477 				}
1478 			}
1479 			TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
1480 				@Override
1481 				public void beforeCommit(boolean readOnly) {
1482 					HookParams hookParams = new HookParams()
1483 						.add(IBaseResource.class, theOldResource)
1484 						.add(IBaseResource.class, theResource);
1485 					myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, hookParams);
1486 				}
1487 			});
1488 		}
1489 
1490 		return savedEntity;
1491 	}
1492 
1493 	protected void updateResourceMetadata(IBaseResourceEntity theEntity, IBaseResource theResource) {
1494 		IIdType id = theEntity.getIdDt();
1495 		if (getContext().getVersion().getVersion().isRi()) {
1496 			id = getContext().getVersion().newIdType().setValue(id.getValue());
1497 		}
1498 
1499 		if (id.hasResourceType() == false) {
1500 			id = id.withResourceType(theEntity.getResourceType());
1501 		}
1502 
1503 		theResource.setId(id);
1504 		if (theResource instanceof IResource) {
1505 			ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, id.getVersionIdPart());
1506 			ResourceMetadataKeyEnum.UPDATED.put((IResource) theResource, theEntity.getUpdated());
1507 		} else {
1508 			IBaseMetaType meta = theResource.getMeta();
1509 			meta.setVersionId(id.getVersionIdPart());
1510 			meta.setLastUpdated(theEntity.getUpdatedDate());
1511 		}
1512 	}
1513 
1514 	private void validateChildReferences(IBase theElement, String thePath) {
1515 		if (theElement == null) {
1516 			return;
1517 		}
1518 		BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
1519 		if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
1520 			return;
1521 		}
1522 
1523 		BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def;
1524 		for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) {
1525 
1526 			List<IBase> values = nextChildDef.getAccessor().getValues(theElement);
1527 			if (values == null || values.isEmpty()) {
1528 				continue;
1529 			}
1530 
1531 			String newPath = thePath + "." + nextChildDef.getElementName();
1532 
1533 			for (IBase nextChild : values) {
1534 				validateChildReferences(nextChild, newPath);
1535 			}
1536 
1537 			if (nextChildDef instanceof RuntimeChildResourceDefinition) {
1538 				RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef;
1539 				Set<String> validTypes = new HashSet<>();
1540 				boolean allowAny = false;
1541 				for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) {
1542 					if (nextValidType.isInterface()) {
1543 						allowAny = true;
1544 						break;
1545 					}
1546 					validTypes.add(getContext().getResourceDefinition(nextValidType).getName());
1547 				}
1548 
1549 				if (allowAny) {
1550 					continue;
1551 				}
1552 
1553 				for (IBase nextChild : values) {
1554 					IBaseReference nextRef = (IBaseReference) nextChild;
1555 					IIdType referencedId = nextRef.getReferenceElement();
1556 					if (!isBlank(referencedId.getResourceType())) {
1557 						if (!isLogicalReference(referencedId)) {
1558 							if (!referencedId.getValue().contains("?")) {
1559 								if (!validTypes.contains(referencedId.getResourceType())) {
1560 									throw new UnprocessableEntityException(
1561 										"Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path");
1562 								}
1563 							}
1564 						}
1565 					}
1566 				}
1567 
1568 			}
1569 		}
1570 	}
1571 
1572 	public void validateDeleteConflictsEmptyOrThrowException(List<DeleteConflict> theDeleteConflicts) {
1573 		if (theDeleteConflicts.isEmpty()) {
1574 			return;
1575 		}
1576 
1577 		IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
1578 		String firstMsg = null;
1579 		for (DeleteConflict next : theDeleteConflicts) {
1580 			StringBuilder b = new StringBuilder();
1581 			b.append("Unable to delete ");
1582 			b.append(next.getTargetId().toUnqualifiedVersionless().getValue());
1583 			b.append(" because at least one resource has a reference to this resource. First reference found was resource ");
1584 			b.append(next.getTargetId().toUnqualifiedVersionless().getValue());
1585 			b.append(" in path ");
1586 			b.append(next.getSourcePath());
1587 			String msg = b.toString();
1588 			if (firstMsg == null) {
1589 				firstMsg = msg;
1590 			}
1591 			OperationOutcomeUtil.addIssue(getContext(), oo, OO_SEVERITY_ERROR, msg, null, "processing");
1592 		}
1593 
1594 		throw new ResourceVersionConflictException(firstMsg, oo);
1595 	}
1596 
1597 	protected void validateMetaCount(int theMetaCount) {
1598 		if (myConfig.getResourceMetaCountHardLimit() != null) {
1599 			if (theMetaCount > myConfig.getResourceMetaCountHardLimit()) {
1600 				throw new UnprocessableEntityException("Resource contains " + theMetaCount + " meta entries (tag/profile/security label), maximum is " + myConfig.getResourceMetaCountHardLimit());
1601 			}
1602 		}
1603 	}
1604 
1605 	/**
1606 	 * 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
1607 	 * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check.
1608 	 *
1609 	 * @param theResource     The resource that is about to be persisted
1610 	 * @param theEntityToSave TODO
1611 	 */
1612 	protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) {
1613 		Object tag = null;
1614 
1615 		int totalMetaCount = 0;
1616 
1617 		if (theResource instanceof IResource) {
1618 			IResource res = (IResource) theResource;
1619 			TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res);
1620 			if (tagList != null) {
1621 				tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1622 				totalMetaCount += tagList.size();
1623 			}
1624 			List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res);
1625 			if (profileList != null) {
1626 				totalMetaCount += profileList.size();
1627 			}
1628 		} else {
1629 			IAnyResource res = (IAnyResource) theResource;
1630 			tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1631 			totalMetaCount += res.getMeta().getTag().size();
1632 			totalMetaCount += res.getMeta().getProfile().size();
1633 			totalMetaCount += res.getMeta().getSecurity().size();
1634 		}
1635 
1636 		if (tag != null) {
1637 			throw new UnprocessableEntityException("Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data");
1638 		}
1639 
1640 		String resName = getContext().getResourceDefinition(theResource).getName();
1641 		validateChildReferences(theResource, resName);
1642 
1643 		validateMetaCount(totalMetaCount);
1644 
1645 	}
1646 
1647 	@Override
1648 	public ISearchParamRegistry getSearchParamRegistry() {
1649 		return mySearchParamRegistry;
1650 	}
1651 
1652 	public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
1653 		if (theRequestDetails != null) {
1654 			theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
1655 		}
1656 	}
1657 
1658 	public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
1659 		if (theRequestDetails != null) {
1660 			theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE);
1661 		}
1662 	}
1663 
1664 	public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) {
1665 		StringBuilder retVal = new StringBuilder();
1666 		@SuppressWarnings("rawtypes")
1667 		List<IPrimitiveType> childElements = theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class);
1668 		for (@SuppressWarnings("rawtypes")
1669 			IPrimitiveType nextType : childElements) {
1670 			if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) {
1671 				String nextValue = nextType.getValueAsString();
1672 				if (isNotBlank(nextValue)) {
1673 					retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
1674 					retVal.append("\n");
1675 				}
1676 			}
1677 		}
1678 		return retVal.toString();
1679 	}
1680 
1681 	public static void populateFullTextFields(final FhirContext theContext, final IBaseResource theResource, ResourceTable theEntity) {
1682 		if (theEntity.getDeleted() != null) {
1683 			theEntity.setNarrativeTextParsedIntoWords(null);
1684 			theEntity.setContentTextParsedIntoWords(null);
1685 		} else {
1686 			theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource));
1687 			theEntity.setContentTextParsedIntoWords(parseContentTextIntoWords(theContext, theResource));
1688 		}
1689 	}
1690 
1691 	public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
1692 		String resourceText = null;
1693 		switch (theResourceEncoding) {
1694 			case JSON:
1695 				resourceText = new String(theResourceBytes, Charsets.UTF_8);
1696 				break;
1697 			case JSONC:
1698 				resourceText = GZipUtil.decompress(theResourceBytes);
1699 				break;
1700 			case DEL:
1701 				break;
1702 		}
1703 		return resourceText;
1704 	}
1705 
1706 	public static byte[] encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, Set<String> theExcludeElements, FhirContext theContext) {
1707 		byte[] bytes;
1708 		IParser parser = theEncoding.newParser(theContext);
1709 		parser.setDontEncodeElements(theExcludeElements);
1710 		String encoded = parser.encodeResourceToString(theResource);
1711 
1712 
1713 		switch (theEncoding) {
1714 			case JSON:
1715 				bytes = encoded.getBytes(Charsets.UTF_8);
1716 				break;
1717 			case JSONC:
1718 				bytes = GZipUtil.compress(encoded);
1719 				break;
1720 			default:
1721 			case DEL:
1722 				bytes = new byte[0];
1723 				break;
1724 		}
1725 
1726 		ourLog.debug("Encoded {} chars of resource body as {} bytes", encoded.length(), bytes.length);
1727 		return bytes;
1728 	}
1729 
1730 	private static String parseNarrativeTextIntoWords(IBaseResource theResource) {
1731 
1732 		StringBuilder b = new StringBuilder();
1733 		if (theResource instanceof IResource) {
1734 			IResource resource = (IResource) theResource;
1735 			List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
1736 			if (xmlEvents != null) {
1737 				for (XMLEvent next : xmlEvents) {
1738 					if (next.isCharacters()) {
1739 						Characters characters = next.asCharacters();
1740 						b.append(characters.getData()).append(" ");
1741 					}
1742 				}
1743 			}
1744 		} else if (theResource instanceof IDomainResource) {
1745 			IDomainResource resource = (IDomainResource) theResource;
1746 			try {
1747 				String divAsString = resource.getText().getDivAsString();
1748 				List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
1749 				if (xmlEvents != null) {
1750 					for (XMLEvent next : xmlEvents) {
1751 						if (next.isCharacters()) {
1752 							Characters characters = next.asCharacters();
1753 							b.append(characters.getData()).append(" ");
1754 						}
1755 					}
1756 				}
1757 			} catch (Exception e) {
1758 				throw new DataFormatException("Unable to convert DIV to string", e);
1759 			}
1760 
1761 		}
1762 		return b.toString();
1763 	}
1764 
1765 	@VisibleForTesting
1766 	public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) {
1767 		ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest;
1768 	}
1769 
1770 	/**
1771 	 * Do not call this method outside of unit tests
1772 	 */
1773 	@VisibleForTesting
1774 	public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
1775 		ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
1776 	}
1777 
1778 	private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) {
1779 		ArrayList<BaseCodingDt> retVal = new ArrayList<>(theSecurityLabels.size());
1780 		for (IBaseCoding next : theSecurityLabels) {
1781 			retVal.add((BaseCodingDt) next);
1782 		}
1783 		return retVal;
1784 	}
1785 
1786 	public static void validateResourceType(BaseHasResource theEntity, String theResourceName) {
1787 		if (!theResourceName.equals(theEntity.getResourceType())) {
1788 			throw new ResourceNotFoundException(
1789 				"Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType());
1790 		}
1791 	}
1792 }