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