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