View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   /*
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2018 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.context.ConfigurationException;
24  import ca.uhn.fhir.context.FhirVersionEnum;
25  import ca.uhn.fhir.context.RuntimeResourceDefinition;
26  import ca.uhn.fhir.context.RuntimeSearchParam;
27  import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
28  import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
29  import ca.uhn.fhir.jpa.entity.*;
30  import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
31  import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
32  import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
33  import ca.uhn.fhir.jpa.util.DeleteConflict;
34  import ca.uhn.fhir.jpa.util.ExpungeOptions;
35  import ca.uhn.fhir.jpa.util.ExpungeOutcome;
36  import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
37  import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
38  import ca.uhn.fhir.model.api.*;
39  import ca.uhn.fhir.model.primitive.IdDt;
40  import ca.uhn.fhir.rest.api.*;
41  import ca.uhn.fhir.rest.api.server.IBundleProvider;
42  import ca.uhn.fhir.rest.api.server.RequestDetails;
43  import ca.uhn.fhir.rest.param.ParameterUtil;
44  import ca.uhn.fhir.rest.param.QualifierDetails;
45  import ca.uhn.fhir.rest.server.exceptions.*;
46  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
47  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
48  import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
49  import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
50  import ca.uhn.fhir.util.*;
51  import org.apache.commons.lang3.Validate;
52  import org.hl7.fhir.instance.model.api.*;
53  import org.hl7.fhir.r4.model.InstantType;
54  import org.springframework.beans.factory.annotation.Autowired;
55  import org.springframework.beans.factory.annotation.Required;
56  import org.springframework.transaction.PlatformTransactionManager;
57  import org.springframework.transaction.TransactionDefinition;
58  import org.springframework.transaction.annotation.Propagation;
59  import org.springframework.transaction.annotation.Transactional;
60  import org.springframework.transaction.support.TransactionTemplate;
61  
62  import javax.annotation.PostConstruct;
63  import javax.persistence.NoResultException;
64  import javax.persistence.TypedQuery;
65  import javax.servlet.http.HttpServletResponse;
66  import java.util.*;
67  
68  import static org.apache.commons.lang3.StringUtils.isNotBlank;
69  
70  @Transactional(propagation = Propagation.REQUIRED)
71  public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
72  
73  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class);
74  	@Autowired
75  	protected PlatformTransactionManager myPlatformTransactionManager;
76  	@Autowired(required = false)
77  	protected IFulltextSearchSvc mySearchDao;
78  	@Autowired()
79  	protected ISearchResultDao mySearchResultDao;
80  	@Autowired
81  	protected DaoConfig myDaoConfig;
82  	@Autowired
83  	private IResourceLinkDao myResourceLinkDao;
84  	private String myResourceName;
85  	private Class<T> myResourceType;
86  	private String mySecondaryPrimaryKeyParamName;
87  	@Autowired
88  	private ISearchParamRegistry mySearchParamRegistry;
89  
90  	@Override
91  	public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
92  		StopWatch w = new StopWatch();
93  		BaseHasResource entity = readEntity(theId);
94  		if (entity == null) {
95  			throw new ResourceNotFoundException(theId);
96  		}
97  
98  		for (BaseTag next : new ArrayList<>(entity.getTags())) {
99  			if (ObjectUtil.equals(next.getTag().getTagType(), theTagType) &&
100 				ObjectUtil.equals(next.getTag().getSystem(), theScheme) &&
101 				ObjectUtil.equals(next.getTag().getCode(), theTerm)) {
102 				return;
103 			}
104 		}
105 
106 		entity.setHasTags(true);
107 
108 		TagDefinition def = getTagOrNull(TagTypeEnum.TAG, theScheme, theTerm, theLabel);
109 		if (def != null) {
110 			BaseTag newEntity = entity.addTag(def);
111 			if (newEntity.getTagId() == null) {
112 				myEntityManager.persist(newEntity);
113 				myEntityManager.merge(entity);
114 			}
115 		}
116 
117 		ourLog.debug("Processed addTag {}/{} on {} in {}ms", theScheme, theTerm, theId, w.getMillisAndRestart());
118 	}
119 
120 	@Override
121 	public DaoMethodOutcome create(final T theResource) {
122 		return create(theResource, null, true, null);
123 	}
124 
125 	@Override
126 	public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) {
127 		return create(theResource, null, true, theRequestDetails);
128 	}
129 
130 	@Override
131 	public DaoMethodOutcome create(final T theResource, String theIfNoneExist) {
132 		return create(theResource, theIfNoneExist, null);
133 	}
134 
135 	@Override
136 	public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, RequestDetails theRequestDetails) {
137 		if (isNotBlank(theResource.getIdElement().getIdPart())) {
138 			if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
139 				String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
140 				throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing"));
141 			} else {
142 				// As of DSTU3, ID and version in the body should be ignored for a create/update
143 				theResource.setId("");
144 			}
145 		}
146 
147 		if (myDaoConfig.getResourceServerIdStrategy() == DaoConfig.IdStrategyEnum.UUID) {
148 			theResource.setId(UUID.randomUUID().toString());
149 		}
150 
151 		return doCreate(theResource, theIfNoneExist, thePerformIndexing, new Date(), theRequestDetails);
152 	}
153 
154 	@Override
155 	public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) {
156 		return create(theResource, theIfNoneExist, true, theRequestDetails);
157 	}
158 
159 	public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) {
160 		return createOperationOutcome(OO_SEVERITY_ERROR, theMessage, theCode);
161 	}
162 
163 	public IBaseOperationOutcome createInfoOperationOutcome(String theMessage) {
164 		return createOperationOutcome(OO_SEVERITY_INFO, theMessage, "informational");
165 	}
166 
167 	protected abstract IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode);
168 
169 	@Override
170 	public DaoMethodOutcome delete(IIdType theId) {
171 		return delete(theId, null);
172 	}
173 
174 	@Override
175 	public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> theDeleteConflicts, RequestDetails theReques) {
176 		if (theId == null || !theId.hasIdPart()) {
177 			throw new InvalidRequestException("Can not perform delete, no ID provided");
178 		}
179 		final ResourceTable entity = readEntityLatestVersion(theId);
180 		if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
181 			throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version");
182 		}
183 
184 		// Don't delete again if it's already deleted
185 		if (entity.getDeleted() != null) {
186 			DaoMethodOutcome outcome = new DaoMethodOutcome();
187 			outcome.setEntity(entity);
188 
189 			IIdType id = getContext().getVersion().newIdType();
190 			id.setValue(entity.getIdDt().getValue());
191 			outcome.setId(id);
192 
193 			IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
194 			String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, 0);
195 			String severity = "information";
196 			String code = "informational";
197 			OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
198 			outcome.setOperationOutcome(oo);
199 
200 			return outcome;
201 		}
202 
203 		StopWatch w = new StopWatch();
204 
205 		T resourceToDelete = toResource(myResourceType, entity, null, false);
206 
207 		// Notify IServerOperationInterceptors about pre-action call
208 		if (theReques != null) {
209 			theReques.getRequestOperationCallback().resourcePreDelete(resourceToDelete);
210 		}
211 		for (IServerInterceptor next : getConfig().getInterceptors()) {
212 			if (next instanceof IServerOperationInterceptor) {
213 				((IServerOperationInterceptor) next).resourcePreDelete(theReques, resourceToDelete);
214 			}
215 		}
216 
217 		validateOkToDelete(theDeleteConflicts, entity, false);
218 
219 		preDelete(resourceToDelete, entity);
220 
221 		// Notify interceptors
222 		if (theReques != null) {
223 			ActionRequestDetails requestDetails = new ActionRequestDetails(theReques, getContext(), theId.getResourceType(), theId);
224 			notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails);
225 		}
226 
227 		Date updateTime = new Date();
228 		ResourceTable savedEntity = updateEntity(theReques, null, entity, updateTime, updateTime);
229 		resourceToDelete.setId(entity.getIdDt());
230 
231 		// Notify JPA interceptors
232 		if (theReques != null) {
233 			ActionRequestDetails requestDetails = new ActionRequestDetails(theReques, getContext(), theId.getResourceType(), theId);
234 			theReques.getRequestOperationCallback().resourceDeleted(resourceToDelete);
235 		}
236 		for (IServerInterceptor next : getConfig().getInterceptors()) {
237 			if (next instanceof IServerOperationInterceptor) {
238 				((IServerOperationInterceptor) next).resourceDeleted(theReques, resourceToDelete);
239 			}
240 		}
241 
242 		DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true);
243 
244 		IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
245 		String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis());
246 		String severity = "information";
247 		String code = "informational";
248 		OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
249 		outcome.setOperationOutcome(oo);
250 
251 		return outcome;
252 	}
253 
254 	@Override
255 	public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) {
256 		List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
257 		StopWatch w = new StopWatch();
258 
259 		DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails);
260 
261 		validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
262 
263 		ourLog.debug("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
264 		return retVal;
265 	}
266 
267 	/**
268 	 * This method gets called by {@link #deleteByUrl(String, List, RequestDetails)} as well as by
269 	 * transaction processors
270 	 */
271 	@Override
272 	public DeleteMethodOutcome deleteByUrl(String theUrl, List<DeleteConflict> deleteConflicts, RequestDetails theRequest) {
273 		StopWatch w = new StopWatch();
274 
275 		Set<Long> resource = processMatchUrl(theUrl, myResourceType);
276 		if (resource.size() > 1) {
277 			if (myDaoConfig.isAllowMultipleDelete() == false) {
278 				throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size()));
279 			}
280 		}
281 
282 		List<ResourceTable> deletedResources = new ArrayList<ResourceTable>();
283 		for (Long pid : resource) {
284 			ResourceTable entity = myEntityManager.find(ResourceTable.class, pid);
285 			deletedResources.add(entity);
286 
287 			T resourceToDelete = toResource(myResourceType, entity, null, false);
288 
289 			// Notify IServerOperationInterceptors about pre-action call
290 			if (theRequest != null) {
291 				theRequest.getRequestOperationCallback().resourcePreDelete(resourceToDelete);
292 			}
293 			for (IServerInterceptor next : getConfig().getInterceptors()) {
294 				if (next instanceof IServerOperationInterceptor) {
295 					((IServerOperationInterceptor) next).resourcePreDelete(theRequest, resourceToDelete);
296 				}
297 			}
298 
299 			validateOkToDelete(deleteConflicts, entity, false);
300 
301 			// Notify interceptors
302 			IdDt idToDelete = entity.getIdDt();
303 			if (theRequest != null) {
304 				ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete);
305 				notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails);
306 			}
307 
308 			// Perform delete
309 			Date updateTime = new Date();
310 			updateEntity(theRequest, null, entity, updateTime, updateTime);
311 			resourceToDelete.setId(entity.getIdDt());
312 
313 			// Notify JPA interceptors
314 			if (theRequest != null) {
315 				theRequest.getRequestOperationCallback().resourceDeleted(resourceToDelete);
316 				ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete);
317 			}
318 			for (IServerInterceptor next : getConfig().getInterceptors()) {
319 				if (next instanceof IServerOperationInterceptor) {
320 					((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete);
321 				}
322 			}
323 		}
324 
325 		IBaseOperationOutcome oo;
326 		if (deletedResources.isEmpty()) {
327 			oo = OperationOutcomeUtil.newInstance(getContext());
328 			String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl);
329 			String severity = "warning";
330 			String code = "not-found";
331 			OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
332 		} else {
333 			oo = OperationOutcomeUtil.newInstance(getContext());
334 			String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", deletedResources.size(), w.getMillis());
335 			String severity = "information";
336 			String code = "informational";
337 			OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
338 		}
339 
340 		ourLog.debug("Processed delete on {} (matched {} resource(s)) in {}ms", theUrl, deletedResources.size(), w.getMillis());
341 
342 		DeleteMethodOutcome retVal = new DeleteMethodOutcome();
343 		retVal.setDeletedEntities(deletedResources);
344 		retVal.setOperationOutcome(oo);
345 		return retVal;
346 	}
347 
348 	@Override
349 	public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) {
350 		List<DeleteConflict> deleteConflicts = new ArrayList<>();
351 
352 		DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequestDetails);
353 
354 		validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
355 
356 		return outcome;
357 	}
358 
359 	@PostConstruct
360 	public void detectSearchDaoDisabled() {
361 		if (mySearchDao != null && mySearchDao.isDisabled()) {
362 			mySearchDao = null;
363 		}
364 	}
365 
366 	private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTime, RequestDetails theRequest) {
367 		StopWatch w = new StopWatch();
368 
369 		preProcessResourceForStorage(theResource);
370 
371 		ResourceTable entity = new ResourceTable();
372 		entity.setResourceType(toResourceName(theResource));
373 
374 		if (isNotBlank(theIfNoneExist)) {
375 			Set<Long> match = processMatchUrl(theIfNoneExist, myResourceType);
376 			if (match.size() > 1) {
377 				String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size());
378 				throw new PreconditionFailedException(msg);
379 			} else if (match.size() == 1) {
380 				Long pid = match.iterator().next();
381 				entity = myEntityManager.find(ResourceTable.class, pid);
382 				return toMethodOutcome(entity, theResource).setCreated(false);
383 			}
384 		}
385 
386 		boolean serverAssignedId;
387 		if (isNotBlank(theResource.getIdElement().getIdPart())) {
388 			switch (myDaoConfig.getResourceClientIdStrategy()) {
389 				case NOT_ALLOWED:
390 					throw new ResourceNotFoundException(
391 						getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart()));
392 				case ALPHANUMERIC:
393 					if (theResource.getIdElement().isIdPartValidLong()) {
394 						throw new InvalidRequestException(
395 							getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart()));
396 					}
397 					createForcedIdIfNeeded(entity, theResource.getIdElement(), false);
398 					break;
399 				case ANY:
400 					createForcedIdIfNeeded(entity, theResource.getIdElement(), true);
401 					break;
402 			}
403 			serverAssignedId = false;
404 		} else {
405 			serverAssignedId = true;
406 		}
407 
408 		// Notify interceptors
409 		if (theRequest != null) {
410 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theResource);
411 			notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails);
412 		}
413 
414 		// Notify JPA interceptors
415 		if (theRequest != null) {
416 			theRequest.getRequestOperationCallback().resourcePreCreate(theResource);
417 		}
418 		for (IServerInterceptor next : getConfig().getInterceptors()) {
419 			if (next instanceof IServerOperationInterceptor) {
420 				((IServerOperationInterceptor) next).resourcePreCreate(theRequest, theResource);
421 			}
422 		}
423 
424 		// Perform actual DB update
425 		ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing);
426 
427 		theResource.setId(entity.getIdDt());
428 		if (serverAssignedId) {
429 			switch (myDaoConfig.getResourceClientIdStrategy()) {
430 				case NOT_ALLOWED:
431 				case ALPHANUMERIC:
432 					break;
433 				case ANY:
434 					ForcedId forcedId = createForcedIdIfNeeded(updatedEntity, theResource.getIdElement(), true);
435 					if (forcedId != null) {
436 						myForcedIdDao.save(forcedId);
437 					}
438 					break;
439 			}
440 		}
441 
442 		/*
443 		 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
444 		 * we'll manually increase the version. This is important because we want the updated version number
445 		 * to be reflected in the resource shared with interceptors
446 		 */
447 		if (!thePerformIndexing) {
448 			incrementId(theResource, entity, theResource.getIdElement());
449 		}
450 
451 		// Notify JPA interceptors
452 		if (!updatedEntity.isUnchangedInCurrentOperation()) {
453 			if (theRequest != null) {
454 				theRequest.getRequestOperationCallback().resourceCreated(theResource);
455 			}
456 			for (IServerInterceptor next : getConfig().getInterceptors()) {
457 				if (next instanceof IServerOperationInterceptor) {
458 					((IServerOperationInterceptor) next).resourceCreated(theRequest, theResource);
459 				}
460 			}
461 		}
462 
463 		DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true);
464 		if (!thePerformIndexing) {
465 			outcome.setId(theResource.getIdElement());
466 		}
467 
468 		String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
469 		outcome.setOperationOutcome(createInfoOperationOutcome(msg));
470 
471 		ourLog.debug(msg);
472 		return outcome;
473 	}
474 
475 	private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource entity) {
476 		List<TagDefinition> tags = toTagList(theMetaAdd);
477 
478 		for (TagDefinition nextDef : tags) {
479 
480 			boolean hasTag = false;
481 			for (BaseTag next : new ArrayList<>(entity.getTags())) {
482 				if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
483 					ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
484 					ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
485 					hasTag = true;
486 					break;
487 				}
488 			}
489 
490 			if (!hasTag) {
491 				entity.setHasTags(true);
492 
493 				TagDefinition def = getTagOrNull(nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay());
494 				if (def != null) {
495 					BaseTag newEntity = entity.addTag(def);
496 					if (newEntity.getTagId() == null) {
497 						myEntityManager.persist(newEntity);
498 					}
499 				}
500 			}
501 		}
502 
503 		validateMetaCount(entity.getTags().size());
504 
505 		myEntityManager.merge(entity);
506 	}
507 
508 	private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource entity) {
509 		List<TagDefinition> tags = toTagList(theMetaDel);
510 
511 		//@formatter:off
512 		for (TagDefinition nextDef : tags) {
513 			for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
514 				if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
515 					ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
516 					ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
517 					myEntityManager.remove(next);
518 					entity.getTags().remove(next);
519 				}
520 			}
521 		}
522 		//@formatter:on
523 
524 		if (entity.getTags().isEmpty()) {
525 			entity.setHasTags(false);
526 		}
527 
528 		myEntityManager.merge(entity);
529 	}
530 
531 	@Override
532 	@Transactional(propagation = Propagation.NEVER)
533 	public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions) {
534 
535 		TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
536 
537 		BaseHasResource entity = txTemplate.execute(t->readEntity(theId));
538 		if (theId.hasVersionIdPart()) {
539 			BaseHasResource currentVersion;
540 			currentVersion = txTemplate.execute(t->readEntity(theId.toVersionless()));
541 			if (entity.getVersion() == currentVersion.getVersion()) {
542 				throw new PreconditionFailedException("Can not perform version-specific expunge of resource " + theId.toUnqualified().getValue() + " as this is the current version");
543 			}
544 
545 			return doExpunge(getResourceName(), entity.getResourceId(), entity.getVersion(), theExpungeOptions);
546 		}
547 
548 		return doExpunge(getResourceName(), entity.getResourceId(), null, theExpungeOptions);
549 	}
550 
551 	@Override
552 	@Transactional(propagation = Propagation.NEVER)
553 	public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions) {
554 		ourLog.info("Beginning TYPE[{}] expunge operation", getResourceName());
555 
556 		return doExpunge(getResourceName(), null, null, theExpungeOptions);
557 	}
558 
559 	@Override
560 	public TagList getAllResourceTags(RequestDetails theRequestDetails) {
561 		// Notify interceptors
562 		ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
563 		notifyInterceptors(RestOperationTypeEnum.GET_TAGS, requestDetails);
564 
565 		StopWatch w = new StopWatch();
566 		TagList tags = super.getTags(myResourceType, null);
567 		ourLog.debug("Processed getTags on {} in {}ms", myResourceName, w.getMillisAndRestart());
568 		return tags;
569 	}
570 
571 	public String getResourceName() {
572 		return myResourceName;
573 	}
574 
575 	@Override
576 	public Class<T> getResourceType() {
577 		return myResourceType;
578 	}
579 
580 	@SuppressWarnings("unchecked")
581 	@Required
582 	public void setResourceType(Class<? extends IBaseResource> theTableType) {
583 		myResourceType = (Class<T>) theTableType;
584 	}
585 
586 	@Override
587 	public TagList getTags(IIdType theResourceId, RequestDetails theRequestDetails) {
588 		// Notify interceptors
589 		ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, null, theResourceId);
590 		notifyInterceptors(RestOperationTypeEnum.GET_TAGS, requestDetails);
591 
592 		StopWatch w = new StopWatch();
593 		TagList retVal = super.getTags(myResourceType, theResourceId);
594 		ourLog.debug("Processed getTags on {} in {}ms", theResourceId, w.getMillisAndRestart());
595 		return retVal;
596 	}
597 
598 	@Override
599 	public IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails) {
600 		// Notify interceptors
601 		ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
602 		notifyInterceptors(RestOperationTypeEnum.HISTORY_TYPE, requestDetails);
603 
604 		StopWatch w = new StopWatch();
605 		IBundleProvider retVal = super.history(myResourceName, null, theSince, theUntil);
606 		ourLog.debug("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart());
607 		return retVal;
608 	}
609 
610 	@Override
611 	public IBundleProvider history(final IIdType theId, final Date theSince, Date theUntil, RequestDetails theRequestDetails) {
612 		// Notify interceptors
613 		ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
614 		notifyInterceptors(RestOperationTypeEnum.HISTORY_INSTANCE, requestDetails);
615 
616 		StopWatch w = new StopWatch();
617 
618 		IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless();
619 		BaseHasResource entity = readEntity(id);
620 
621 		IBundleProvider retVal = super.history(myResourceName, entity.getId(), theSince, theUntil);
622 
623 		ourLog.debug("Processed history on {} in {}ms", id, w.getMillisAndRestart());
624 		return retVal;
625 	}
626 
627 	protected boolean isPagingProviderDatabaseBacked(RequestDetails theRequestDetails) {
628 		if (theRequestDetails == null || theRequestDetails.getServer() == null) {
629 			return false;
630 		}
631 		return theRequestDetails.getServer().getPagingProvider() instanceof DatabaseBackedPagingProvider;
632 	}
633 
634 	protected void markResourcesMatchingExpressionAsNeedingReindexing(Boolean theCurrentlyReindexing, String theExpression) {
635 		// Avoid endless loops
636 		if (Boolean.TRUE.equals(theCurrentlyReindexing)) {
637 			return;
638 		}
639 
640 		if (myDaoConfig.isMarkResourcesForReindexingUponSearchParameterChange()) {
641 			if (isNotBlank(theExpression) && theExpression.contains(".")) {
642 				final String resourceType = theExpression.substring(0, theExpression.indexOf('.'));
643 				ourLog.debug("Marking all resources of type {} for reindexing due to updated search parameter with path: {}", resourceType, theExpression);
644 
645 				TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
646 				txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
647 				txTemplate.execute(t->{
648 					myResourceReindexingSvc.markAllResourcesForReindexing(resourceType);
649 					return null;
650 				});
651 
652 				ourLog.debug("Marked resources of type {} for reindexing", resourceType);
653 			}
654 		}
655 
656 		mySearchParamRegistry.requestRefresh();
657 	}
658 
659 	@Autowired
660 	private IResourceReindexingSvc myResourceReindexingSvc;
661 
662 	@Override
663 	public <MT extends IBaseMetaType> MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) {
664 		// Notify interceptors
665 		if (theRequestDetails != null) {
666 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theResourceId);
667 			notifyInterceptors(RestOperationTypeEnum.META_ADD, requestDetails);
668 		}
669 
670 		StopWatch w = new StopWatch();
671 		BaseHasResource entity = readEntity(theResourceId);
672 		if (entity == null) {
673 			throw new ResourceNotFoundException(theResourceId);
674 		}
675 
676 		ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
677 		if (latestVersion.getVersion() != entity.getVersion()) {
678 			doMetaAdd(theMetaAdd, entity);
679 		} else {
680 			doMetaAdd(theMetaAdd, latestVersion);
681 
682 			// Also update history entry
683 			ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion());
684 			doMetaAdd(theMetaAdd, history);
685 		}
686 
687 		ourLog.debug("Processed metaAddOperation on {} in {}ms", new Object[] {theResourceId, w.getMillisAndRestart()});
688 
689 		@SuppressWarnings("unchecked")
690 		MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequestDetails);
691 		return retVal;
692 	}
693 
694 	@Override
695 	public <MT extends IBaseMetaType> MT metaDeleteOperation(IIdType theResourceId, MT theMetaDel, RequestDetails theRequestDetails) {
696 		// Notify interceptors
697 		if (theRequestDetails != null) {
698 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theResourceId);
699 			notifyInterceptors(RestOperationTypeEnum.META_DELETE, requestDetails);
700 		}
701 
702 		StopWatch w = new StopWatch();
703 		BaseHasResource entity = readEntity(theResourceId);
704 		if (entity == null) {
705 			throw new ResourceNotFoundException(theResourceId);
706 		}
707 
708 		ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
709 		if (latestVersion.getVersion() != entity.getVersion()) {
710 			doMetaDelete(theMetaDel, entity);
711 		} else {
712 			doMetaDelete(theMetaDel, latestVersion);
713 
714 			// Also update history entry
715 			ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(entity.getId(), entity.getVersion());
716 			doMetaDelete(theMetaDel, history);
717 		}
718 
719 		myEntityManager.flush();
720 
721 		ourLog.debug("Processed metaDeleteOperation on {} in {}ms", new Object[] {theResourceId.getValue(), w.getMillisAndRestart()});
722 
723 		@SuppressWarnings("unchecked")
724 		MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequestDetails);
725 		return retVal;
726 	}
727 
728 	@Override
729 	public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequestDetails) {
730 		// Notify interceptors
731 		if (theRequestDetails != null) {
732 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
733 			notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
734 		}
735 
736 		Set<TagDefinition> tagDefs = new HashSet<>();
737 		BaseHasResource entity = readEntity(theId);
738 		for (BaseTag next : entity.getTags()) {
739 			tagDefs.add(next.getTag());
740 		}
741 		MT retVal = toMetaDt(theType, tagDefs);
742 
743 		retVal.setLastUpdated(entity.getUpdatedDate());
744 		retVal.setVersionId(Long.toString(entity.getVersion()));
745 
746 		return retVal;
747 	}
748 
749 	@SuppressWarnings("JpaQlInspection")
750 	@Override
751 	public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
752 		// Notify interceptors
753 		if (theRequestDetails != null) {
754 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), null);
755 			notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
756 		}
757 
758 		String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)";
759 		TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
760 		q.setParameter("res_type", myResourceName);
761 		List<TagDefinition> tagDefinitions = q.getResultList();
762 
763 		return toMetaDt(theType, tagDefinitions);
764 	}
765 
766 	@Override
767 	public DaoMethodOutcome patch(IIdType theId, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails) {
768 		ResourceTable entityToUpdate = readEntityLatestVersion(theId);
769 		if (theId.hasVersionIdPart()) {
770 			if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
771 				throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
772 			}
773 		}
774 
775 		validateResourceType(entityToUpdate);
776 
777 		IBaseResource resourceToUpdate = toResource(entityToUpdate, false);
778 		IBaseResource destination;
779 		if (thePatchType == PatchTypeEnum.JSON_PATCH) {
780 			destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
781 		} else {
782 			destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
783 		}
784 
785 		@SuppressWarnings("unchecked")
786 		T destinationCasted = (T) destination;
787 		return update(destinationCasted, null, true, theRequestDetails);
788 	}
789 
790 	@PostConstruct
791 	public void postConstruct() {
792 		RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType);
793 		myResourceName = def.getName();
794 
795 		if (mySecondaryPrimaryKeyParamName != null) {
796 			RuntimeSearchParam sp = getSearchParamByName(def, mySecondaryPrimaryKeyParamName);
797 			if (sp == null) {
798 				throw new ConfigurationException("Unknown search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "]");
799 			}
800 			if (sp.getParamType() != RestSearchParameterTypeEnum.TOKEN) {
801 				throw new ConfigurationException("Search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "] is not a token type, only token is supported");
802 			}
803 		}
804 
805 	}
806 
807 	/**
808 	 * Subclasses may override to provide behaviour. Invoked within a delete
809 	 * transaction with the resource that is about to be deleted.
810 	 */
811 	protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete) {
812 		// nothing by default
813 	}
814 
815 	/**
816 	 * May be overridden by subclasses to validate resources prior to storage
817 	 *
818 	 * @param theResource The resource that is about to be stored
819 	 */
820 	protected void preProcessResourceForStorage(T theResource) {
821 		String type = getContext().getResourceDefinition(theResource).getName();
822 		if (!getResourceName().equals(type)) {
823 			throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "incorrectResourceType", type, getResourceName()));
824 		}
825 
826 		if (theResource.getIdElement().hasIdPart()) {
827 			if (!theResource.getIdElement().isIdPartValid()) {
828 				throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart()));
829 			}
830 		}
831 
832 		/*
833 		 * Replace absolute references with relative ones if configured to do so
834 		 */
835 		if (getConfig().getTreatBaseUrlsAsLocal().isEmpty() == false) {
836 			FhirTerser t = getContext().newTerser();
837 			List<ResourceReferenceInfo> refs = t.getAllResourceReferences(theResource);
838 			for (ResourceReferenceInfo nextRef : refs) {
839 				IIdType refId = nextRef.getResourceReference().getReferenceElement();
840 				if (refId != null && refId.hasBaseUrl()) {
841 					if (getConfig().getTreatBaseUrlsAsLocal().contains(refId.getBaseUrl())) {
842 						IIdType newRefId = refId.toUnqualified();
843 						nextRef.getResourceReference().setReference(newRefId.getValue());
844 					}
845 				}
846 			}
847 		}
848 	}
849 
850 	@Override
851 	public Set<Long> processMatchUrl(String theMatchUrl) {
852 		return processMatchUrl(theMatchUrl, getResourceType());
853 	}
854 
855 	@Override
856 	public IBaseResource readByPid(Long thePid) {
857 		StopWatch w = new StopWatch();
858 
859 		Optional<ResourceTable> entity = myResourceTableDao.findById(thePid);
860 		if (!entity.isPresent()) {
861 			throw new ResourceNotFoundException("No resource found with PID " + thePid);
862 		}
863 		if (entity.get().getDeleted() != null) {
864 			throw new ResourceGoneException("Resource was deleted at " + new InstantType(entity.get().getDeleted()).getValueAsString());
865 		}
866 
867 		T retVal = toResource(myResourceType, entity.get(), null, false);
868 
869 		ourLog.debug("Processed read on {} in {}ms", thePid, w.getMillis());
870 		return retVal;
871 	}
872 
873 
874 	@Override
875 	public T read(IIdType theId) {
876 		return read(theId, null);
877 	}
878 
879 	@Override
880 	public T read(IIdType theId, RequestDetails theRequestDetails) {
881 		validateResourceTypeAndThrowIllegalArgumentException(theId);
882 
883 		// Notify interceptors
884 		if (theRequestDetails != null) {
885 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
886 			RestOperationTypeEnum operationType = theId.hasVersionIdPart() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
887 			notifyInterceptors(operationType, requestDetails);
888 		}
889 
890 		StopWatch w = new StopWatch();
891 		BaseHasResource entity = readEntity(theId);
892 		validateResourceType(entity);
893 
894 		T retVal = toResource(myResourceType, entity, null, false);
895 
896 		if (entity.getDeleted() != null) {
897 			throw new ResourceGoneException("Resource was deleted at " + new InstantType(entity.getDeleted()).getValueAsString());
898 		}
899 
900 		ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
901 		return retVal;
902 	}
903 
904 	@Override
905 	public BaseHasResource readEntity(IIdType theId) {
906 
907 		return readEntity(theId, true);
908 	}
909 
910 	@Override
911 	public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) {
912 		validateResourceTypeAndThrowIllegalArgumentException(theId);
913 
914 		Long pid = translateForcedIdToPid(getResourceName(), theId.getIdPart());
915 		BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid);
916 
917 		if (entity == null) {
918 			throw new ResourceNotFoundException(theId);
919 		}
920 
921 		if (theId.hasVersionIdPart()) {
922 			if (theId.isVersionIdPartValidLong() == false) {
923 				throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
924 			}
925 			if (entity.getVersion() != theId.getVersionIdPartAsLong()) {
926 				entity = null;
927 			}
928 		}
929 
930 		if (entity == null) {
931 			if (theId.hasVersionIdPart()) {
932 				TypedQuery<ResourceHistoryTable> q = myEntityManager
933 					.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
934 				q.setParameter("RID", pid);
935 				q.setParameter("RTYP", myResourceName);
936 				q.setParameter("RVER", theId.getVersionIdPartAsLong());
937 				try {
938 					entity = q.getSingleResult();
939 				} catch (NoResultException e) {
940 					throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
941 				}
942 			}
943 		}
944 
945 		validateResourceType(entity);
946 
947 		if (theCheckForForcedId) {
948 			validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
949 		}
950 		return entity;
951 	}
952 
953 	protected ResourceTable readEntityLatestVersion(IIdType theId) {
954 		ResourceTable entity = myEntityManager.find(ResourceTable.class, translateForcedIdToPid(getResourceName(), theId.getIdPart()));
955 		if (entity == null) {
956 			throw new ResourceNotFoundException(theId);
957 		}
958 		validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
959 		return entity;
960 	}
961 
962 	@Override
963 	public void reindex(T theResource, ResourceTable theEntity) {
964 		ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getId());
965 		if (theResource != null) {
966 			CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE);
967 		}
968 		updateEntity(null, theResource, theEntity, theEntity.getDeleted(), true, false, theEntity.getUpdatedDate(), true, false);
969 		if (theResource != null) {
970 			CURRENTLY_REINDEXING.put(theResource, null);
971 		}
972 	}
973 
974 	@Override
975 	public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) {
976 		removeTag(theId, theTagType, theScheme, theTerm, null);
977 	}
978 
979 	@Override
980 	public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequestDetails) {
981 		// Notify interceptors
982 		if (theRequestDetails != null) {
983 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), theId);
984 			notifyInterceptors(RestOperationTypeEnum.DELETE_TAGS, requestDetails);
985 		}
986 
987 		StopWatch w = new StopWatch();
988 		BaseHasResource entity = readEntity(theId);
989 		if (entity == null) {
990 			throw new ResourceNotFoundException(theId);
991 		}
992 
993 		for (BaseTag next : new ArrayList<>(entity.getTags())) {
994 			if (ObjectUtil.equals(next.getTag().getTagType(), theTagType) &&
995 				ObjectUtil.equals(next.getTag().getSystem(), theScheme) &&
996 				ObjectUtil.equals(next.getTag().getCode(), theTerm)) {
997 				myEntityManager.remove(next);
998 				entity.getTags().remove(next);
999 			}
1000 		}
1001 
1002 		if (entity.getTags().isEmpty()) {
1003 			entity.setHasTags(false);
1004 		}
1005 
1006 		myEntityManager.merge(entity);
1007 
1008 		ourLog.debug("Processed remove tag {}/{} on {} in {}ms", theScheme, theTerm, theId.getValue(), w.getMillisAndRestart());
1009 	}
1010 
1011 	@Transactional(propagation = Propagation.SUPPORTS)
1012 	@Override
1013 	public IBundleProvider search(final SearchParameterMap theParams) {
1014 		return search(theParams, null);
1015 	}
1016 
1017 	@Transactional(propagation = Propagation.SUPPORTS)
1018 	@Override
1019 	public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails) {
1020 		return search(theParams, theRequestDetails, null);
1021 	}
1022 
1023 	@Transactional(propagation = Propagation.SUPPORTS)
1024 	@Override
1025 	public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequestDetails, HttpServletResponse theServletResponse) {
1026 
1027 		if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
1028 			for (List<List<? extends IQueryParameterType>> nextAnds : theParams.values()) {
1029 				for (List<? extends IQueryParameterType> nextOrs : nextAnds) {
1030 					for (IQueryParameterType next : nextOrs) {
1031 						if (next.getMissing() != null) {
1032 							throw new MethodNotAllowedException(":missing modifier is disabled on this server");
1033 						}
1034 					}
1035 				}
1036 			}
1037 		}
1038 
1039 		// Notify interceptors
1040 		if (theRequestDetails != null) {
1041 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), getResourceName(), null);
1042 			notifyInterceptors(RestOperationTypeEnum.SEARCH_TYPE, requestDetails);
1043 
1044 			if (theRequestDetails.isSubRequest()) {
1045 				Integer max = myDaoConfig.getMaximumSearchResultCountInTransaction();
1046 				if (max != null) {
1047 					Validate.inclusiveBetween(1, Integer.MAX_VALUE, max.intValue(), "Maximum search result count in transaction ust be a positive integer");
1048 					theParams.setLoadSynchronousUpTo(myDaoConfig.getMaximumSearchResultCountInTransaction());
1049 				}
1050 			}
1051 
1052 			if (!isPagingProviderDatabaseBacked(theRequestDetails)) {
1053 				theParams.setLoadSynchronous(true);
1054 			}
1055 		}
1056 
1057 		CacheControlDirective cacheControlDirective = new CacheControlDirective();
1058 		if (theRequestDetails != null) {
1059 			cacheControlDirective.parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL));
1060 		}
1061 
1062 		IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective);
1063 
1064 		if (retVal instanceof PersistedJpaBundleProvider) {
1065 			PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) retVal;
1066 			if (provider.isCacheHit()) {
1067 				if (theServletResponse != null && theRequestDetails != null) {
1068 					String value = "HIT from " + theRequestDetails.getFhirServerBase();
1069 					theServletResponse.addHeader(Constants.HEADER_X_CACHE, value);
1070 				}
1071 			}
1072 		}
1073 
1074 		return retVal;
1075 	}
1076 
1077 	@Override
1078 	public Set<Long> searchForIds(SearchParameterMap theParams) {
1079 
1080 		SearchBuilder builder = newSearchBuilder();
1081 		builder.setType(getResourceType(), getResourceName());
1082 
1083 		// FIXME: fail if too many results
1084 
1085 		HashSet<Long> retVal = new HashSet<Long>();
1086 
1087 		String uuid = UUID.randomUUID().toString();
1088 		Iterator<Long> iter = builder.createQuery(theParams, uuid);
1089 		while (iter.hasNext()) {
1090 			retVal.add(iter.next());
1091 		}
1092 
1093 		return retVal;
1094 	}
1095 
1096 	/**
1097 	 * If set, the given param will be treated as a secondary primary key, and multiple resources will not be able to share the same value.
1098 	 */
1099 	public void setSecondaryPrimaryKeyParamName(String theSecondaryPrimaryKeyParamName) {
1100 		mySecondaryPrimaryKeyParamName = theSecondaryPrimaryKeyParamName;
1101 	}
1102 
1103 	@PostConstruct
1104 	public void start() {
1105 		ourLog.debug("Starting resource DAO for type: {}", getResourceName());
1106 	}
1107 
1108 	protected <MT extends IBaseMetaType> MT toMetaDt(Class<MT> theType, Collection<TagDefinition> tagDefinitions) {
1109 		MT retVal;
1110 		try {
1111 			retVal = theType.newInstance();
1112 		} catch (Exception e) {
1113 			throw new InternalErrorException("Failed to instantiate " + theType.getName(), e);
1114 		}
1115 		for (TagDefinition next : tagDefinitions) {
1116 			switch (next.getTagType()) {
1117 				case PROFILE:
1118 					retVal.addProfile(next.getCode());
1119 					break;
1120 				case SECURITY_LABEL:
1121 					retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
1122 					break;
1123 				case TAG:
1124 					retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
1125 					break;
1126 			}
1127 		}
1128 		return retVal;
1129 	}
1130 
1131 	private DaoMethodOutcome toMethodOutcome(final BaseHasResource theEntity, IBaseResource theResource) {
1132 		DaoMethodOutcome outcome = new DaoMethodOutcome();
1133 
1134 		IIdType id = theEntity.getIdDt();
1135 		if (getContext().getVersion().getVersion().isRi()) {
1136 			id = getContext().getVersion().newIdType().setValue(id.getValue());
1137 		}
1138 
1139 		outcome.setId(id);
1140 		outcome.setResource(theResource);
1141 		if (theResource != null) {
1142 			theResource.setId(id);
1143 			if (theResource instanceof IResource) {
1144 				ResourceMetadataKeyEnum.UPDATED.put((IResource) theResource, theEntity.getUpdated());
1145 			} else {
1146 				IBaseMetaType meta = theResource.getMeta();
1147 				meta.setLastUpdated(theEntity.getUpdatedDate());
1148 			}
1149 		}
1150 		return outcome;
1151 	}
1152 
1153 	private DaoMethodOutcome toMethodOutcome(final ResourceTable theEntity, IBaseResource theResource) {
1154 		DaoMethodOutcome retVal = toMethodOutcome((BaseHasResource) theEntity, theResource);
1155 		retVal.setEntity(theEntity);
1156 		return retVal;
1157 	}
1158 
1159 	private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) {
1160 		ArrayList<TagDefinition> retVal = new ArrayList<TagDefinition>();
1161 
1162 		for (IBaseCoding next : theMeta.getTag()) {
1163 			retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay()));
1164 		}
1165 		for (IBaseCoding next : theMeta.getSecurity()) {
1166 			retVal.add(new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay()));
1167 		}
1168 		for (IPrimitiveType<String> next : theMeta.getProfile()) {
1169 			retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseHapiFhirDao.NS_JPA_PROFILE, next.getValue(), null));
1170 		}
1171 
1172 		return retVal;
1173 	}
1174 
1175 	@Transactional(propagation = Propagation.SUPPORTS)
1176 	@Override
1177 	public void translateRawParameters(Map<String, List<String>> theSource, SearchParameterMap theTarget) {
1178 		if (theSource == null || theSource.isEmpty()) {
1179 			return;
1180 		}
1181 
1182 		Map<String, RuntimeSearchParam> searchParams = mySerarchParamRegistry.getActiveSearchParams(getResourceName());
1183 
1184 		Set<String> paramNames = theSource.keySet();
1185 		for (String nextParamName : paramNames) {
1186 			QualifierDetails qualifiedParamName = SearchMethodBinding.extractQualifiersFromParameterName(nextParamName);
1187 			RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
1188 			if (param == null) {
1189 				String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), new TreeSet<String>(searchParams.keySet()));
1190 				throw new InvalidRequestException(msg);
1191 			}
1192 
1193 			// Should not be null since the check above would have caught it
1194 			RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceName);
1195 			RuntimeSearchParam paramDef = getSearchParamByName(resourceDef, qualifiedParamName.getParamName());
1196 
1197 			for (String nextValue : theSource.get(nextParamName)) {
1198 				if (isNotBlank(nextValue)) {
1199 					QualifiedParamList qualifiedParam = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifiedParamName.getWholeQualifier(), nextValue);
1200 					List<QualifiedParamList> paramList = Collections.singletonList(qualifiedParam);
1201 					IQueryParameterAnd<?> parsedParam = ParameterUtil.parseQueryParams(getContext(), paramDef, nextParamName, paramList);
1202 					theTarget.add(qualifiedParamName.getParamName(), parsedParam);
1203 				}
1204 			}
1205 
1206 		}
1207 	}
1208 
1209 	@Override
1210 	public DaoMethodOutcome update(T theResource) {
1211 		return update(theResource, null, null);
1212 	}
1213 
1214 	@Override
1215 	public DaoMethodOutcome update(T theResource, RequestDetails theRequestDetails) {
1216 		return update(theResource, null, theRequestDetails);
1217 	}
1218 
1219 	@Override
1220 	public DaoMethodOutcome update(T theResource, String theMatchUrl) {
1221 		return update(theResource, theMatchUrl, null);
1222 	}
1223 
1224 	@Override
1225 	public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails) {
1226 		StopWatch w = new StopWatch();
1227 
1228 		preProcessResourceForStorage(theResource);
1229 
1230 		final ResourceTable entity;
1231 
1232 		IIdType resourceId;
1233 		if (isNotBlank(theMatchUrl)) {
1234 			StopWatch sw = new StopWatch();
1235 			Set<Long> match = processMatchUrl(theMatchUrl, myResourceType);
1236 			if (match.size() > 1) {
1237 				String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
1238 				throw new PreconditionFailedException(msg);
1239 			} else if (match.size() == 1) {
1240 				Long pid = match.iterator().next();
1241 				entity = myEntityManager.find(ResourceTable.class, pid);
1242 				resourceId = entity.getIdDt();
1243 			} else {
1244 				return create(theResource, null, thePerformIndexing, theRequestDetails);
1245 			}
1246 		} else {
1247 			/*
1248 			 * Note: resourceId will not be null or empty here, because we
1249 			 * check it and reject requests in
1250 			 * BaseOutcomeReturningMethodBindingWithResourceParam
1251 			 */
1252 			resourceId = theResource.getIdElement();
1253 
1254 			try {
1255 				entity = readEntityLatestVersion(resourceId);
1256 			} catch (ResourceNotFoundException e) {
1257 				return doCreate(theResource, null, thePerformIndexing, new Date(), theRequestDetails);
1258 			}
1259 		}
1260 
1261 		if (resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) != entity.getVersion()) {
1262 			throw new ResourceVersionConflictException("Trying to update " + resourceId + " but this is not the current version");
1263 		}
1264 
1265 		if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) {
1266 			throw new UnprocessableEntityException(
1267 				"Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]");
1268 		}
1269 
1270 		IBaseResource oldResource = toResource(entity, false);
1271 
1272 		/*
1273 		 * Mark the entity as not deleted - This is also done in the actual updateInternal()
1274 		 * method later on so it usually doesn't matter whether we do it here, but in the
1275 		 * case of a transaction with multiple PUTs we don't get there until later so
1276 		 * having this here means that a transaction can have a reference in one
1277 		 * resource to another resource in the same transaction that is being
1278 		 * un-deleted by the transaction. Wacky use case, sure. But it's real.
1279 		 *
1280 		 * See SystemProviderR4Test#testTransactionReSavesPreviouslyDeletedResources
1281 		 * for a test that needs this.
1282 		 */
1283 		entity.setDeleted(null);
1284 
1285 		/*
1286 		 * If we aren't indexing, that means we're doing this inside a transaction.
1287 		 * The transaction will do the actual storage to the database a bit later on,
1288 		 * after placeholder IDs have been replaced, by calling {@link #updateInternal}
1289 		 * directly. So we just bail now.
1290 		 */
1291 		if (!thePerformIndexing) {
1292 			DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(false);
1293 			outcome.setPreviousResource(oldResource);
1294 			return outcome;
1295 		}
1296 
1297 		/*
1298 		 * Otherwise, we're not in a transaction
1299 		 */
1300 		ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, theRequestDetails, entity, resourceId, oldResource);
1301 		DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false);
1302 
1303 		if (!thePerformIndexing) {
1304 			outcome.setId(theResource.getIdElement());
1305 		}
1306 
1307 		String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
1308 		outcome.setOperationOutcome(createInfoOperationOutcome(msg));
1309 
1310 		ourLog.debug(msg);
1311 		return outcome;
1312 	}
1313 
1314 	@Override
1315 	public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
1316 		return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
1317 	}
1318 
1319 	@Override
1320 	public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
1321 		return update(theResource, theMatchUrl, true, theRequestDetails);
1322 	}
1323 
1324 	/**
1325 	 * Get the resource definition from the criteria which specifies the resource type
1326 	 *
1327 	 * @param criteria
1328 	 * @return
1329 	 */
1330 	@Override
1331 	public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) {
1332 		String resourceName;
1333 		if (criteria == null || criteria.trim().isEmpty()) {
1334 			throw new IllegalArgumentException("Criteria cannot be empty");
1335 		}
1336 		if (criteria.contains("?")) {
1337 			resourceName = criteria.substring(0, criteria.indexOf("?"));
1338 		} else {
1339 			resourceName = criteria;
1340 		}
1341 
1342 		return getContext().getResourceDefinition(resourceName);
1343 	}
1344 
1345 	private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) {
1346 		if (entity.getForcedId() != null) {
1347 			if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) {
1348 				if (theId.isIdPartValidLong()) {
1349 					// This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that
1350 					// as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer
1351 					// to the
1352 					// forced ID)
1353 					throw new ResourceNotFoundException(theId);
1354 				}
1355 			}
1356 		}
1357 	}
1358 
1359 	protected void validateOkToDelete(List<DeleteConflict> theDeleteConflicts, ResourceTable theEntity, boolean theForValidate) {
1360 		TypedQuery<ResourceLink> query = myEntityManager.createQuery("SELECT l FROM ResourceLink l WHERE l.myTargetResourcePid = :target_pid", ResourceLink.class);
1361 		query.setParameter("target_pid", theEntity.getId());
1362 		query.setMaxResults(1);
1363 		List<ResourceLink> resultList = query.getResultList();
1364 		if (resultList.isEmpty()) {
1365 			return;
1366 		}
1367 
1368 		if (myDaoConfig.isEnforceReferentialIntegrityOnDelete() == false && !theForValidate) {
1369 			ourLog.debug("Deleting {} resource dependencies which can no longer be satisfied", resultList.size());
1370 			myResourceLinkDao.deleteAll(resultList);
1371 			return;
1372 		}
1373 
1374 		ResourceLink link = resultList.get(0);
1375 		IdDt targetId = theEntity.getIdDt();
1376 		IdDt sourceId = link.getSourceResource().getIdDt();
1377 		String sourcePath = link.getSourcePath();
1378 
1379 		theDeleteConflicts.add(new DeleteConflict(sourceId, sourcePath, targetId));
1380 	}
1381 
1382 	private void validateResourceType(BaseHasResource entity) {
1383 		validateResourceType(entity, myResourceName);
1384 	}
1385 
1386 	private void validateResourceTypeAndThrowIllegalArgumentException(IIdType theId) {
1387 		if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) {
1388 			throw new IllegalArgumentException("Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName);
1389 		}
1390 	}
1391 
1392 }