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