@SuppressWarnings("unchecked") private Bundle transaction( ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { BundleType transactionType = theRequest.getTypeElement().getValue(); if (transactionType == BundleType.BATCH) { return batch(theRequestDetails, theRequest); } if (transactionType == null) { String message = "Transactiion Bundle did not specify valid Bundle.type, assuming " + BundleType.TRANSACTION.toCode(); ourLog.warn(message); transactionType = BundleType.TRANSACTION; } if (transactionType != BundleType.TRANSACTION) { throw new InvalidRequestException( "Unable to process transaction where incoming Bundle.type = " + transactionType.toCode()); } ourLog.info("Beginning {} with {} resources", theActionName, theRequest.getEntry().size()); long start = System.currentTimeMillis(); Date updateTime = new Date(); Set<IdType> allIds = new LinkedHashSet<IdType>(); Map<IdType, IdType> idSubstitutions = new HashMap<IdType, IdType>(); Map<IdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<IdType, DaoMethodOutcome>(); // Do all entries have a verb? for (int i = 0; i < theRequest.getEntry().size(); i++) { BundleEntryComponent nextReqEntry = theRequest.getEntry().get(i); HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue(); if (verb == null) { throw new InvalidRequestException( getContext() .getLocalizer() .getMessage( BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextReqEntry.getRequest().getMethod(), i)); } } /* * We want to execute the transaction request bundle elements in the order * specified by the FHIR specification (see TransactionSorter) so we save the * original order in the request, then sort it. * * Entries with a type of GET are removed from the bundle so that they * can be processed at the very end. We do this because the incoming resources * are saved in a two-phase way in order to deal with interdependencies, and * we want the GET processing to use the final indexing state */ Bundle response = new Bundle(); List<BundleEntryComponent> getEntries = new ArrayList<BundleEntryComponent>(); IdentityHashMap<BundleEntryComponent, Integer> originalRequestOrder = new IdentityHashMap<Bundle.BundleEntryComponent, Integer>(); for (int i = 0; i < theRequest.getEntry().size(); i++) { originalRequestOrder.put(theRequest.getEntry().get(i), i); response.addEntry(); if (theRequest.getEntry().get(i).getRequest().getMethodElement().getValue() == HTTPVerb.GET) { getEntries.add(theRequest.getEntry().get(i)); } } Collections.sort(theRequest.getEntry(), new TransactionSorter()); List<IIdType> deletedResources = new ArrayList<IIdType>(); List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>(); /* * Loop through the request and process any entries of type * PUT, POST or DELETE */ for (int i = 0; i < theRequest.getEntry().size(); i++) { if (i % 100 == 0) { ourLog.info("Processed {} non-GET entries out of {}", i, theRequest.getEntry().size()); } BundleEntryComponent nextReqEntry = theRequest.getEntry().get(i); Resource res = nextReqEntry.getResource(); IdType nextResourceId = null; if (res != null) { nextResourceId = res.getIdElement(); if (nextResourceId.hasIdPart() == false) { if (isNotBlank(nextReqEntry.getFullUrl())) { nextResourceId = new IdType(nextReqEntry.getFullUrl()); } } if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { throw new InvalidRequestException( "Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); } if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { nextResourceId = new IdType(toResourceName(res.getClass()), nextResourceId.getIdPart()); res.setId(nextResourceId); } /* * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness */ if (isPlaceholder(nextResourceId)) { if (!allIds.add(nextResourceId)) { throw new InvalidRequestException( getContext() .getLocalizer() .getMessage( BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId)); } } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) { IdType nextId = nextResourceId.toUnqualifiedVersionless(); if (!allIds.add(nextId)) { throw new InvalidRequestException( getContext() .getLocalizer() .getMessage( BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId)); } } } HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue(); String resourceType = res != null ? getContext().getResourceDefinition(res).getName() : null; BundleEntryComponent nextRespEntry = response.getEntry().get(originalRequestOrder.get(nextReqEntry)); switch (verb) { case POST: { // CREATE @SuppressWarnings("rawtypes") IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); res.setId((String) null); DaoMethodOutcome outcome; outcome = resourceDao.create(res, nextReqEntry.getRequest().getIfNoneExist(), false); handleTransactionCreateOrUpdateOutcome( idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res); break; } case DELETE: { // DELETE String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); UrlParts parts = UrlUtil.parseUrl(url); ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb.toCode(), url); int status = Constants.STATUS_HTTP_204_NO_CONTENT; if (parts.getResourceId() != null) { ResourceTable deleted = dao.delete( new IdType(parts.getResourceType(), parts.getResourceId()), deleteConflicts); if (deleted != null) { deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless()); } } else { List<ResourceTable> allDeleted = dao.deleteByUrl( parts.getResourceType() + '?' + parts.getParams(), deleteConflicts); for (ResourceTable deleted : allDeleted) { deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless()); } if (allDeleted.isEmpty()) { status = Constants.STATUS_HTTP_404_NOT_FOUND; } } nextRespEntry.getResponse().setStatus(toStatusString(status)); break; } case PUT: { // UPDATE @SuppressWarnings("rawtypes") IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); DaoMethodOutcome outcome; String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); UrlParts parts = UrlUtil.parseUrl(url); if (isNotBlank(parts.getResourceId())) { res.setId(new IdType(parts.getResourceType(), parts.getResourceId())); outcome = resourceDao.update(res, null, false); } else { res.setId((String) null); outcome = resourceDao.update(res, parts.getResourceType() + '?' + parts.getParams(), false); } handleTransactionCreateOrUpdateOutcome( idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res); break; } } } /* * Make sure that there are no conflicts from deletions. E.g. we can't delete something * if something else has a reference to it.. Unless the thing that has a reference to it * was also deleted as a part of this transaction, which is why we check this now at the * end. */ for (Iterator<DeleteConflict> iter = deleteConflicts.iterator(); iter.hasNext(); ) { DeleteConflict next = iter.next(); if (deletedResources.contains(next.getTargetId().toVersionless())) { iter.remove(); } } validateDeleteConflictsEmptyOrThrowException(deleteConflicts); /* * Perform ID substitutions and then index each resource we have saved */ FhirTerser terser = getContext().newTerser(); for (DaoMethodOutcome nextOutcome : idToPersistedOutcome.values()) { IBaseResource nextResource = (IBaseResource) nextOutcome.getResource(); if (nextResource == null) { continue; } List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class); for (IBaseReference nextRef : allRefs) { IIdType nextId = nextRef.getReferenceElement(); if (idSubstitutions.containsKey(nextId)) { IdType newId = idSubstitutions.get(nextId); ourLog.info(" * Replacing resource ref {} with {}", nextId, newId); nextRef.setReference(newId.getValue()); } else { ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); } } IPrimitiveType<Date> deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; updateEntity( nextResource, nextOutcome.getEntity(), false, deletedTimestampOrNull, true, false, updateTime); } myEntityManager.flush(); /* * Double check we didn't allow any duplicates we shouldn't have */ for (BundleEntryComponent nextEntry : theRequest.getEntry()) { if (nextEntry.getRequest().getMethodElement().getValue() == HTTPVerb.POST) { String matchUrl = nextEntry.getRequest().getIfNoneExist(); if (isNotBlank(matchUrl)) { IFhirResourceDao<?> resourceDao = getDao(nextEntry.getResource().getClass()); Set<Long> val = resourceDao.processMatchUrl(matchUrl); if (val.size() > 1) { throw new InvalidRequestException( "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); } } } } for (IdType next : allIds) { IdType replacement = idSubstitutions.get(next); if (replacement == null) { continue; } if (replacement.equals(next)) { continue; } ourLog.info( "Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); } /* * Loop through the request and process any entries of type GET */ for (int i = 0; i < getEntries.size(); i++) { BundleEntryComponent nextReqEntry = getEntries.get(i); Integer originalOrder = originalRequestOrder.get(nextReqEntry); BundleEntryComponent nextRespEntry = response.getEntry().get(originalOrder); ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); requestDetails.setServletRequest(theRequestDetails.getServletRequest()); requestDetails.setRequestType(RequestTypeEnum.GET); requestDetails.setServer(theRequestDetails.getServer()); String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerb.GET); int qIndex = url.indexOf('?'); ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create(); requestDetails.setParameters(new HashMap<String, String[]>()); if (qIndex != -1) { String params = url.substring(qIndex); List<NameValuePair> parameters = translateMatchUrl(params); for (NameValuePair next : parameters) { paramValues.put(next.getName(), next.getValue()); } for (java.util.Map.Entry<String, Collection<String>> nextParamEntry : paramValues.asMap().entrySet()) { String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]); requestDetails.getParameters().put(nextParamEntry.getKey(), nextValue); } url = url.substring(0, qIndex); } requestDetails.setRequestPath(url); requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase()); theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url); BaseMethodBinding<?> method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url); if (method == null) { throw new IllegalArgumentException("Unable to handle GET " + url); } if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) { requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch()); } if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) { requestDetails.addHeader( Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist()); } if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) { requestDetails.addHeader( Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch()); } if (method instanceof BaseResourceReturningMethodBinding) { try { ResourceOrDstu1Bundle responseData = ((BaseResourceReturningMethodBinding) method) .invokeServer(theRequestDetails.getServer(), requestDetails, new byte[0]); IBaseResource resource = responseData.getResource(); if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) { resource = filterNestedBundle(requestDetails, resource); } nextRespEntry.setResource((Resource) resource); nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); } catch (NotModifiedException e) { nextRespEntry .getResponse() .setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED)); } } else { throw new IllegalArgumentException("Unable to handle GET " + url); } } long delay = System.currentTimeMillis() - start; ourLog.info(theActionName + " completed in {}ms", new Object[] {delay}); response.setType(BundleType.TRANSACTIONRESPONSE); return response; }
@Transactional(propagation = Propagation.REQUIRED) @Override public List<IResource> transaction( RequestDetails theRequestDetails, List<IResource> theResources) { ourLog.info("Beginning transaction with {} resources", theResources.size()); // Notify interceptors ActionRequestDetails requestDetails = new ActionRequestDetails(null, null); notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); long start = System.currentTimeMillis(); Set<IdDt> allIds = new HashSet<IdDt>(); for (int i = 0; i < theResources.size(); i++) { IResource res = theResources.get(i); if (res.getId().hasIdPart() && !res.getId().hasResourceType() && !isPlaceholder(res.getId())) { res.setId(new IdDt(toResourceName(res.getClass()), res.getId().getIdPart())); } /* * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness */ if (isPlaceholder(res.getId())) { if (!allIds.add(res.getId())) { throw new InvalidRequestException( "Transaction bundle contains multiple resources with ID: " + res.getId()); } } else if (res.getId().hasResourceType() && res.getId().hasIdPart()) { IdDt nextId = res.getId().toUnqualifiedVersionless(); if (!allIds.add(nextId)) { throw new InvalidRequestException( "Transaction bundle contains multiple resources with ID: " + nextId); } } } FhirTerser terser = getContext().newTerser(); int creations = 0; int updates = 0; Map<IdDt, IdDt> idConversions = new HashMap<IdDt, IdDt>(); List<ResourceTable> persistedResources = new ArrayList<ResourceTable>(); List<IResource> retVal = new ArrayList<IResource>(); OperationOutcome oo = new OperationOutcome(); retVal.add(oo); Date updateTime = new Date(); for (int resourceIdx = 0; resourceIdx < theResources.size(); resourceIdx++) { IResource nextResource = theResources.get(resourceIdx); IdDt nextId = nextResource.getId(); if (nextId == null) { nextId = new IdDt(); } String resourceName = toResourceName(nextResource); BundleEntryTransactionMethodEnum nextResouceOperationIn = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(nextResource); if (nextResouceOperationIn == null && hasValue(ResourceMetadataKeyEnum.DELETED_AT.get(nextResource))) { nextResouceOperationIn = BundleEntryTransactionMethodEnum.DELETE; } String matchUrl = ResourceMetadataKeyEnum.LINK_SEARCH.get(nextResource); Set<Long> candidateMatches = null; if (StringUtils.isNotBlank(matchUrl)) { candidateMatches = processMatchUrl(matchUrl, nextResource.getClass()); } ResourceTable entity; if (nextResouceOperationIn == BundleEntryTransactionMethodEnum.POST) { entity = null; } else if (nextResouceOperationIn == BundleEntryTransactionMethodEnum.PUT || nextResouceOperationIn == BundleEntryTransactionMethodEnum.DELETE) { if (candidateMatches == null || candidateMatches.size() == 0) { if (nextId == null || StringUtils.isBlank(nextId.getIdPart())) { throw new InvalidRequestException( getContext() .getLocalizer() .getMessage( BaseHapiFhirSystemDao.class, "transactionOperationFailedNoId", nextResouceOperationIn.name())); } entity = tryToLoadEntity(nextId); if (entity == null) { if (nextResouceOperationIn == BundleEntryTransactionMethodEnum.PUT) { ourLog.debug( "Attempting to UPDATE resource with unknown ID '{}', will CREATE instead", nextId); } else if (candidateMatches == null) { throw new InvalidRequestException( getContext() .getLocalizer() .getMessage( BaseHapiFhirSystemDao.class, "transactionOperationFailedUnknownId", nextResouceOperationIn.name(), nextId)); } else { ourLog.debug("Resource with match URL [{}] already exists, will be NOOP", matchUrl); persistedResources.add(null); retVal.add(nextResource); continue; } } } else if (candidateMatches.size() == 1) { entity = loadFirstEntityFromCandidateMatches(candidateMatches); } else { throw new InvalidRequestException( getContext() .getLocalizer() .getMessage( BaseHapiFhirSystemDao.class, "transactionOperationWithMultipleMatchFailure", nextResouceOperationIn.name(), matchUrl, candidateMatches.size())); } } else if (nextId.isEmpty() || isPlaceholder(nextId)) { entity = null; } else { entity = tryToLoadEntity(nextId); } BundleEntryTransactionMethodEnum nextResouceOperationOut; if (entity == null) { nextResouceOperationOut = BundleEntryTransactionMethodEnum.POST; entity = toEntity(nextResource); entity.setUpdated(updateTime); entity.setPublished(updateTime); if (nextId.isEmpty() == false && "cid:".equals(nextId.getBaseUrl())) { ourLog.debug( "Resource in transaction has ID[{}], will replace with server assigned ID", nextId.getIdPart()); } else if (nextResouceOperationIn == BundleEntryTransactionMethodEnum.POST) { if (nextId.isEmpty() == false) { ourLog.debug( "Resource in transaction has ID[{}] but is marked for CREATE, will ignore ID", nextId.getIdPart()); } if (candidateMatches != null) { if (candidateMatches.size() == 1) { ourLog.debug("Resource with match URL [{}] already exists, will be NOOP", matchUrl); BaseHasResource existingEntity = loadFirstEntityFromCandidateMatches(candidateMatches); IResource existing = (IResource) toResource(existingEntity, false); persistedResources.add(null); retVal.add(existing); continue; } if (candidateMatches.size() > 1) { throw new InvalidRequestException( getContext() .getLocalizer() .getMessage( BaseHapiFhirSystemDao.class, "transactionOperationWithMultipleMatchFailure", BundleEntryTransactionMethodEnum.POST.name(), matchUrl, candidateMatches.size())); } } } else { createForcedIdIfNeeded(entity, nextId); } myEntityManager.persist(entity); if (entity.getForcedId() != null) { myEntityManager.persist(entity.getForcedId()); } creations++; ourLog.info( "Resource Type[{}] with ID[{}] does not exist, creating it", resourceName, nextId); } else { nextResouceOperationOut = nextResouceOperationIn; if (nextResouceOperationOut == null) { nextResouceOperationOut = BundleEntryTransactionMethodEnum.PUT; } updates++; ourLog.info("Resource Type[{}] with ID[{}] exists, updating it", resourceName, nextId); } persistedResources.add(entity); retVal.add(nextResource); ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(nextResource, nextResouceOperationOut); } ourLog.info("Flushing transaction to database"); myEntityManager.flush(); for (int i = 0; i < persistedResources.size(); i++) { ResourceTable entity = persistedResources.get(i); String resourceName = toResourceName(theResources.get(i)); IdDt nextId = theResources.get(i).getId(); IdDt newId; if (entity == null) { newId = retVal.get(i + 1).getId().toUnqualifiedVersionless(); } else { newId = entity.getIdDt().toUnqualifiedVersionless(); } if (nextId == null || nextId.isEmpty()) { ourLog.info( "Transaction resource (with no preexisting ID) has been assigned new ID[{}]", nextId, newId); } else { if (nextId.toUnqualifiedVersionless().equals(newId)) { ourLog.info("Transaction resource ID[{}] is being updated", newId); } else { if (isPlaceholder(nextId)) { // nextId = new IdDt(resourceName, nextId.getIdPart()); ourLog.info("Transaction resource ID[{}] has been assigned new ID[{}]", nextId, newId); idConversions.put(nextId, newId); idConversions.put(new IdDt(resourceName + "/" + nextId.getValue()), newId); } } } } for (IResource nextResource : theResources) { List<BaseResourceReferenceDt> allRefs = terser.getAllPopulatedChildElementsOfType(nextResource, BaseResourceReferenceDt.class); for (BaseResourceReferenceDt nextRef : allRefs) { IdDt nextId = nextRef.getReference(); if (idConversions.containsKey(nextId)) { IdDt newId = idConversions.get(nextId); ourLog.info(" * Replacing resource ref {} with {}", nextId, newId); nextRef.setReference(newId); } else { ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); } } } ourLog.info("Re-flushing updated resource references and extracting search criteria"); for (int i = 0; i < theResources.size(); i++) { IResource resource = theResources.get(i); ResourceTable table = persistedResources.get(i); if (table == null) { continue; } InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(resource); Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; if (deletedInstantOrNull == null && ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(resource) == BundleEntryTransactionMethodEnum.DELETE) { deletedTimestampOrNull = updateTime; ResourceMetadataKeyEnum.DELETED_AT.put(resource, new InstantDt(deletedTimestampOrNull)); } updateEntity(resource, table, table.getId() != null, deletedTimestampOrNull, updateTime); } long delay = System.currentTimeMillis() - start; ourLog.info( "Transaction completed in {}ms with {} creations and {} updates", new Object[] {delay, creations, updates}); oo.addIssue() .setSeverity(IssueSeverityEnum.INFORMATION) .setDetails( "Transaction completed in " + delay + "ms with " + creations + " creations and " + updates + " updates"); return retVal; }