View Javadoc
1   package ca.uhn.fhir.jpa.dao.r4;
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.RuntimeResourceDefinition;
24  import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
25  import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
26  import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome;
27  import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
28  import ca.uhn.fhir.jpa.entity.ResourceTable;
29  import ca.uhn.fhir.jpa.entity.TagDefinition;
30  import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
31  import ca.uhn.fhir.jpa.util.DeleteConflict;
32  import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
33  import ca.uhn.fhir.parser.DataFormatException;
34  import ca.uhn.fhir.parser.IParser;
35  import ca.uhn.fhir.rest.api.Constants;
36  import ca.uhn.fhir.rest.api.PreferReturnEnum;
37  import ca.uhn.fhir.rest.api.RequestTypeEnum;
38  import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
39  import ca.uhn.fhir.rest.api.server.RequestDetails;
40  import ca.uhn.fhir.rest.param.ParameterUtil;
41  import ca.uhn.fhir.rest.server.RestfulServerUtils;
42  import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
43  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
44  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
45  import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
46  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
47  import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
48  import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
49  import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
50  import ca.uhn.fhir.util.FhirTerser;
51  import ca.uhn.fhir.util.UrlUtil;
52  import ca.uhn.fhir.util.UrlUtil.UrlParts;
53  import com.google.common.collect.ArrayListMultimap;
54  import org.apache.commons.lang3.Validate;
55  import org.apache.http.NameValuePair;
56  import org.hl7.fhir.instance.model.api.*;
57  import org.hl7.fhir.r4.model.*;
58  import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
59  import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent;
60  import org.hl7.fhir.r4.model.Bundle.BundleType;
61  import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
62  import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
63  import org.springframework.beans.factory.annotation.Autowired;
64  import org.springframework.transaction.PlatformTransactionManager;
65  import org.springframework.transaction.TransactionDefinition;
66  import org.springframework.transaction.TransactionStatus;
67  import org.springframework.transaction.annotation.Propagation;
68  import org.springframework.transaction.annotation.Transactional;
69  import org.springframework.transaction.support.TransactionCallback;
70  import org.springframework.transaction.support.TransactionTemplate;
71  
72  import javax.persistence.TypedQuery;
73  import java.util.*;
74  import java.util.Map.Entry;
75  
76  import static org.apache.commons.lang3.StringUtils.*;
77  
78  public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
79  
80  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4.class);
81  
82  	@Autowired
83  	private PlatformTransactionManager myTxManager;
84  
85  	private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) {
86  		ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size());
87  		long start = System.currentTimeMillis();
88  
89  		TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
90  		txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
91  
92  		Bundle resp = new Bundle();
93  		resp.setType(BundleType.BATCHRESPONSE);
94  
95  		/*
96  		 * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others
97  		 */
98  
99  		for (final BundleEntryComponent nextRequestEntry : theRequest.getEntry()) {
100 
101 			BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
102 
103 			TransactionCallback<Bundle> callback = new TransactionCallback<Bundle>() {
104 				@Override
105 				public Bundle doInTransaction(TransactionStatus theStatus) {
106 					Bundle subRequestBundle = new Bundle();
107 					subRequestBundle.setType(BundleType.TRANSACTION);
108 					subRequestBundle.addEntry(nextRequestEntry);
109 
110 					Bundle subResponseBundle = transaction((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request");
111 					return subResponseBundle;
112 				}
113 			};
114 
115 			try {
116 				Bundle nextResponseBundle = callback.doInTransaction(null);
117 
118 				BundleEntryComponent subResponseEntry = nextResponseBundle.getEntry().get(0);
119 				resp.addEntry(subResponseEntry);
120 
121 				/*
122 				 * If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it
123 				 */
124 				if (subResponseEntry.getResource() == null) {
125 					subResponseEntry.setResource(nextResponseBundle.getEntry().get(0).getResource());
126 				}
127 
128 			} catch (BaseServerResponseException e) {
129 				caughtEx.setException(e);
130 			} catch (Throwable t) {
131 				ourLog.error("Failure during BATCH sub transaction processing", t);
132 				caughtEx.setException(new InternalErrorException(t));
133 			}
134 
135 			if (caughtEx.getException() != null) {
136 				BundleEntryComponent nextEntry = resp.addEntry();
137 
138 				populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry);
139 
140 				BundleEntryResponseComponent nextEntryResp = nextEntry.getResponse();
141 				nextEntryResp.setStatus(toStatusString(caughtEx.getException().getStatusCode()));
142 			}
143 
144 		}
145 
146 		long delay = System.currentTimeMillis() - start;
147 		ourLog.info("Batch completed in {}ms", new Object[] {delay});
148 
149 		return resp;
150 	}
151 
152 	private Bundle doTransaction(final ServletRequestDetails theRequestDetails, final Bundle theRequest, final String theActionName) {
153 		BundleType transactionType = theRequest.getTypeElement().getValue();
154 		if (transactionType == BundleType.BATCH) {
155 			return batch(theRequestDetails, theRequest);
156 		}
157 
158 		if (transactionType == null) {
159 			String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + BundleType.TRANSACTION.toCode();
160 			ourLog.warn(message);
161 			transactionType = BundleType.TRANSACTION;
162 		}
163 		if (transactionType != BundleType.TRANSACTION) {
164 			throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType.toCode());
165 		}
166 
167 		ourLog.debug("Beginning {} with {} resources", theActionName, theRequest.getEntry().size());
168 
169 		long start = System.currentTimeMillis();
170 		final Date updateTime = new Date();
171 
172 		final Set<IdType> allIds = new LinkedHashSet<>();
173 		final Map<IdType, IdType> idSubstitutions = new HashMap<>();
174 		final Map<IdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<>();
175 
176 		// Do all entries have a verb?
177 		for (int i = 0; i < theRequest.getEntry().size(); i++) {
178 			BundleEntryComponent nextReqEntry = theRequest.getEntry().get(i);
179 			HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue();
180 			if (verb == null) {
181 				throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextReqEntry.getRequest().getMethod(), i));
182 			}
183 		}
184 
185 		/*
186 		 * We want to execute the transaction request bundle elements in the order
187 		 * specified by the FHIR specification (see TransactionSorter) so we save the
188 		 * original order in the request, then sort it.
189 		 *
190 		 * Entries with a type of GET are removed from the bundle so that they
191 		 * can be processed at the very end. We do this because the incoming resources
192 		 * are saved in a two-phase way in order to deal with interdependencies, and
193 		 * we want the GET processing to use the final indexing state
194 		 */
195 		final Bundle response = new Bundle();
196 		List<BundleEntryComponent> getEntries = new ArrayList<>();
197 		final IdentityHashMap<BundleEntryComponent, Integer> originalRequestOrder = new IdentityHashMap<>();
198 		for (int i = 0; i < theRequest.getEntry().size(); i++) {
199 			originalRequestOrder.put(theRequest.getEntry().get(i), i);
200 			response.addEntry();
201 			if (theRequest.getEntry().get(i).getRequest().getMethodElement().getValue() == HTTPVerb.GET) {
202 				getEntries.add(theRequest.getEntry().get(i));
203 			}
204 		}
205 
206 		/*
207 		 * See FhirSystemDaoR4Test#testTransactionWithPlaceholderIdInMatchUrl
208 		 * Basically if the resource has a match URL that references a placeholder,
209 		 * we try to handle the resource with the placeholder first.
210 		 */
211 		Set<String> placeholderIds = new HashSet<String>();
212 		final List<BundleEntryComponent> entries = theRequest.getEntry();
213 		for (BundleEntryComponent nextEntry : entries) {
214 			if (isNotBlank(nextEntry.getFullUrl()) && nextEntry.getFullUrl().startsWith(IdType.URN_PREFIX)) {
215 				placeholderIds.add(nextEntry.getFullUrl());
216 			}
217 		}
218 		Collections.sort(entries, new TransactionSorter(placeholderIds));
219 
220 		/*
221 		 * All of the write operations in the transaction (PUT, POST, etc.. basically anything
222 		 * except GET) are performed in their own database transaction before we do the reads.
223 		 * We do this because the reads (specifically the searches) often spawn their own
224 		 * secondary database transaction and if we allow that within the primary
225 		 * database transaction we can end up with deadlocks if the server is under
226 		 * heavy load with lots of concurrent transactions using all available
227 		 * database connections.
228 		 */
229 		TransactionTemplate txManager = new TransactionTemplate(myTxManager);
230 		Map<BundleEntryComponent, ResourceTable> entriesToProcess = txManager.execute(new TransactionCallback<Map<BundleEntryComponent, ResourceTable>>() {
231 			@Override
232 			public Map<BundleEntryComponent, ResourceTable> doInTransaction(TransactionStatus status) {
233 				return doTransactionWriteOperations(theRequestDetails, theRequest, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries);
234 			}
235 		});
236 		for (Entry<BundleEntryComponent, ResourceTable> nextEntry : entriesToProcess.entrySet()) {
237 			String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue();
238 			String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart();
239 			nextEntry.getKey().getResponse().setLocation(responseLocation);
240 			nextEntry.getKey().getResponse().setEtag(responseEtag);
241 		}
242 
243 		/*
244 		 * Loop through the request and process any entries of type GET
245 		 */
246 		for (int i = 0; i < getEntries.size(); i++) {
247 			BundleEntryComponent nextReqEntry = getEntries.get(i);
248 			Integer originalOrder = originalRequestOrder.get(nextReqEntry);
249 			BundleEntryComponent nextRespEntry = response.getEntry().get(originalOrder);
250 
251 			ServletSubRequestDetails requestDetails = new ServletSubRequestDetails();
252 			requestDetails.setServletRequest(theRequestDetails.getServletRequest());
253 			requestDetails.setRequestType(RequestTypeEnum.GET);
254 			requestDetails.setServer(theRequestDetails.getServer());
255 
256 			String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerb.GET);
257 
258 			int qIndex = url.indexOf('?');
259 			ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
260 			requestDetails.setParameters(new HashMap<String, String[]>());
261 			if (qIndex != -1) {
262 				String params = url.substring(qIndex);
263 				List<NameValuePair> parameters = translateMatchUrl(params);
264 				for (NameValuePair next : parameters) {
265 					paramValues.put(next.getName(), next.getValue());
266 				}
267 				for (java.util.Map.Entry<String, Collection<String>> nextParamEntry : paramValues.asMap().entrySet()) {
268 					String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]);
269 					requestDetails.addParameter(nextParamEntry.getKey(), nextValue);
270 				}
271 				url = url.substring(0, qIndex);
272 			}
273 
274 			requestDetails.setRequestPath(url);
275 			requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase());
276 
277 			theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url);
278 			BaseMethodBinding<?> method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url);
279 			if (method == null) {
280 				throw new IllegalArgumentException("Unable to handle GET " + url);
281 			}
282 
283 			if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) {
284 				requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch());
285 			}
286 			if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) {
287 				requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist());
288 			}
289 			if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) {
290 				requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch());
291 			}
292 
293 			Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {0}", url);
294 			try {
295 				IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails);
296 				if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
297 					resource = filterNestedBundle(requestDetails, resource);
298 				}
299 				nextRespEntry.setResource((Resource) resource);
300 				nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
301 			} catch (NotModifiedException e) {
302 				nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
303 			} catch (BaseServerResponseException e) {
304 				ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
305 				nextRespEntry.getResponse().setStatus(toStatusString(e.getStatusCode()));
306 				populateEntryWithOperationOutcome(e, nextRespEntry);
307 			}
308 
309 		}
310 
311 		long delay = System.currentTimeMillis() - start;
312 		ourLog.info(theActionName + " completed in {}ms", new Object[] {delay});
313 
314 		response.setType(BundleType.TRANSACTIONRESPONSE);
315 		return response;
316 	}
317 
318 	@SuppressWarnings("unchecked")
319 	private Map<BundleEntryComponent, ResourceTable> doTransactionWriteOperations(RequestDetails theRequestDetails, Bundle theRequest, String theActionName, Date theUpdateTime, Set<IdType> theAllIds,
320 																											Map<IdType, IdType> theIdSubstitutions, Map<IdType, DaoMethodOutcome> theIdToPersistedOutcome, Bundle theResponse, IdentityHashMap<BundleEntryComponent, Integer> theOriginalRequestOrder, List<BundleEntryComponent> theEntries) {
321 		Set<String> deletedResources = new HashSet<>();
322 		List<DeleteConflict> deleteConflicts = new ArrayList<>();
323 		Map<BundleEntryComponent, ResourceTable> entriesToProcess = new IdentityHashMap<>();
324 		Set<ResourceTable> nonUpdatedEntities = new HashSet<>();
325 		Set<ResourceTable> updatedEntities = new HashSet<>();
326 		Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<>();
327 
328 		/*
329 		 * Loop through the request and process any entries of type
330 		 * PUT, POST or DELETE
331 		 */
332 		for (int i = 0; i < theEntries.size(); i++) {
333 
334 			if (i % 100 == 0) {
335 				ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size());
336 			}
337 
338 			BundleEntryComponent nextReqEntry = theEntries.get(i);
339 			Resource res = nextReqEntry.getResource();
340 			IdType nextResourceId = null;
341 			if (res != null) {
342 
343 				nextResourceId = res.getIdElement();
344 
345 				if (!nextResourceId.hasIdPart()) {
346 					if (isNotBlank(nextReqEntry.getFullUrl())) {
347 						nextResourceId = new IdType(nextReqEntry.getFullUrl());
348 					}
349 				}
350 
351 				if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) {
352 					throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
353 				}
354 
355 				if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
356 					nextResourceId = new IdType(toResourceName(res.getClass()), nextResourceId.getIdPart());
357 					res.setId(nextResourceId);
358 				}
359 
360 				/*
361 				 * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
362 				 */
363 				if (isPlaceholder(nextResourceId)) {
364 					if (!theAllIds.add(nextResourceId)) {
365 						throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
366 					}
367 				} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
368 					IdType nextId = nextResourceId.toUnqualifiedVersionless();
369 					if (!theAllIds.add(nextId)) {
370 						throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
371 					}
372 				}
373 
374 			}
375 
376 			HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue();
377 
378 			String resourceType = res != null ? getContext().getResourceDefinition(res).getName() : null;
379 			BundleEntryComponent nextRespEntry = theResponse.getEntry().get(theOriginalRequestOrder.get(nextReqEntry));
380 
381 			switch (verb) {
382 				case POST: {
383 					// CREATE
384 					@SuppressWarnings("rawtypes")
385 					IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
386 					res.setId((String) null);
387 					DaoMethodOutcome outcome;
388 					String matchUrl = nextReqEntry.getRequest().getIfNoneExist();
389 					matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
390 					outcome = resourceDao.create(res, matchUrl, false, theRequestDetails);
391 					if (nextResourceId != null) {
392 						handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails);
393 					}
394 					entriesToProcess.put(nextRespEntry, outcome.getEntity());
395 					if (outcome.getCreated() == false) {
396 						nonUpdatedEntities.add(outcome.getEntity());
397 					} else {
398 						if (isNotBlank(matchUrl)) {
399 							conditionalRequestUrls.put(matchUrl, res.getClass());
400 						}
401 					}
402 
403 					break;
404 				}
405 				case DELETE: {
406 					// DELETE
407 					String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
408 					UrlParts parts = UrlUtil.parseUrl(url);
409 					ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb.toCode(), url);
410 					int status = Constants.STATUS_HTTP_204_NO_CONTENT;
411 					if (parts.getResourceId() != null) {
412 						IdType deleteId = new IdType(parts.getResourceType(), parts.getResourceId());
413 						if (!deletedResources.contains(deleteId.getValueAsString())) {
414 							DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails);
415 							if (outcome.getEntity() != null) {
416 								deletedResources.add(deleteId.getValueAsString());
417 								entriesToProcess.put(nextRespEntry, outcome.getEntity());
418 							}
419 						}
420 					} else {
421 						String matchUrl = parts.getResourceType() + '?' + parts.getParams();
422 						matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
423 						DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails);
424 						List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
425 						for (ResourceTable deleted : allDeleted) {
426 							deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
427 						}
428 						if (allDeleted.isEmpty()) {
429 							status = Constants.STATUS_HTTP_204_NO_CONTENT;
430 						}
431 
432 						nextRespEntry.getResponse().setOutcome((Resource) deleteOutcome.getOperationOutcome());
433 					}
434 
435 					nextRespEntry.getResponse().setStatus(toStatusString(status));
436 
437 					break;
438 				}
439 				case PUT: {
440 					// UPDATE
441 					@SuppressWarnings("rawtypes")
442 					IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
443 
444 					String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
445 
446 					DaoMethodOutcome outcome;
447 					UrlParts parts = UrlUtil.parseUrl(url);
448 					if (isNotBlank(parts.getResourceId())) {
449 						String version = null;
450 						if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) {
451 							version = ParameterUtil.parseETagValue(nextReqEntry.getRequest().getIfMatch());
452 						}
453 						res.setId(new IdType(parts.getResourceType(), parts.getResourceId(), version));
454 						outcome = resourceDao.update(res, null, false, theRequestDetails);
455 					} else {
456 						res.setId((String) null);
457 						String matchUrl;
458 						if (isNotBlank(parts.getParams())) {
459 							matchUrl = parts.getResourceType() + '?' + parts.getParams();
460 						} else {
461 							matchUrl = parts.getResourceType();
462 						}
463 						matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
464 						outcome = resourceDao.update(res, matchUrl, false, theRequestDetails);
465 						if (Boolean.TRUE.equals(outcome.getCreated())) {
466 							conditionalRequestUrls.put(matchUrl, res.getClass());
467 						}
468 					}
469 
470 					if (outcome.getCreated() == Boolean.FALSE) {
471 						updatedEntities.add(outcome.getEntity());
472 					}
473 
474 					handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails);
475 					entriesToProcess.put(nextRespEntry, outcome.getEntity());
476 					break;
477 				}
478 				case GET:
479 				case NULL:
480 				case HEAD:
481 				case PATCH:
482 					break;
483 
484 			}
485 		}
486 
487 		/*
488 		 * Make sure that there are no conflicts from deletions. E.g. we can't delete something
489 		 * if something else has a reference to it.. Unless the thing that has a reference to it
490 		 * was also deleted as a part of this transaction, which is why we check this now at the
491 		 * end.
492 		 */
493 
494 		deleteConflicts.removeIf(next ->
495 			deletedResources.contains(next.getTargetId().toUnqualifiedVersionless().getValue()));
496 		validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
497 
498 		/*
499 		 * Perform ID substitutions and then index each resource we have saved
500 		 */
501 
502 		FhirTerser terser = getContext().newTerser();
503 		for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
504 			IBaseResource nextResource = nextOutcome.getResource();
505 			if (nextResource == null) {
506 				continue;
507 			}
508 
509 			// References
510 			List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class);
511 			for (IBaseReference nextRef : allRefs) {
512 				IIdType nextId = nextRef.getReferenceElement();
513 				if (!nextId.hasIdPart()) {
514 					continue;
515 				}
516 				if (theIdSubstitutions.containsKey(nextId)) {
517 					IdType newId = theIdSubstitutions.get(nextId);
518 					ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
519 					nextRef.setReference(newId.getValue());
520 				} else if (nextId.getValue().startsWith("urn:")) {
521 					throw new InvalidRequestException("Unable to satisfy placeholder ID: " + nextId.getValue());
522 				} else {
523 					ourLog.debug(" * Reference [{}] does not exist in bundle", nextId);
524 				}
525 			}
526 
527 			// URIs
528 			List<UriType> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, UriType.class);
529 			for (UriType nextRef : allUris) {
530 				if (nextRef instanceof IIdType) {
531 					continue; // No substitution on the resource ID itself!
532 				}
533 				IdType nextUriString = new IdType(nextRef.getValueAsString());
534 				if (theIdSubstitutions.containsKey(nextUriString)) {
535 					IdType newId = theIdSubstitutions.get(nextUriString);
536 					ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
537 					nextRef.setValue(newId.getValue());
538 				} else {
539 					ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString);
540 				}
541 			}
542 
543 			IPrimitiveType<Date> deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource);
544 			Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
545 
546 			if (updatedEntities.contains(nextOutcome.getEntity())) {
547 				updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource());
548 			} else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) {
549 				updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true);
550 			}
551 		}
552 
553 		flushJpaSession();
554 
555 		/*
556 		 * Double check we didn't allow any duplicates we shouldn't have
557 		 */
558 		for (Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
559 			String matchUrl = nextEntry.getKey();
560 			Class<? extends IBaseResource> resType = nextEntry.getValue();
561 			if (isNotBlank(matchUrl)) {
562 				IFhirResourceDao<?> resourceDao = getDao(resType);
563 				Set<Long> val = resourceDao.processMatchUrl(matchUrl);
564 				if (val.size() > 1) {
565 					throw new InvalidRequestException(
566 						"Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");
567 				}
568 			}
569 		}
570 
571 		for (IdType next : theAllIds) {
572 			IdType replacement = theIdSubstitutions.get(next);
573 			if (replacement == null) {
574 				continue;
575 			}
576 			if (replacement.equals(next)) {
577 				continue;
578 			}
579 			ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
580 		}
581 		return entriesToProcess;
582 	}
583 
584 	private String extractTransactionUrlOrThrowException(BundleEntryComponent nextEntry, HTTPVerb verb) {
585 		String url = nextEntry.getRequest().getUrl();
586 		if (isBlank(url)) {
587 			throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
588 		}
589 		return url;
590 	}
591 
592 	/**
593 	 * This method is called for nested bundles (e.g. if we received a transaction with an entry that
594 	 * was a GET search, this method is called on the bundle for the search result, that will be placed in the
595 	 * outer bundle). This method applies the _summary and _content parameters to the output of
596 	 * that bundle.
597 	 * <p>
598 	 * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
599 	 */
600 	private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
601 		IParser p = getContext().newJsonParser();
602 		RestfulServerUtils.configureResponseParser(theRequestDetails, p);
603 		return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
604 	}
605 
606 
607 	@Override
608 	public Meta metaGetOperation(RequestDetails theRequestDetails) {
609 		// Notify interceptors
610 		ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
611 		notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
612 
613 		String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t)";
614 		TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
615 		List<TagDefinition> tagDefinitions = q.getResultList();
616 
617 		return toMeta(tagDefinitions);
618 	}
619 
620 	private String performIdSubstitutionsInMatchUrl(Map<IdType, IdType> theIdSubstitutions, String theMatchUrl) {
621 		String matchUrl = theMatchUrl;
622 		if (isNotBlank(matchUrl)) {
623 			for (Entry<IdType, IdType> nextSubstitutionEntry : theIdSubstitutions.entrySet()) {
624 				IdType nextTemporaryId = nextSubstitutionEntry.getKey();
625 				IdType nextReplacementId = nextSubstitutionEntry.getValue();
626 				String nextTemporaryIdPart = nextTemporaryId.getIdPart();
627 				String nextReplacementIdPart = nextReplacementId.getValueAsString();
628 				if (nextTemporaryId.isUrn() && nextTemporaryIdPart.length() > IdType.URN_PREFIX.length()) {
629 					matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart);
630 					matchUrl = matchUrl.replace(UrlUtil.escapeUrlParam(nextTemporaryIdPart), nextReplacementIdPart);
631 				}
632 			}
633 		}
634 		return matchUrl;
635 	}
636 
637 	private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BundleEntryComponent nextEntry) {
638 		OperationOutcome oo = new OperationOutcome();
639 		oo.addIssue().setSeverity(IssueSeverity.ERROR).setDiagnostics(caughtEx.getMessage());
640 		nextEntry.getResponse().setOutcome(oo);
641 	}
642 
643 	private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
644 		RuntimeResourceDefinition resType;
645 		try {
646 			resType = getContext().getResourceDefinition(theParts.getResourceType());
647 		} catch (DataFormatException e) {
648 			String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
649 			throw new InvalidRequestException(msg);
650 		}
651 		IFhirResourceDao<? extends IBaseResource> dao = null;
652 		if (resType != null) {
653 			dao = getDao(resType.getImplementingClass());
654 		}
655 		if (dao == null) {
656 			String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
657 			throw new InvalidRequestException(msg);
658 		}
659 
660 		// if (theParts.getResourceId() == null && theParts.getParams() == null) {
661 		// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
662 		// throw new InvalidRequestException(msg);
663 		// }
664 
665 		return dao;
666 	}
667 
668 	protected Meta toMeta(Collection<TagDefinition> tagDefinitions) {
669 		Meta retVal = new Meta();
670 		for (TagDefinition next : tagDefinitions) {
671 			switch (next.getTagType()) {
672 				case PROFILE:
673 					retVal.addProfile(next.getCode());
674 					break;
675 				case SECURITY_LABEL:
676 					retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
677 					break;
678 				case TAG:
679 					retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
680 					break;
681 			}
682 		}
683 		return retVal;
684 	}
685 
686 	@Transactional(propagation = Propagation.NEVER)
687 	@Override
688 	public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
689 		if (theRequestDetails != null) {
690 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
691 			notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
692 		}
693 
694 		String actionName = "Transaction";
695 		return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
696 	}
697 
698 	private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
699 		super.markRequestAsProcessingSubRequest(theRequestDetails);
700 		try {
701 			return doTransaction(theRequestDetails, theRequest, theActionName);
702 		} finally {
703 			super.clearRequestAsProcessingSubRequest(theRequestDetails);
704 		}
705 	}
706 
707 	private static void handleTransactionCreateOrUpdateOutcome(Map<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome,
708 																				  BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes, RequestDetails theRequestDetails) {
709 		IdType newId = (IdType) outcome.getId().toUnqualifiedVersionless();
710 		IdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
711 		if (newId.equals(resourceId) == false) {
712 			idSubstitutions.put(resourceId, newId);
713 			if (isPlaceholder(resourceId)) {
714 				/*
715 				 * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient.
716 				 */
717 				idSubstitutions.put(new IdType(theResourceType + '/' + resourceId.getValue()), newId);
718 			}
719 		}
720 		idToPersistedOutcome.put(newId, outcome);
721 		if (outcome.getCreated().booleanValue()) {
722 			newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_201_CREATED));
723 		} else {
724 			newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
725 		}
726 		newEntry.getResponse().setLastModified(((Resource) theRes).getMeta().getLastUpdated());
727 
728 		if (theRequestDetails != null) {
729 			if (outcome.getResource() != null) {
730 				String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER);
731 				PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer);
732 				if (preferReturn != null) {
733 					if (preferReturn == PreferReturnEnum.REPRESENTATION) {
734 						newEntry.setResource((Resource) outcome.getResource());
735 					}
736 				}
737 			}
738 		}
739 
740 	}
741 
742 	private static boolean isPlaceholder(IdType theId) {
743 		if (theId != null && theId.getValue() != null) {
744 			if (theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:")) {
745 				return true;
746 			}
747 		}
748 		return false;
749 	}
750 
751 	private static String toStatusString(int theStatusCode) {
752 		return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
753 	}
754 
755 	/**
756 	 * Transaction Order, per the spec:
757 	 * <p>
758 	 * Process any DELETE interactions
759 	 * Process any POST interactions
760 	 * Process any PUT interactions
761 	 * Process any GET interactions
762 	 */
763 	//@formatter:off
764 	public class TransactionSorter implements Comparator<BundleEntryComponent> {
765 
766 		private Set<String> myPlaceholderIds;
767 
768 		public TransactionSorter(Set<String> thePlaceholderIds) {
769 			myPlaceholderIds = thePlaceholderIds;
770 		}
771 
772 		@Override
773 		public int compare(BundleEntryComponent theO1, BundleEntryComponent theO2) {
774 			int o1 = toOrder(theO1);
775 			int o2 = toOrder(theO2);
776 
777 			if (o1 == o2) {
778 				String matchUrl1 = toMatchUrl(theO1);
779 				String matchUrl2 = toMatchUrl(theO2);
780 				if (isBlank(matchUrl1) && isBlank(matchUrl2)) {
781 					return 0;
782 				}
783 				if (isBlank(matchUrl1)) {
784 					return -1;
785 				}
786 				if (isBlank(matchUrl2)) {
787 					return 1;
788 				}
789 
790 				boolean match1containsSubstitutions = false;
791 				boolean match2containsSubstitutions = false;
792 				for (String nextPlaceholder : myPlaceholderIds) {
793 					if (matchUrl1.contains(nextPlaceholder)) {
794 						match1containsSubstitutions = true;
795 					}
796 					if (matchUrl2.contains(nextPlaceholder)) {
797 						match2containsSubstitutions = true;
798 					}
799 				}
800 
801 				if (match1containsSubstitutions && match2containsSubstitutions) {
802 					return 0;
803 				}
804 				if (!match1containsSubstitutions && !match2containsSubstitutions) {
805 					return 0;
806 				}
807 				if (match1containsSubstitutions) {
808 					return 1;
809 				} else {
810 					return -1;
811 				}
812 			}
813 
814 			return o1 - o2;
815 		}
816 
817 		private String toMatchUrl(BundleEntryComponent theEntry) {
818 			HTTPVerb verb = theEntry.getRequest().getMethod();
819 			if (verb == HTTPVerb.POST) {
820 				return theEntry.getRequest().getIfNoneExist();
821 			}
822 			if (verb == HTTPVerb.PUT || verb == HTTPVerb.DELETE) {
823 				String url = extractTransactionUrlOrThrowException(theEntry, verb);
824 				UrlParts parts = UrlUtil.parseUrl(url);
825 				if (isBlank(parts.getResourceId())) {
826 					return parts.getResourceType() + '?' + parts.getParams();
827 				}
828 			}
829 			return null;
830 		}
831 
832 		private int toOrder(BundleEntryComponent theO1) {
833 			int o1 = 0;
834 			if (theO1.getRequest().getMethodElement().getValue() != null) {
835 				switch (theO1.getRequest().getMethodElement().getValue()) {
836 					case DELETE:
837 						o1 = 1;
838 						break;
839 					case POST:
840 						o1 = 2;
841 						break;
842 					case PUT:
843 						o1 = 3;
844 						break;
845 					case PATCH:
846 						o1 = 4;
847 						break;
848 					case HEAD:
849 						o1 = 5;
850 						break;
851 					case GET:
852 						o1 = 6;
853 						break;
854 					case NULL:
855 						o1 = 0;
856 						break;
857 				}
858 			}
859 			return o1;
860 		}
861 
862 	}
863 
864 	//@formatter:off
865 
866 	private static class BaseServerResponseExceptionHolder {
867 		private BaseServerResponseException myException;
868 
869 		public BaseServerResponseException getException() {
870 			return myException;
871 		}
872 
873 		public void setException(BaseServerResponseException myException) {
874 			this.myException = myException;
875 		}
876 	}
877 
878 }