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