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