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