View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   /*
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2018 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.context.RuntimeResourceDefinition;
24  import ca.uhn.fhir.jpa.entity.ResourceTable;
25  import ca.uhn.fhir.jpa.entity.TagDefinition;
26  import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
27  import ca.uhn.fhir.jpa.util.DeleteConflict;
28  import ca.uhn.fhir.model.api.IResource;
29  import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
30  import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
31  import ca.uhn.fhir.model.dstu2.composite.MetaDt;
32  import ca.uhn.fhir.model.dstu2.resource.Bundle;
33  import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry;
34  import ca.uhn.fhir.model.dstu2.resource.Bundle.EntryResponse;
35  import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
36  import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum;
37  import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum;
38  import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum;
39  import ca.uhn.fhir.model.primitive.IdDt;
40  import ca.uhn.fhir.model.primitive.InstantDt;
41  import ca.uhn.fhir.model.primitive.UriDt;
42  import ca.uhn.fhir.parser.DataFormatException;
43  import ca.uhn.fhir.parser.IParser;
44  import ca.uhn.fhir.rest.api.Constants;
45  import ca.uhn.fhir.rest.api.RequestTypeEnum;
46  import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
47  import ca.uhn.fhir.rest.api.server.RequestDetails;
48  import ca.uhn.fhir.rest.server.RestfulServerUtils;
49  import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
50  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
51  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
52  import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
53  import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
54  import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
55  import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
56  import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
57  import ca.uhn.fhir.util.FhirTerser;
58  import ca.uhn.fhir.util.UrlUtil;
59  import ca.uhn.fhir.util.UrlUtil.UrlParts;
60  import com.google.common.collect.ArrayListMultimap;
61  import org.apache.http.NameValuePair;
62  import org.hl7.fhir.instance.model.api.IBaseResource;
63  import org.hl7.fhir.instance.model.api.IIdType;
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.TransactionCallbackWithoutResult;
72  import org.springframework.transaction.support.TransactionTemplate;
73  
74  import javax.persistence.TypedQuery;
75  import java.util.*;
76  
77  import static org.apache.commons.lang3.StringUtils.*;
78  
79  public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
80  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu2.class);
81  
82  	@Autowired
83  	private PlatformTransactionManager myTxManager;
84  
85  	private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) {
86  		ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size());
87  		long start = System.currentTimeMillis();
88  
89  		TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
90  		txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
91  
92  		Bundle resp = new Bundle();
93  		resp.setType(BundleTypeEnum.BATCH_RESPONSE);
94  
95  		/*
96  		 * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others
97  		 */
98  
99  		for (final Entry nextRequestEntry : theRequest.getEntry()) {
100 
101 			TransactionCallback<Bundle> callback = new TransactionCallback<Bundle>() {
102 				@Override
103 				public Bundle doInTransaction(TransactionStatus theStatus) {
104 					Bundle subRequestBundle = new Bundle();
105 					subRequestBundle.setType(BundleTypeEnum.TRANSACTION);
106 					subRequestBundle.addEntry(nextRequestEntry);
107 					return transaction((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request");
108 				}
109 			};
110 
111 			BaseServerResponseException caughtEx;
112 			try {
113 				Bundle nextResponseBundle;
114 				if (nextRequestEntry.getRequest().getMethodElement().getValueAsEnum() == HTTPVerbEnum.GET) {
115 					// Don't process GETs in a transaction because they'll
116 					// create their own
117 					nextResponseBundle = callback.doInTransaction(null);
118 				} else {
119 					nextResponseBundle = txTemplate.execute(callback);
120 				}
121 				caughtEx = null;
122 
123 				Entry subResponseEntry = nextResponseBundle.getEntry().get(0);
124 				resp.addEntry(subResponseEntry);
125 				/*
126 				 * 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
127 				 */
128 				if (subResponseEntry.getResource() == null) {
129 					subResponseEntry.setResource(nextResponseBundle.getEntry().get(0).getResource());
130 				}
131 
132 			} catch (BaseServerResponseException e) {
133 				caughtEx = e;
134 			} catch (Throwable t) {
135 				ourLog.error("Failure during BATCH sub transaction processing", t);
136 				caughtEx = new InternalErrorException(t);
137 			}
138 
139 			if (caughtEx != null) {
140 				Entry nextEntry = resp.addEntry();
141 
142 				OperationOutcome oo = new OperationOutcome();
143 				oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setDiagnostics(caughtEx.getMessage());
144 				nextEntry.setResource(oo);
145 
146 				EntryResponse nextEntryResp = nextEntry.getResponse();
147 				nextEntryResp.setStatus(toStatusString(caughtEx.getStatusCode()));
148 			}
149 
150 		}
151 
152 		long delay = System.currentTimeMillis() - start;
153 		ourLog.info("Batch completed in {}ms", new Object[] {delay});
154 
155 		return resp;
156 	}
157 
158 	@SuppressWarnings("unchecked")
159 	private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
160 		BundleTypeEnum transactionType = theRequest.getTypeElement().getValueAsEnum();
161 		if (transactionType == BundleTypeEnum.BATCH) {
162 			return batch(theRequestDetails, theRequest);
163 		}
164 
165 		return doTransaction(theRequestDetails, theRequest, theActionName, transactionType);
166 	}
167 
168 	private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName, BundleTypeEnum theTransactionType) {
169 		if (theTransactionType == null) {
170 			String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + BundleTypeEnum.TRANSACTION.getCode();
171 			ourLog.warn(message);
172 			theTransactionType = BundleTypeEnum.TRANSACTION;
173 		}
174 		if (theTransactionType != BundleTypeEnum.TRANSACTION) {
175 			throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + theTransactionType.getCode());
176 		}
177 
178 		ourLog.info("Beginning {} with {} resources", theActionName, theRequest.getEntry().size());
179 
180 		long start = System.currentTimeMillis();
181 		Date updateTime = new Date();
182 
183 		Set<IdDt> allIds = new LinkedHashSet<IdDt>();
184 		Map<IdDt, IdDt> idSubstitutions = new HashMap<IdDt, IdDt>();
185 		Map<IdDt, DaoMethodOutcome> idToPersistedOutcome = new HashMap<IdDt, DaoMethodOutcome>();
186 
187 		/*
188 		 * We want to execute the transaction request bundle elements in the order
189 		 * specified by the FHIR specification (see TransactionSorter) so we save the
190 		 * original order in the request, then sort it.
191 		 *
192 		 * Entries with a type of GET are removed from the bundle so that they
193 		 * can be processed at the very end. We do this because the incoming resources
194 		 * are saved in a two-phase way in order to deal with interdependencies, and
195 		 * we want the GET processing to use the final indexing state
196 		 */
197 		Bundle response = new Bundle();
198 		List<Entry> getEntries = new ArrayList<Entry>();
199 		IdentityHashMap<Entry, Integer> originalRequestOrder = new IdentityHashMap<Entry, Integer>();
200 		for (int i = 0; i < theRequest.getEntry().size(); i++) {
201 			originalRequestOrder.put(theRequest.getEntry().get(i), i);
202 			response.addEntry();
203 			if (theRequest.getEntry().get(i).getRequest().getMethodElement().getValueAsEnum() == HTTPVerbEnum.GET) {
204 				getEntries.add(theRequest.getEntry().get(i));
205 			}
206 		}
207 		Collections.sort(theRequest.getEntry(), new TransactionSorter());
208 
209 		List<IIdType> deletedResources = new ArrayList<>();
210 		List<DeleteConflict> deleteConflicts = new ArrayList<>();
211 		Map<Entry, ResourceTable> entriesToProcess = new IdentityHashMap<>();
212 		Set<ResourceTable> nonUpdatedEntities = new HashSet<ResourceTable>();
213 		Set<ResourceTable> updatedEntities = new HashSet<>();
214 
215 		/*
216 		 * Handle: GET/PUT/POST
217 		 */
218 		TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
219 		txTemplate.execute(t->{
220 			handleTransactionWriteOperations(theRequestDetails, theRequest, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, deletedResources, deleteConflicts, entriesToProcess, nonUpdatedEntities, updatedEntities);
221 			return null;
222 		});
223 
224 		/*
225 		 * Loop through the request and process any entries of type GET
226 		 */
227 		for (int i = 0; i < getEntries.size(); i++) {
228 			Entry nextReqEntry = getEntries.get(i);
229 			Integer originalOrder = originalRequestOrder.get(nextReqEntry);
230 			Entry nextRespEntry = response.getEntry().get(originalOrder);
231 
232 			ServletSubRequestDetails requestDetails = new ServletSubRequestDetails();
233 			requestDetails.setServletRequest(theRequestDetails.getServletRequest());
234 			requestDetails.setRequestType(RequestTypeEnum.GET);
235 			requestDetails.setServer(theRequestDetails.getServer());
236 
237 			String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerbEnum.GET);
238 
239 			int qIndex = url.indexOf('?');
240 			ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
241 			requestDetails.setParameters(new HashMap<String, String[]>());
242 			if (qIndex != -1) {
243 				String params = url.substring(qIndex);
244 				List<NameValuePair> parameters = translateMatchUrl(params);
245 				for (NameValuePair next : parameters) {
246 					paramValues.put(next.getName(), next.getValue());
247 				}
248 				for (Map.Entry<String, Collection<String>> nextParamEntry : paramValues.asMap().entrySet()) {
249 					String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]);
250 					requestDetails.addParameter(nextParamEntry.getKey(), nextValue);
251 				}
252 				url = url.substring(0, qIndex);
253 			}
254 
255 			requestDetails.setRequestPath(url);
256 			requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase());
257 
258 			theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url);
259 			BaseMethodBinding<?> method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url);
260 			if (method == null) {
261 				throw new IllegalArgumentException("Unable to handle GET " + url);
262 			}
263 
264 			if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) {
265 				requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch());
266 			}
267 			if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) {
268 				requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist());
269 			}
270 			if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) {
271 				requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch());
272 			}
273 
274 			if (method instanceof BaseResourceReturningMethodBinding) {
275 				try {
276 					IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails);
277 					if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
278 						resource = filterNestedBundle(requestDetails, resource);
279 					}
280 					nextRespEntry.setResource((IResource) resource);
281 					nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
282 				} catch (NotModifiedException e) {
283 					nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
284 				}
285 			} else {
286 				throw new IllegalArgumentException("Unable to handle GET " + url);
287 			}
288 
289 		}
290 
291 		for (Map.Entry<Entry, ResourceTable> nextEntry : entriesToProcess.entrySet()) {
292 			nextEntry.getKey().getResponse().setLocation(nextEntry.getValue().getIdDt().toUnqualified().getValue());
293 			nextEntry.getKey().getResponse().setEtag(nextEntry.getValue().getIdDt().getVersionIdPart());
294 		}
295 
296 		long delay = System.currentTimeMillis() - start;
297 		int numEntries = theRequest.getEntry().size();
298 		long delayPer = delay / numEntries;
299 		ourLog.info("{} completed in {}ms ({} entries at {}ms per entry)", new Object[] {theActionName, delay, numEntries, delayPer});
300 
301 		response.setType(BundleTypeEnum.TRANSACTION_RESPONSE);
302 		return response;
303 	}
304 
305 	private void handleTransactionWriteOperations(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName, Date theUpdateTime, Set<IdDt> theAllIds, Map<IdDt, IdDt> theIdSubstitutions, Map<IdDt, DaoMethodOutcome> theIdToPersistedOutcome, Bundle theResponse, IdentityHashMap<Entry, Integer> theOriginalRequestOrder, List<IIdType> theDeletedResources, List<DeleteConflict> theDeleteConflicts, Map<Entry, ResourceTable> theEntriesToProcess, Set<ResourceTable> theNonUpdatedEntities, Set<ResourceTable> theUpdatedEntities) {
306 		/*
307 		 * Loop through the request and process any entries of type
308 		 * PUT, POST or DELETE
309 		 */
310 		for (int i = 0; i < theRequest.getEntry().size(); i++) {
311 
312 			if (i % 100 == 0) {
313 				ourLog.debug("Processed {} non-GET entries out of {}", i, theRequest.getEntry().size());
314 			}
315 
316 			Entry nextReqEntry = theRequest.getEntry().get(i);
317 			IResource res = nextReqEntry.getResource();
318 			IdDt nextResourceId = null;
319 			if (res != null) {
320 
321 				nextResourceId = res.getId();
322 
323 				if (!nextResourceId.hasIdPart()) {
324 					if (isNotBlank(nextReqEntry.getFullUrl())) {
325 						nextResourceId = new IdDt(nextReqEntry.getFullUrl());
326 					}
327 				}
328 
329 				if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) {
330 					throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
331 				}
332 
333 				if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
334 					nextResourceId = new IdDt(toResourceName(res.getClass()), nextResourceId.getIdPart());
335 					res.setId(nextResourceId);
336 				}
337 
338 				/*
339 				 * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
340 				 */
341 				if (isPlaceholder(nextResourceId)) {
342 					if (!theAllIds.add(nextResourceId)) {
343 						throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
344 					}
345 				} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
346 					IdDt nextId = nextResourceId.toUnqualifiedVersionless();
347 					if (!theAllIds.add(nextId)) {
348 						throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
349 					}
350 				}
351 
352 			}
353 
354 			HTTPVerbEnum verb = nextReqEntry.getRequest().getMethodElement().getValueAsEnum();
355 			if (verb == null) {
356 				throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextReqEntry.getRequest().getMethod()));
357 			}
358 
359 			String resourceType = res != null ? getContext().getResourceDefinition(res).getName() : null;
360 			Entry nextRespEntry = theResponse.getEntry().get(theOriginalRequestOrder.get(nextReqEntry));
361 
362 			switch (verb) {
363 				case POST: {
364 					// CREATE
365 					@SuppressWarnings("rawtypes")
366 					IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
367 					res.setId((String) null);
368 					DaoMethodOutcome outcome;
369 					outcome = resourceDao.create(res, nextReqEntry.getRequest().getIfNoneExist(), false, theRequestDetails);
370 					handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res);
371 					theEntriesToProcess.put(nextRespEntry, outcome.getEntity());
372 					if (outcome.getCreated() == false) {
373 						theNonUpdatedEntities.add(outcome.getEntity());
374 					}
375 					break;
376 				}
377 				case DELETE: {
378 					// DELETE
379 					String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
380 					UrlParts parts = UrlUtil.parseUrl(url);
381 					IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb.getCode(), url);
382 					int status = Constants.STATUS_HTTP_204_NO_CONTENT;
383 					if (parts.getResourceId() != null) {
384 						DaoMethodOutcome outcome = dao.delete(new IdDt(parts.getResourceType(), parts.getResourceId()), theDeleteConflicts, theRequestDetails);
385 						if (outcome.getEntity() != null) {
386 							theDeletedResources.add(outcome.getId().toUnqualifiedVersionless());
387 							theEntriesToProcess.put(nextRespEntry, outcome.getEntity());
388 						}
389 					} else {
390 						DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), theDeleteConflicts, theRequestDetails);
391 						List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
392 						for (ResourceTable deleted : allDeleted) {
393 							theDeletedResources.add(deleted.getIdDt().toUnqualifiedVersionless());
394 						}
395 						if (allDeleted.isEmpty()) {
396 							status = Constants.STATUS_HTTP_404_NOT_FOUND;
397 						}
398 					}
399 
400 					nextRespEntry.getResponse().setStatus(toStatusString(status));
401 					break;
402 				}
403 				case PUT: {
404 					// UPDATE
405 					@SuppressWarnings("rawtypes")
406 					IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
407 
408 					DaoMethodOutcome outcome;
409 
410 					String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
411 
412 					UrlParts parts = UrlUtil.parseUrl(url);
413 					if (isNotBlank(parts.getResourceId())) {
414 						res.setId(new IdDt(parts.getResourceType(), parts.getResourceId()));
415 						outcome = resourceDao.update(res, null, false, theRequestDetails);
416 					} else {
417 						res.setId((String) null);
418 						outcome = resourceDao.update(res, parts.getResourceType() + '?' + parts.getParams(), false, theRequestDetails);
419 					}
420 
421 					if (outcome.getCreated() == Boolean.FALSE) {
422 						theUpdatedEntities.add(outcome.getEntity());
423 					}
424 
425 					handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res);
426 					theEntriesToProcess.put(nextRespEntry, outcome.getEntity());
427 					break;
428 				}
429 				case GET:
430 					break;
431 			}
432 		}
433 
434 		/*
435 		 * Make sure that there are no conflicts from deletions. E.g. we can't delete something
436 		 * if something else has a reference to it.. Unless the thing that has a reference to it
437 		 * was also deleted as a part of this transaction, which is why we check this now at the
438 		 * end.
439 		 */
440 
441 		theDeleteConflicts.removeIf(next -> theDeletedResources.contains(next.getTargetId().toVersionless()));
442 		validateDeleteConflictsEmptyOrThrowException(theDeleteConflicts);
443 
444 		/*
445 		 * Perform ID substitutions and then index each resource we have saved
446 		 */
447 
448 		FhirTerser terser = getContext().newTerser();
449 		for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
450 			IResource nextResource = (IResource) nextOutcome.getResource();
451 			if (nextResource == null) {
452 				continue;
453 			}
454 
455 			// References
456 			List<BaseResourceReferenceDt> allRefs = terser.getAllPopulatedChildElementsOfType(nextResource, BaseResourceReferenceDt.class);
457 			for (BaseResourceReferenceDt nextRef : allRefs) {
458 				IdDt nextId = nextRef.getReference();
459 				if (!nextId.hasIdPart()) {
460 					continue;
461 				}
462 				if (theIdSubstitutions.containsKey(nextId)) {
463 					IdDt newId = theIdSubstitutions.get(nextId);
464 					ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
465 					nextRef.setReference(newId);
466 				} else {
467 					ourLog.debug(" * Reference [{}] does not exist in bundle", nextId);
468 				}
469 			}
470 
471 			// URIs
472 			List<UriDt> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, UriDt.class);
473 			for (UriDt nextRef : allUris) {
474 				if (nextRef instanceof IIdType) {
475 					continue; // No substitution on the resource ID itself!
476 				}
477 				IdDt nextUriString = new IdDt(nextRef.getValueAsString());
478 				if (theIdSubstitutions.containsKey(nextUriString)) {
479 					IdDt newId = theIdSubstitutions.get(nextUriString);
480 					ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
481 					nextRef.setValue(newId.getValue());
482 				} else {
483 					ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString);
484 				}
485 			}
486 
487 
488 			InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(nextResource);
489 			Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
490 			if (theUpdatedEntities.contains(nextOutcome.getEntity())) {
491 				updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource());
492 			} else if (!theNonUpdatedEntities.contains(nextOutcome.getEntity())) {
493 				updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true);
494 			}
495 		}
496 
497 		myEntityManager.flush();
498 
499 		/*
500 		 * Double check we didn't allow any duplicates we shouldn't have
501 		 */
502 		for (Entry nextEntry : theRequest.getEntry()) {
503 			if (nextEntry.getRequest().getMethodElement().getValueAsEnum() == HTTPVerbEnum.POST) {
504 				String matchUrl = nextEntry.getRequest().getIfNoneExist();
505 				if (isNotBlank(matchUrl)) {
506 					IFhirResourceDao<?> resourceDao = getDao(nextEntry.getResource().getClass());
507 					Set<Long> val = resourceDao.processMatchUrl(matchUrl);
508 					if (val.size() > 1) {
509 						throw new InvalidRequestException(
510 							"Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");
511 					}
512 				}
513 			}
514 		}
515 
516 		for (IdDt next : theAllIds) {
517 			IdDt replacement = theIdSubstitutions.get(next);
518 			if (replacement == null) {
519 				continue;
520 			}
521 			if (replacement.equals(next)) {
522 				continue;
523 			}
524 			ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
525 		}
526 	}
527 
528 	private String extractTransactionUrlOrThrowException(Entry nextEntry, HTTPVerbEnum verb) {
529 		String url = nextEntry.getRequest().getUrl();
530 		if (isBlank(url)) {
531 			throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
532 		}
533 		return url;
534 	}
535 
536 	/**
537 	 * This method is called for nested bundles (e.g. if we received a transaction with an entry that
538 	 * was a GET search, this method is called on the bundle for the search result, that will be placed in the
539 	 * outer bundle). This method applies the _summary and _content parameters to the output of
540 	 * that bundle.
541 	 * <p>
542 	 * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
543 	 */
544 	private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
545 		IParser p = getContext().newJsonParser();
546 		RestfulServerUtils.configureResponseParser(theRequestDetails, p);
547 		return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
548 	}
549 
550 	@Override
551 	public MetaDt metaGetOperation(RequestDetails theRequestDetails) {
552 		// Notify interceptors
553 		ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
554 		notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
555 
556 		String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t)";
557 		TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
558 		List<TagDefinition> tagDefinitions = q.getResultList();
559 
560 		MetaDt retVal = toMetaDt(tagDefinitions);
561 
562 		return retVal;
563 	}
564 
565 	private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
566 		RuntimeResourceDefinition resType;
567 		try {
568 			resType = getContext().getResourceDefinition(theParts.getResourceType());
569 		} catch (DataFormatException e) {
570 			String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
571 			throw new InvalidRequestException(msg);
572 		}
573 		IFhirResourceDao<? extends IBaseResource> dao = null;
574 		if (resType != null) {
575 			dao = getDao(resType.getImplementingClass());
576 		}
577 		if (dao == null) {
578 			String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
579 			throw new InvalidRequestException(msg);
580 		}
581 
582 		// if (theParts.getResourceId() == null && theParts.getParams() == null) {
583 		// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
584 		// throw new InvalidRequestException(msg);
585 		// }
586 
587 		return dao;
588 	}
589 
590 	protected MetaDt toMetaDt(Collection<TagDefinition> tagDefinitions) {
591 		MetaDt retVal = new MetaDt();
592 		for (TagDefinition next : tagDefinitions) {
593 			switch (next.getTagType()) {
594 				case PROFILE:
595 					retVal.addProfile(next.getCode());
596 					break;
597 				case SECURITY_LABEL:
598 					retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
599 					break;
600 				case TAG:
601 					retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
602 					break;
603 			}
604 		}
605 		return retVal;
606 	}
607 
608 	@Transactional(propagation = Propagation.NEVER)
609 	@Override
610 	public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
611 		if (theRequestDetails != null) {
612 			ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
613 			notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
614 		}
615 
616 		String actionName = "Transaction";
617 		return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
618 	}
619 
620 	private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
621 		super.markRequestAsProcessingSubRequest(theRequestDetails);
622 		try {
623 			return doTransaction(theRequestDetails, theRequest, theActionName);
624 		} finally {
625 			super.clearRequestAsProcessingSubRequest(theRequestDetails);
626 		}
627 	}
628 
629 	private static void handleTransactionCreateOrUpdateOutcome(Map<IdDt, IdDt> idSubstitutions, Map<IdDt, DaoMethodOutcome> idToPersistedOutcome, IdDt nextResourceId, DaoMethodOutcome outcome,
630 																				  Entry newEntry, String theResourceType, IResource theRes) {
631 		IdDt newId = (IdDt) outcome.getId().toUnqualifiedVersionless();
632 		IdDt resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
633 		if (newId.equals(resourceId) == false) {
634 			idSubstitutions.put(resourceId, newId);
635 			if (isPlaceholder(resourceId)) {
636 				/*
637 				 * 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.
638 				 */
639 				idSubstitutions.put(new IdDt(theResourceType + '/' + resourceId.getValue()), newId);
640 			}
641 		}
642 		idToPersistedOutcome.put(newId, outcome);
643 		if (outcome.getCreated().booleanValue()) {
644 			newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_201_CREATED));
645 		} else {
646 			newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
647 		}
648 		newEntry.getResponse().setLastModified(ResourceMetadataKeyEnum.UPDATED.get(theRes));
649 	}
650 
651 	private static boolean isPlaceholder(IdDt theId) {
652 		if (theId.getValue() != null) {
653 			if (theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:")) {
654 				return true;
655 			}
656 		}
657 		return false;
658 	}
659 
660 	private static String toStatusString(int theStatusCode) {
661 		return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
662 	}
663 
664 	//@formatter:off
665 
666 	/**
667 	 * Transaction Order, per the spec:
668 	 * <p>
669 	 * Process any DELETE interactions
670 	 * Process any POST interactions
671 	 * Process any PUT interactions
672 	 * Process any GET interactions
673 	 */
674 	//@formatter:off
675 	public class TransactionSorter implements Comparator<Entry> {
676 
677 		@Override
678 		public int compare(Entry theO1, Entry theO2) {
679 			int o1 = toOrder(theO1);
680 			int o2 = toOrder(theO2);
681 
682 			return o1 - o2;
683 		}
684 
685 		private int toOrder(Entry theO1) {
686 			int o1 = 0;
687 			if (theO1.getRequest().getMethodElement().getValueAsEnum() != null) {
688 				switch (theO1.getRequest().getMethodElement().getValueAsEnum()) {
689 					case DELETE:
690 						o1 = 1;
691 						break;
692 					case POST:
693 						o1 = 2;
694 						break;
695 					case PUT:
696 						o1 = 3;
697 						break;
698 					case GET:
699 						o1 = 4;
700 						break;
701 				}
702 			}
703 			return o1;
704 		}
705 
706 	}
707 
708 }