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