View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   /*-
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2019 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.context.FhirContext;
24  import ca.uhn.fhir.context.RuntimeResourceDefinition;
25  import ca.uhn.fhir.interceptor.api.HookParams;
26  import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
27  import ca.uhn.fhir.interceptor.api.Pointcut;
28  import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect;
29  import ca.uhn.fhir.jpa.delete.DeleteConflictList;
30  import ca.uhn.fhir.jpa.delete.DeleteConflictService;
31  import ca.uhn.fhir.jpa.model.entity.ResourceTable;
32  import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
33  import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
34  import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
35  import ca.uhn.fhir.jpa.util.DeleteConflict;
36  import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
37  import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
38  import ca.uhn.fhir.parser.DataFormatException;
39  import ca.uhn.fhir.parser.IParser;
40  import ca.uhn.fhir.rest.api.*;
41  import ca.uhn.fhir.rest.api.server.RequestDetails;
42  import ca.uhn.fhir.rest.param.ParameterUtil;
43  import ca.uhn.fhir.rest.server.RestfulServerUtils;
44  import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
45  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
46  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
47  import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
48  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
49  import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
50  import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
51  import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
52  import ca.uhn.fhir.util.*;
53  import com.google.common.base.Charsets;
54  import com.google.common.collect.ArrayListMultimap;
55  import org.apache.commons.lang3.Validate;
56  import org.apache.http.NameValuePair;
57  import org.hibernate.Session;
58  import org.hibernate.internal.SessionImpl;
59  import org.hl7.fhir.dstu3.model.Bundle;
60  import org.hl7.fhir.exceptions.FHIRException;
61  import org.hl7.fhir.instance.model.api.*;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  import org.springframework.beans.factory.annotation.Autowired;
65  import org.springframework.transaction.PlatformTransactionManager;
66  import org.springframework.transaction.TransactionDefinition;
67  import org.springframework.transaction.support.TransactionCallback;
68  import org.springframework.transaction.support.TransactionTemplate;
69  
70  import javax.persistence.EntityManager;
71  import javax.persistence.PersistenceContext;
72  import javax.persistence.PersistenceContextType;
73  import javax.persistence.PersistenceException;
74  import java.util.*;
75  import java.util.stream.Collectors;
76  
77  import static org.apache.commons.lang3.StringUtils.*;
78  
79  public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
80  
81  	public static final String URN_PREFIX = "urn:";
82  	private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class);
83  	private BaseHapiFhirDao myDao;
84  	@Autowired
85  	private PlatformTransactionManager myTxManager;
86  	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
87  	private EntityManager myEntityManager;
88  	@Autowired
89  	private FhirContext myContext;
90  	@Autowired
91  	private ITransactionProcessorVersionAdapter<BUNDLE, BUNDLEENTRY> myVersionAdapter;
92  	@Autowired
93  	private MatchUrlService myMatchUrlService;
94  	@Autowired
95  	private DaoRegistry myDaoRegistry;
96  	@Autowired(required = false)
97  	private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect;
98  	@Autowired
99  	private DeleteConflictService myDeleteConflictService;
100 	@Autowired
101 	private IInterceptorBroadcaster myInterceptorBroadcaster;
102 
103 	public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
104 		if (theRequestDetails != null) {
105 			IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
106 			myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
107 		}
108 
109 		String actionName = "Transaction";
110 		BUNDLE response = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, theRequest, actionName);
111 
112 		List<BUNDLEENTRY> entries = myVersionAdapter.getEntries(response);
113 		for (int i = 0; i < entries.size(); i++) {
114 			if (ElementUtil.isEmpty(entries.get(i))) {
115 				entries.remove(i);
116 				i--;
117 			}
118 		}
119 
120 		return response;
121 	}
122 
123 	public BUNDLE collection(final RequestDetails theRequestDetails, BUNDLE theRequest) {
124 		String transactionType = myVersionAdapter.getBundleType(theRequest);
125 
126 		if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) {
127 			throw new InvalidRequestException("Can not process collection Bundle of type: " + transactionType);
128 		}
129 
130 		ourLog.info("Beginning storing collection with {} resources", myVersionAdapter.getEntries(theRequest).size());
131 		long start = System.currentTimeMillis();
132 
133 		TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
134 		txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
135 
136 		BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode());
137 
138 		List<IBaseResource> resources = new ArrayList<>();
139 		for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) {
140 			IBaseResource resource = myVersionAdapter.getResource(nextRequestEntry);
141 			resources.add(resource);
142 		}
143 
144 		BUNDLE transactionBundle = myVersionAdapter.createBundle("transaction");
145 		for (IBaseResource next : resources) {
146 			BUNDLEENTRY entry = myVersionAdapter.addEntry(transactionBundle);
147 			myVersionAdapter.setResource(entry, next);
148 			myVersionAdapter.setRequestVerb(entry, "PUT");
149 			myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue());
150 		}
151 
152 		transaction(theRequestDetails, transactionBundle);
153 
154 		return resp;
155 	}
156 
157 	private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) {
158 		myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry);
159 	}
160 
161 	private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome,
162 																		 BUNDLEENTRY newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) {
163 		IIdType newId = outcome.getId().toUnqualifiedVersionless();
164 		IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
165 		if (newId.equals(resourceId) == false) {
166 			idSubstitutions.put(resourceId, newId);
167 			if (isPlaceholder(resourceId)) {
168 				/*
169 				 * 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.
170 				 */
171 				IIdType id = myContext.getVersion().newIdType();
172 				id.setValue(theResourceType + '/' + resourceId.getValue());
173 				idSubstitutions.put(id, newId);
174 			}
175 		}
176 		idToPersistedOutcome.put(newId, outcome);
177 		if (outcome.getCreated().booleanValue()) {
178 			myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED));
179 		} else {
180 			myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
181 		}
182 		Date lastModifier = getLastModified(theRes);
183 		myVersionAdapter.setResponseLastModified(newEntry, lastModifier);
184 
185 		if (theRequestDetails != null) {
186 			if (outcome.getResource() != null) {
187 				String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER);
188 				PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(null, prefer);
189 				if (preferReturn != null) {
190 					if (preferReturn == PreferReturnEnum.REPRESENTATION) {
191 						outcome.fireResourceViewCallbacks();
192 						myVersionAdapter.setResource(newEntry, outcome.getResource());
193 					}
194 				}
195 			}
196 		}
197 
198 	}
199 
200 	private Date getLastModified(IBaseResource theRes) {
201 		return theRes.getMeta().getLastUpdated();
202 	}
203 
204 	private String performIdSubstitutionsInMatchUrl(Map<IIdType, IIdType> theIdSubstitutions, String theMatchUrl) {
205 		String matchUrl = theMatchUrl;
206 		if (isNotBlank(matchUrl)) {
207 			for (Map.Entry<IIdType, IIdType> nextSubstitutionEntry : theIdSubstitutions.entrySet()) {
208 				IIdType nextTemporaryId = nextSubstitutionEntry.getKey();
209 				IIdType nextReplacementId = nextSubstitutionEntry.getValue();
210 				String nextTemporaryIdPart = nextTemporaryId.getIdPart();
211 				String nextReplacementIdPart = nextReplacementId.getValueAsString();
212 				if (isUrn(nextTemporaryId) && nextTemporaryIdPart.length() > URN_PREFIX.length()) {
213 					matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart);
214 					matchUrl = matchUrl.replace(UrlUtil.escapeUrlParam(nextTemporaryIdPart), nextReplacementIdPart);
215 				}
216 			}
217 		}
218 		return matchUrl;
219 	}
220 
221 	private boolean isUrn(IIdType theId) {
222 		return defaultString(theId.getValue()).startsWith(URN_PREFIX);
223 	}
224 
225 	public void setDao(BaseHapiFhirDao theDao) {
226 		myDao = theDao;
227 	}
228 
229 	private BUNDLE processTransactionAsSubRequest(ServletRequestDetails theRequestDetails, BUNDLE theRequest, String theActionName) {
230 		BaseHapiFhirDao.markRequestAsProcessingSubRequest(theRequestDetails);
231 		try {
232 			return processTransaction(theRequestDetails, theRequest, theActionName);
233 		} finally {
234 			BaseHapiFhirDao.clearRequestAsProcessingSubRequest(theRequestDetails);
235 		}
236 	}
237 
238 	private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) {
239 		ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size());
240 		long start = System.currentTimeMillis();
241 
242 		TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
243 		txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
244 
245 		BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode());
246 
247 		/*
248 		 * 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
249 		 */
250 
251 		for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) {
252 
253 			BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
254 
255 			try {
256 				BUNDLE subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode());
257 				myVersionAdapter.addEntry(subRequestBundle, nextRequestEntry);
258 
259 				BUNDLE nextResponseBundle = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request");
260 
261 				BUNDLEENTRY subResponseEntry = myVersionAdapter.getEntries(nextResponseBundle).get(0);
262 				myVersionAdapter.addEntry(resp, subResponseEntry);
263 
264 				/*
265 				 * 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
266 				 */
267 				if (myVersionAdapter.getResource(subResponseEntry) == null) {
268 					BUNDLEENTRY nextResponseBundleFirstEntry = myVersionAdapter.getEntries(nextResponseBundle).get(0);
269 					myVersionAdapter.setResource(subResponseEntry, myVersionAdapter.getResource(nextResponseBundleFirstEntry));
270 				}
271 
272 			} catch (BaseServerResponseException e) {
273 				caughtEx.setException(e);
274 			} catch (Throwable t) {
275 				ourLog.error("Failure during BATCH sub transaction processing", t);
276 				caughtEx.setException(new InternalErrorException(t));
277 			}
278 
279 			if (caughtEx.getException() != null) {
280 				BUNDLEENTRY nextEntry = myVersionAdapter.addEntry(resp);
281 
282 				populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry);
283 
284 				myVersionAdapter.setResponseStatus(nextEntry, toStatusString(caughtEx.getException().getStatusCode()));
285 			}
286 
287 		}
288 
289 		long delay = System.currentTimeMillis() - start;
290 		ourLog.info("Batch completed in {}ms", new Object[]{delay});
291 
292 		return resp;
293 	}
294 
295 	private BUNDLE processTransaction(final ServletRequestDetails theRequestDetails, final BUNDLE theRequest, final String theActionName) {
296 		validateDependencies();
297 
298 		String transactionType = myVersionAdapter.getBundleType(theRequest);
299 
300 		if (org.hl7.fhir.r4.model.Bundle.BundleType.BATCH.toCode().equals(transactionType)) {
301 			return batch(theRequestDetails, theRequest);
302 		}
303 
304 		if (transactionType == null) {
305 			String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + Bundle.BundleType.TRANSACTION.toCode();
306 			ourLog.warn(message);
307 			transactionType = org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode();
308 		}
309 		if (!org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode().equals(transactionType)) {
310 			throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType);
311 		}
312 
313 		ourLog.debug("Beginning {} with {} resources", theActionName, myVersionAdapter.getEntries(theRequest).size());
314 
315 		final Date updateTime = new Date();
316 		final StopWatch transactionStopWatch = new StopWatch();
317 
318 		final Set<IIdType> allIds = new LinkedHashSet<>();
319 		final Map<IIdType, IIdType> idSubstitutions = new HashMap<>();
320 		final Map<IIdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<>();
321 		List<BUNDLEENTRY> requestEntries = myVersionAdapter.getEntries(theRequest);
322 
323 		// Do all entries have a verb?
324 		for (int i = 0; i < myVersionAdapter.getEntries(theRequest).size(); i++) {
325 			BUNDLEENTRY nextReqEntry = requestEntries.get(i);
326 			String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
327 			if (verb == null || !isValidVerb(verb)) {
328 				throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", verb, i));
329 			}
330 		}
331 
332 		/*
333 		 * We want to execute the transaction request bundle elements in the order
334 		 * specified by the FHIR specification (see TransactionSorter) so we save the
335 		 * original order in the request, then sort it.
336 		 *
337 		 * Entries with a type of GET are removed from the bundle so that they
338 		 * can be processed at the very end. We do this because the incoming resources
339 		 * are saved in a two-phase way in order to deal with interdependencies, and
340 		 * we want the GET processing to use the final indexing state
341 		 */
342 		final BUNDLE response = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTIONRESPONSE.toCode());
343 		List<BUNDLEENTRY> getEntries = new ArrayList<>();
344 		final IdentityHashMap<BUNDLEENTRY, Integer> originalRequestOrder = new IdentityHashMap<>();
345 		for (int i = 0; i < requestEntries.size(); i++) {
346 			originalRequestOrder.put(requestEntries.get(i), i);
347 			myVersionAdapter.addEntry(response);
348 			if (myVersionAdapter.getEntryRequestVerb(requestEntries.get(i)).equals("GET")) {
349 				getEntries.add(requestEntries.get(i));
350 			}
351 		}
352 
353 		/*
354 		 * See FhirSystemDaoDstu3Test#testTransactionWithPlaceholderIdInMatchUrl
355 		 * Basically if the resource has a match URL that references a placeholder,
356 		 * we try to handle the resource with the placeholder first.
357 		 */
358 		Set<String> placeholderIds = new HashSet<>();
359 		final List<BUNDLEENTRY> entries = requestEntries;
360 		for (BUNDLEENTRY nextEntry : entries) {
361 			String fullUrl = myVersionAdapter.getFullUrl(nextEntry);
362 			if (isNotBlank(fullUrl) && fullUrl.startsWith(URN_PREFIX)) {
363 				placeholderIds.add(fullUrl);
364 			}
365 		}
366 		entries.sort(new TransactionSorter(placeholderIds));
367 
368 		/*
369 		 * All of the write operations in the transaction (PUT, POST, etc.. basically anything
370 		 * except GET) are performed in their own database transaction before we do the reads.
371 		 * We do this because the reads (specifically the searches) often spawn their own
372 		 * secondary database transaction and if we allow that within the primary
373 		 * database transaction we can end up with deadlocks if the server is under
374 		 * heavy load with lots of concurrent transactions using all available
375 		 * database connections.
376 		 */
377 		TransactionTemplate txManager = new TransactionTemplate(myTxManager);
378 		Map<BUNDLEENTRY, ResourceTable> entriesToProcess = txManager.execute(status -> {
379 			Map<BUNDLEENTRY, ResourceTable> retVal = doTransactionWriteOperations(theRequestDetails, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries, transactionStopWatch);
380 
381 			transactionStopWatch.startTask("Commit writes to database");
382 			return retVal;
383 		});
384 		transactionStopWatch.endCurrentTask();
385 
386 		for (Map.Entry<BUNDLEENTRY, ResourceTable> nextEntry : entriesToProcess.entrySet()) {
387 			String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue();
388 			String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart();
389 			myVersionAdapter.setResponseLocation(nextEntry.getKey(), responseLocation);
390 			myVersionAdapter.setResponseETag(nextEntry.getKey(), responseEtag);
391 		}
392 
393 		/*
394 		 * Loop through the request and process any entries of type GET
395 		 */
396 		if (getEntries.size() > 0) {
397 			transactionStopWatch.startTask("Process " + getEntries.size() + " GET entries");
398 		}
399 		for (BUNDLEENTRY nextReqEntry : getEntries) {
400 			Integer originalOrder = originalRequestOrder.get(nextReqEntry);
401 			BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(response).get(originalOrder);
402 
403 			ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(theRequestDetails);
404 			requestDetails.setServletRequest(theRequestDetails.getServletRequest());
405 			requestDetails.setRequestType(RequestTypeEnum.GET);
406 			requestDetails.setServer(theRequestDetails.getServer());
407 
408 			String url = extractTransactionUrlOrThrowException(nextReqEntry, "GET");
409 
410 			int qIndex = url.indexOf('?');
411 			ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
412 			requestDetails.setParameters(new HashMap<>());
413 			if (qIndex != -1) {
414 				String params = url.substring(qIndex);
415 				List<NameValuePair> parameters = myMatchUrlService.translateMatchUrl(params);
416 				for (NameValuePair next : parameters) {
417 					paramValues.put(next.getName(), next.getValue());
418 				}
419 				for (Map.Entry<String, Collection<String>> nextParamEntry : paramValues.asMap().entrySet()) {
420 					String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]);
421 					requestDetails.addParameter(nextParamEntry.getKey(), nextValue);
422 				}
423 				url = url.substring(0, qIndex);
424 			}
425 
426 			requestDetails.setRequestPath(url);
427 			requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase());
428 
429 			theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url);
430 			BaseMethodBinding<?> method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url);
431 			if (method == null) {
432 				throw new IllegalArgumentException("Unable to handle GET " + url);
433 			}
434 
435 			if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
436 				requestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
437 			}
438 			if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry))) {
439 				requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry));
440 			}
441 			if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry))) {
442 				requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry));
443 			}
444 
445 			Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
446 			try {
447 
448 				BaseResourceReturningMethodBinding methodBinding = (BaseResourceReturningMethodBinding) method;
449 				requestDetails.setRestOperationType(methodBinding.getRestOperationType());
450 
451 				IBaseResource resource = methodBinding.doInvokeServer(theRequestDetails.getServer(), requestDetails);
452 				if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
453 					resource = filterNestedBundle(requestDetails, resource);
454 				}
455 				myVersionAdapter.setResource(nextRespEntry, resource);
456 				myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
457 			} catch (NotModifiedException e) {
458 				myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
459 			} catch (BaseServerResponseException e) {
460 				ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
461 				myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode()));
462 				populateEntryWithOperationOutcome(e, nextRespEntry);
463 			}
464 
465 		}
466 		transactionStopWatch.endCurrentTask();
467 
468 		// Interceptor broadcast: JPA_PERFTRACE_INFO
469 		if (JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequestDetails)) {
470 			String taskDurations = transactionStopWatch.formatTaskDurations();
471 			StorageProcessingMessage message = new StorageProcessingMessage();
472 			message.setMessage("Transaction timing:\n" + taskDurations);
473 			HookParams params = new HookParams()
474 				.add(RequestDetails.class, theRequestDetails)
475 				.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
476 				.add(StorageProcessingMessage.class, message);
477 			JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_INFO, params);
478 		}
479 
480 		return response;
481 	}
482 
483 	private boolean isValidVerb(String theVerb) {
484 		try {
485 			return org.hl7.fhir.r4.model.Bundle.HTTPVerb.fromCode(theVerb) != null;
486 		} catch (FHIRException theE) {
487 			return false;
488 		}
489 	}
490 
491 	/**
492 	 * This method is called for nested bundles (e.g. if we received a transaction with an entry that
493 	 * was a GET search, this method is called on the bundle for the search result, that will be placed in the
494 	 * outer bundle). This method applies the _summary and _content parameters to the output of
495 	 * that bundle.
496 	 * <p>
497 	 * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
498 	 */
499 	private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
500 		IParser p = myContext.newJsonParser();
501 		RestfulServerUtils.configureResponseParser(theRequestDetails, p);
502 		return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
503 	}
504 
505 	private void validateDependencies() {
506 		Validate.notNull(myEntityManager);
507 		Validate.notNull(myContext);
508 		Validate.notNull(myDao);
509 		Validate.notNull(myTxManager);
510 	}
511 
512 	private IIdType newIdType(String theValue) {
513 		return myContext.getVersion().newIdType().setValue(theValue);
514 	}
515 
516 
517 	private Map<BUNDLEENTRY, ResourceTable> doTransactionWriteOperations(final ServletRequestDetails theRequest, String theActionName, Date theUpdateTime, Set<IIdType> theAllIds,
518 																								Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, BUNDLE theResponse, IdentityHashMap<BUNDLEENTRY, Integer> theOriginalRequestOrder, List<BUNDLEENTRY> theEntries, StopWatch theTransactionStopWatch) {
519 
520 		if (theRequest != null) {
521 			theRequest.startDeferredOperationCallback();
522 		}
523 		try {
524 
525 			Set<String> deletedResources = new HashSet<>();
526 			DeleteConflictList deleteConflicts = new DeleteConflictList();
527 			Map<BUNDLEENTRY, ResourceTable> entriesToProcess = new IdentityHashMap<>();
528 			Set<ResourceTable> nonUpdatedEntities = new HashSet<>();
529 			Set<ResourceTable> updatedEntities = new HashSet<>();
530 			List<IBaseResource> updatedResources = new ArrayList<>();
531 			Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<>();
532 
533 			/*
534 			 * Look for duplicate conditional creates and consolidate them
535 			 */
536 			final HashMap<String, String> keyToUuid = new HashMap<>();
537 			final IdentityHashMap<IBaseResource, String> identityToUuid = new IdentityHashMap<>();
538 			for (int index = 0, originalIndex = 0; index < theEntries.size(); index++, originalIndex++) {
539 				BUNDLEENTRY nextReqEntry = theEntries.get(index);
540 				IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
541 				if (resource != null) {
542 					String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
543 					String entryUrl = myVersionAdapter.getFullUrl(nextReqEntry);
544 					String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
545 					String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
546 					String key = verb + "|" + requestUrl + "|" + ifNoneExist;
547 
548 					// Conditional UPDATE
549 					boolean consolidateEntry = false;
550 					if ("PUT".equals(verb)) {
551 						if (isNotBlank(entryUrl) && isNotBlank(requestUrl)) {
552 							int questionMarkIndex = requestUrl.indexOf('?');
553 							if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) {
554 								consolidateEntry = true;
555 							}
556 						}
557 					}
558 
559 					// Conditional CREATE
560 					if ("POST".equals(verb)) {
561 						if (isNotBlank(entryUrl) && isNotBlank(requestUrl) && isNotBlank(ifNoneExist)) {
562 							if (!entryUrl.equals(requestUrl)) {
563 								consolidateEntry = true;
564 							}
565 						}
566 					}
567 
568 					if (consolidateEntry) {
569 						if (!keyToUuid.containsKey(key)) {
570 							keyToUuid.put(key, entryUrl);
571 							identityToUuid.put(resource, entryUrl);
572 						} else {
573 							ourLog.info("Discarding transaction bundle entry {} as it contained a duplicate conditional {}", originalIndex, verb);
574 							theEntries.remove(index);
575 							index--;
576 							String existingUuid = keyToUuid.get(key);
577 							for (BUNDLEENTRY nextEntry : theEntries) {
578 								IBaseResource nextResource = myVersionAdapter.getResource(nextEntry);
579 								for (ResourceReferenceInfo nextReference : myContext.newTerser().getAllResourceReferences(nextResource)) {
580 									// We're interested in any references directly to the placeholder ID, but also
581 									// references that have a resource target that has the placeholder ID.
582 									String nextReferenceId = nextReference.getResourceReference().getReferenceElement().getValue();
583 									if (isBlank(nextReferenceId) && nextReference.getResourceReference().getResource() != null) {
584 										nextReferenceId = nextReference.getResourceReference().getResource().getIdElement().getValue();
585 									}
586 									if (entryUrl.equals(nextReferenceId)) {
587 										nextReference.getResourceReference().setReference(existingUuid);
588 										nextReference.getResourceReference().setResource(null);
589 									}
590 								}
591 							}
592 						}
593 					}
594 				}
595 			}
596 
597 
598 			/*
599 			 * Loop through the request and process any entries of type
600 			 * PUT, POST or DELETE
601 			 */
602 			for (int i = 0; i < theEntries.size(); i++) {
603 
604 				if (i % 250 == 0) {
605 					ourLog.info("Processed {} non-GET entries out of {} in transaction", i, theEntries.size());
606 				}
607 
608 				BUNDLEENTRY nextReqEntry = theEntries.get(i);
609 				IBaseResource res = myVersionAdapter.getResource(nextReqEntry);
610 				IIdType nextResourceId = null;
611 				if (res != null) {
612 
613 					nextResourceId = res.getIdElement();
614 
615 					if (!nextResourceId.hasIdPart()) {
616 						if (isNotBlank(myVersionAdapter.getFullUrl(nextReqEntry))) {
617 							nextResourceId = newIdType(myVersionAdapter.getFullUrl(nextReqEntry));
618 						}
619 					}
620 
621 					if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) {
622 						throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
623 					}
624 
625 					if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
626 						nextResourceId = newIdType(toResourceName(res.getClass()), nextResourceId.getIdPart());
627 						res.setId(nextResourceId);
628 					}
629 
630 					/*
631 					 * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
632 					 */
633 					if (isPlaceholder(nextResourceId)) {
634 						if (!theAllIds.add(nextResourceId)) {
635 							throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
636 						}
637 					} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
638 						IIdType nextId = nextResourceId.toUnqualifiedVersionless();
639 						if (!theAllIds.add(nextId)) {
640 							throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
641 						}
642 					}
643 
644 				}
645 
646 				String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
647 				String resourceType = res != null ? myContext.getResourceDefinition(res).getName() : null;
648 				Integer order = theOriginalRequestOrder.get(nextReqEntry);
649 				BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(theResponse).get(order);
650 
651 				theTransactionStopWatch.startTask("Bundle.entry[" + i + "]: " + verb + " " + defaultString(resourceType));
652 
653 				switch (verb) {
654 					case "POST": {
655 						// CREATE
656 						validateResourcePresent(res, order, verb);
657 						@SuppressWarnings("rawtypes")
658 						IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
659 						res.setId((String) null);
660 						DaoMethodOutcome outcome;
661 						String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
662 						matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
663 						outcome = resourceDao.create(res, matchUrl, false, theUpdateTime, theRequest);
664 						if (nextResourceId != null) {
665 							handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest);
666 						}
667 						entriesToProcess.put(nextRespEntry, outcome.getEntity());
668 						if (outcome.getCreated() == false) {
669 							nonUpdatedEntities.add(outcome.getEntity());
670 						} else {
671 							if (isNotBlank(matchUrl)) {
672 								conditionalRequestUrls.put(matchUrl, res.getClass());
673 							}
674 						}
675 
676 						break;
677 					}
678 					case "DELETE": {
679 						// DELETE
680 						String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
681 						UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
682 						ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
683 						int status = Constants.STATUS_HTTP_204_NO_CONTENT;
684 						if (parts.getResourceId() != null) {
685 							IIdType deleteId = newIdType(parts.getResourceType(), parts.getResourceId());
686 							if (!deletedResources.contains(deleteId.getValueAsString())) {
687 								DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequest);
688 								if (outcome.getEntity() != null) {
689 									deletedResources.add(deleteId.getValueAsString());
690 									entriesToProcess.put(nextRespEntry, outcome.getEntity());
691 								}
692 							}
693 						} else {
694 							String matchUrl = parts.getResourceType() + '?' + parts.getParams();
695 							matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
696 							DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequest);
697 							List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
698 							for (ResourceTable deleted : allDeleted) {
699 								deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
700 							}
701 							if (allDeleted.isEmpty()) {
702 								status = Constants.STATUS_HTTP_204_NO_CONTENT;
703 							}
704 
705 							myVersionAdapter.setResponseOutcome(nextRespEntry, deleteOutcome.getOperationOutcome());
706 						}
707 
708 						myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(status));
709 
710 						break;
711 					}
712 					case "PUT": {
713 						// UPDATE
714 						validateResourcePresent(res, order, verb);
715 						@SuppressWarnings("rawtypes")
716 						IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
717 
718 						String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
719 
720 						DaoMethodOutcome outcome;
721 						UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
722 						if (isNotBlank(parts.getResourceId())) {
723 							String version = null;
724 							if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
725 								version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
726 							}
727 							res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version));
728 							outcome = resourceDao.update(res, null, false, false, theRequest);
729 						} else {
730 							res.setId((String) null);
731 							String matchUrl;
732 							if (isNotBlank(parts.getParams())) {
733 								matchUrl = parts.getResourceType() + '?' + parts.getParams();
734 							} else {
735 								matchUrl = parts.getResourceType();
736 							}
737 							matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
738 							outcome = resourceDao.update(res, matchUrl, false, false, theRequest);
739 							if (Boolean.TRUE.equals(outcome.getCreated())) {
740 								conditionalRequestUrls.put(matchUrl, res.getClass());
741 							}
742 						}
743 
744 						if (outcome.getCreated() == Boolean.FALSE
745 							|| (outcome.getCreated() == Boolean.TRUE && outcome.getId().getVersionIdPartAsLong() > 1)) {
746 							updatedEntities.add(outcome.getEntity());
747 							if (outcome.getResource() != null) {
748 								updatedResources.add(outcome.getResource());
749 							}
750 						}
751 
752 						handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest);
753 						entriesToProcess.put(nextRespEntry, outcome.getEntity());
754 						break;
755 					}
756 					case "PATCH": {
757 						// PATCH
758 						validateResourcePresent(res, order, verb);
759 
760 						String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
761 						UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
762 
763 						String matchUrl = toMatchUrl(nextReqEntry);
764 						matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
765 						String patchBody = null;
766 						String contentType = null;
767 
768 						if (res instanceof IBaseBinary) {
769 							IBaseBinary binary = (IBaseBinary) res;
770 							if (binary.getContent() != null && binary.getContent().length > 0) {
771 								patchBody = new String(binary.getContent(), Charsets.UTF_8);
772 							}
773 							contentType = binary.getContentType();
774 						}
775 
776 						if (isBlank(patchBody)) {
777 							String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingPatchBody");
778 							throw new InvalidRequestException(msg);
779 						}
780 						if (isBlank(contentType)) {
781 							String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingPatchContentType");
782 							throw new InvalidRequestException(msg);
783 						}
784 
785 						ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
786 						PatchTypeEnum patchType = PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(contentType);
787 						IIdType patchId = myContext.getVersion().newIdType().setValue(parts.getResourceId());
788 						DaoMethodOutcome outcome = dao.patch(patchId, matchUrl, patchType, patchBody, theRequest);
789 						updatedEntities.add(outcome.getEntity());
790 						if (outcome.getResource() != null) {
791 							updatedResources.add(outcome.getResource());
792 						}
793 
794 						break;
795 					}
796 					case "GET":
797 						break;
798 					default:
799 						throw new InvalidRequestException("Unable to handle verb in transaction: " + verb);
800 
801 				}
802 
803 				theTransactionStopWatch.endCurrentTask();
804 			}
805 
806 
807 			/*
808 			 * Make sure that there are no conflicts from deletions. E.g. we can't delete something
809 			 * if something else has a reference to it.. Unless the thing that has a reference to it
810 			 * was also deleted as a part of this transaction, which is why we check this now at the
811 			 * end.
812 			 */
813 			for (Iterator<DeleteConflict> iter = deleteConflicts.iterator(); iter.hasNext(); ) {
814 				DeleteConflict nextDeleteConflict = iter.next();
815 
816 				/*
817 				 * If we have a conflict, it means we can't delete Resource/A because
818 				 * Resource/B has a reference to it. We'll ignore that conflict though
819 				 * if it turns out we're also deleting Resource/B in this transaction.
820 				 */
821 				if (deletedResources.contains(nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue())) {
822 					iter.remove();
823 					continue;
824 				}
825 
826 				/*
827 				 * And then, this is kind of a last ditch check. It's also ok to delete
828 				 * Resource/A if Resource/B isn't being deleted, but it is being UPDATED
829 				 * in this transaction, and the updated version of it has no references
830 				 * to Resource/A any more.
831 				 */
832 				String sourceId = nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue();
833 				String targetId = nextDeleteConflict.getTargetId().toUnqualifiedVersionless().getValue();
834 				Optional<IBaseResource> updatedSource = updatedResources
835 					.stream()
836 					.filter(t -> sourceId.equals(t.getIdElement().toUnqualifiedVersionless().getValue()))
837 					.findFirst();
838 				if (updatedSource.isPresent()) {
839 					List<ResourceReferenceInfo> referencesInSource = myContext.newTerser().getAllResourceReferences(updatedSource.get());
840 					boolean sourceStillReferencesTarget = referencesInSource
841 						.stream()
842 						.anyMatch(t -> targetId.equals(t.getResourceReference().getReferenceElement().toUnqualifiedVersionless().getValue()));
843 					if (!sourceStillReferencesTarget) {
844 						iter.remove();
845 					}
846 				}
847 			}
848 			myDeleteConflictService.validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
849 
850 			/*
851 			 * Perform ID substitutions and then index each resource we have saved
852 			 */
853 
854 			FhirTerser terser = myContext.newTerser();
855 			theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
856 			int i = 0;
857 			for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
858 
859 				if (i++ % 250 == 0) {
860 					ourLog.info("Have indexed {} entities out of {} in transaction", i, theIdToPersistedOutcome.values().size());
861 				}
862 
863 				IBaseResource nextResource = nextOutcome.getResource();
864 				if (nextResource == null) {
865 					continue;
866 				}
867 
868 				// References
869 				List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(nextResource);
870 				for (ResourceReferenceInfo nextRef : allRefs) {
871 					IIdType nextId = nextRef.getResourceReference().getReferenceElement();
872 					if (!nextId.hasIdPart()) {
873 						continue;
874 					}
875 					if (theIdSubstitutions.containsKey(nextId)) {
876 						IIdType newId = theIdSubstitutions.get(nextId);
877 						ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
878 						nextRef.getResourceReference().setReference(newId.getValue());
879 					} else if (nextId.getValue().startsWith("urn:")) {
880 						throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType());
881 					} else {
882 						ourLog.debug(" * Reference [{}] does not exist in bundle", nextId);
883 					}
884 				}
885 
886 				// URIs
887 				Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) myContext.getElementDefinition("uri").getImplementingClass();
888 				List<? extends IPrimitiveType<?>> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, uriType);
889 				for (IPrimitiveType<?> nextRef : allUris) {
890 					if (nextRef instanceof IIdType) {
891 						continue; // No substitution on the resource ID itself!
892 					}
893 					IIdType nextUriString = newIdType(nextRef.getValueAsString());
894 					if (theIdSubstitutions.containsKey(nextUriString)) {
895 						IIdType newId = theIdSubstitutions.get(nextUriString);
896 						ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
897 						nextRef.setValueAsString(newId.getValue());
898 					} else {
899 						ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString);
900 					}
901 				}
902 
903 				IPrimitiveType<Date> deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource);
904 				Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
905 
906 				if (updatedEntities.contains(nextOutcome.getEntity())) {
907 					myDao.updateInternal(theRequest, nextResource, true, false, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource());
908 				} else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) {
909 					myDao.updateEntity(theRequest, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true);
910 				}
911 			}
912 
913 			theTransactionStopWatch.endCurrentTask();
914 			theTransactionStopWatch.startTask("Flush writes to database");
915 
916 			try {
917 				flushJpaSession();
918 			} catch (PersistenceException e) {
919 				if (myHapiFhirHibernateJpaDialect != null) {
920 					List<String> types = theIdToPersistedOutcome.keySet().stream().filter(t -> t != null).map(t -> t.getResourceType()).collect(Collectors.toList());
921 					String message = "Error flushing transaction with resource types: " + types;
922 					throw myHapiFhirHibernateJpaDialect.translate(e, message);
923 				}
924 				throw e;
925 			}
926 
927 			theTransactionStopWatch.endCurrentTask();
928 			if (conditionalRequestUrls.size() > 0) {
929 				theTransactionStopWatch.startTask("Check for conflicts in conditional resources");
930 			}
931 
932 			/*
933 			 * Double check we didn't allow any duplicates we shouldn't have
934 			 */
935 			for (Map.Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
936 				String matchUrl = nextEntry.getKey();
937 				Class<? extends IBaseResource> resType = nextEntry.getValue();
938 				if (isNotBlank(matchUrl)) {
939 					IFhirResourceDao<?> resourceDao = myDao.getDao(resType);
940 					Set<Long> val = resourceDao.processMatchUrl(matchUrl, theRequest);
941 					if (val.size() > 1) {
942 						throw new InvalidRequestException(
943 							"Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");
944 					}
945 				}
946 			}
947 
948 			theTransactionStopWatch.endCurrentTask();
949 
950 			for (IIdType next : theAllIds) {
951 				IIdType replacement = theIdSubstitutions.get(next);
952 				if (replacement == null) {
953 					continue;
954 				}
955 				if (replacement.equals(next)) {
956 					continue;
957 				}
958 				ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
959 			}
960 			return entriesToProcess;
961 
962 		} finally {
963 			if (theRequest != null) {
964 				theRequest.stopDeferredRequestOperationCallbackAndRunDeferredItems();
965 			}
966 		}
967 	}
968 
969 	private void validateResourcePresent(IBaseResource theResource, Integer theOrder, String theVerb) {
970 		if (theResource == null) {
971 			String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingMandatoryResource", theVerb, theOrder);
972 			throw new InvalidRequestException(msg);
973 		}
974 	}
975 
976 	private IIdType newIdType(String theResourceType, String theResourceId, String theVersion) {
977 		org.hl7.fhir.r4.model.IdType id = new org.hl7.fhir.r4.model.IdType(theResourceType, theResourceId, theVersion);
978 		return myContext.getVersion().newIdType().setValue(id.getValue());
979 	}
980 
981 	private IIdType newIdType(String theToResourceName, String theIdPart) {
982 		return newIdType(theToResourceName, theIdPart, null);
983 	}
984 
985 	private IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
986 		return myDaoRegistry.getResourceDao(theClass);
987 	}
988 
989 	protected void flushJpaSession() {
990 		SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class);
991 		int insertionCount = session.getActionQueue().numberOfInsertions();
992 		int updateCount = session.getActionQueue().numberOfUpdates();
993 
994 		StopWatch sw = new StopWatch();
995 		myEntityManager.flush();
996 		ourLog.debug("Session flush took {}ms for {} inserts and {} updates", sw.getMillis(), insertionCount, updateCount);
997 	}
998 
999 	protected String toResourceName(Class<? extends IBaseResource> theResourceType) {
1000 		return myContext.getResourceDefinition(theResourceType).getName();
1001 	}
1002 
1003 	public void setContext(FhirContext theContext) {
1004 		myContext = theContext;
1005 	}
1006 
1007 	private String extractTransactionUrlOrThrowException(BUNDLEENTRY nextEntry, String verb) {
1008 		String url = myVersionAdapter.getEntryRequestUrl(nextEntry);
1009 		if (isBlank(url)) {
1010 			throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb));
1011 		}
1012 		return url;
1013 	}
1014 
1015 	private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlUtil.UrlParts theParts, String theVerb, String theUrl) {
1016 		RuntimeResourceDefinition resType;
1017 		try {
1018 			resType = myContext.getResourceDefinition(theParts.getResourceType());
1019 		} catch (DataFormatException e) {
1020 			String msg = myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
1021 			throw new InvalidRequestException(msg);
1022 		}
1023 		IFhirResourceDao<? extends IBaseResource> dao = null;
1024 		if (resType != null) {
1025 			dao = myDao.getDao(resType.getImplementingClass());
1026 		}
1027 		if (dao == null) {
1028 			String msg = myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
1029 			throw new InvalidRequestException(msg);
1030 		}
1031 
1032 		// if (theParts.getResourceId() == null && theParts.getParams() == null) {
1033 		// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
1034 		// throw new InvalidRequestException(msg);
1035 		// }
1036 
1037 		return dao;
1038 	}
1039 
1040 	public interface ITransactionProcessorVersionAdapter<BUNDLE, BUNDLEENTRY> {
1041 
1042 		void setResponseStatus(BUNDLEENTRY theBundleEntry, String theStatus);
1043 
1044 		void setResponseLastModified(BUNDLEENTRY theBundleEntry, Date theLastModified);
1045 
1046 		void setResource(BUNDLEENTRY theBundleEntry, IBaseResource theResource);
1047 
1048 		IBaseResource getResource(BUNDLEENTRY theBundleEntry);
1049 
1050 		String getBundleType(BUNDLE theRequest);
1051 
1052 		void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, BUNDLEENTRY theEntry);
1053 
1054 		BUNDLE createBundle(String theBundleType);
1055 
1056 		List<BUNDLEENTRY> getEntries(BUNDLE theRequest);
1057 
1058 		void addEntry(BUNDLE theBundle, BUNDLEENTRY theEntry);
1059 
1060 		BUNDLEENTRY addEntry(BUNDLE theBundle);
1061 
1062 		String getEntryRequestVerb(BUNDLEENTRY theEntry);
1063 
1064 		String getFullUrl(BUNDLEENTRY theEntry);
1065 
1066 		String getEntryIfNoneExist(BUNDLEENTRY theEntry);
1067 
1068 		String getEntryRequestUrl(BUNDLEENTRY theEntry);
1069 
1070 		void setResponseLocation(BUNDLEENTRY theEntry, String theResponseLocation);
1071 
1072 		void setResponseETag(BUNDLEENTRY theEntry, String theEtag);
1073 
1074 		String getEntryRequestIfMatch(BUNDLEENTRY theEntry);
1075 
1076 		String getEntryRequestIfNoneExist(BUNDLEENTRY theEntry);
1077 
1078 		String getEntryRequestIfNoneMatch(BUNDLEENTRY theEntry);
1079 
1080 		void setResponseOutcome(BUNDLEENTRY theEntry, IBaseOperationOutcome theOperationOutcome);
1081 
1082 		void setRequestVerb(BUNDLEENTRY theEntry, String theVerb);
1083 
1084 		void setRequestUrl(BUNDLEENTRY theEntry, String theUrl);
1085 	}
1086 
1087 	/**
1088 	 * Transaction Order, per the spec:
1089 	 * <p>
1090 	 * Process any DELETE interactions
1091 	 * Process any POST interactions
1092 	 * Process any PUT interactions
1093 	 * Process any PATCH interactions
1094 	 * Process any GET interactions
1095 	 */
1096 	//@formatter:off
1097 	public class TransactionSorter implements Comparator<BUNDLEENTRY> {
1098 
1099 		private Set<String> myPlaceholderIds;
1100 
1101 		public TransactionSorter(Set<String> thePlaceholderIds) {
1102 			myPlaceholderIds = thePlaceholderIds;
1103 		}
1104 
1105 		@Override
1106 		public int compare(BUNDLEENTRY theO1, BUNDLEENTRY theO2) {
1107 			int o1 = toOrder(theO1);
1108 			int o2 = toOrder(theO2);
1109 
1110 			if (o1 == o2) {
1111 				String matchUrl1 = toMatchUrl(theO1);
1112 				String matchUrl2 = toMatchUrl(theO2);
1113 				if (isBlank(matchUrl1) && isBlank(matchUrl2)) {
1114 					return 0;
1115 				}
1116 				if (isBlank(matchUrl1)) {
1117 					return -1;
1118 				}
1119 				if (isBlank(matchUrl2)) {
1120 					return 1;
1121 				}
1122 
1123 				boolean match1containsSubstitutions = false;
1124 				boolean match2containsSubstitutions = false;
1125 				for (String nextPlaceholder : myPlaceholderIds) {
1126 					if (matchUrl1.contains(nextPlaceholder)) {
1127 						match1containsSubstitutions = true;
1128 					}
1129 					if (matchUrl2.contains(nextPlaceholder)) {
1130 						match2containsSubstitutions = true;
1131 					}
1132 				}
1133 
1134 				if (match1containsSubstitutions && match2containsSubstitutions) {
1135 					return 0;
1136 				}
1137 				if (!match1containsSubstitutions && !match2containsSubstitutions) {
1138 					return 0;
1139 				}
1140 				if (match1containsSubstitutions) {
1141 					return 1;
1142 				} else {
1143 					return -1;
1144 				}
1145 			}
1146 
1147 			return o1 - o2;
1148 		}
1149 
1150 		private int toOrder(BUNDLEENTRY theO1) {
1151 			int o1 = 0;
1152 			if (myVersionAdapter.getEntryRequestVerb(theO1) != null) {
1153 				switch (myVersionAdapter.getEntryRequestVerb(theO1)) {
1154 					case "DELETE":
1155 						o1 = 1;
1156 						break;
1157 					case "POST":
1158 						o1 = 2;
1159 						break;
1160 					case "PUT":
1161 						o1 = 3;
1162 						break;
1163 					case "PATCH":
1164 						o1 = 4;
1165 						break;
1166 					case "GET":
1167 						o1 = 5;
1168 						break;
1169 					default:
1170 						o1 = 0;
1171 						break;
1172 				}
1173 			}
1174 			return o1;
1175 		}
1176 
1177 	}
1178 
1179 	private static class BaseServerResponseExceptionHolder {
1180 		private BaseServerResponseException myException;
1181 
1182 		public BaseServerResponseException getException() {
1183 			return myException;
1184 		}
1185 
1186 		public void setException(BaseServerResponseException myException) {
1187 			this.myException = myException;
1188 		}
1189 	}
1190 
1191 	public static boolean isPlaceholder(IIdType theId) {
1192 		if (theId != null && theId.getValue() != null) {
1193 			return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:");
1194 		}
1195 		return false;
1196 	}
1197 
1198 	private static String toStatusString(int theStatusCode) {
1199 		return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
1200 	}
1201 
1202 	private String toMatchUrl(BUNDLEENTRY theEntry) {
1203 		String verb = myVersionAdapter.getEntryRequestVerb(theEntry);
1204 		if (verb.equals("POST")) {
1205 			return myVersionAdapter.getEntryIfNoneExist(theEntry);
1206 		}
1207 		if (verb.equals("PATCH")) {
1208 			return myVersionAdapter.getEntryRequestIfMatch(theEntry);
1209 		}
1210 		if (verb.equals("PUT") || verb.equals("DELETE")) {
1211 			String url = extractTransactionUrlOrThrowException(theEntry, verb);
1212 			UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
1213 			if (isBlank(parts.getResourceId())) {
1214 				return parts.getResourceType() + '?' + parts.getParams();
1215 			}
1216 		}
1217 		return null;
1218 	}
1219 
1220 }