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