Merge branch 'master' of github.com:jamesagnew/hapi-fhir

This commit is contained in:
James Agnew 2015-11-14 13:01:52 -05:00
commit 326db08f0e
11 changed files with 639 additions and 243 deletions

View File

@ -53,6 +53,7 @@ import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.client.utils.URLEncodedUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -93,6 +94,7 @@ import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.ResourceTag; import ca.uhn.fhir.jpa.entity.ResourceTag;
import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.entity.TagTypeEnum; import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IQueryParameterType;
@ -126,14 +128,20 @@ import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import net.sourceforge.cobertura.CoverageIgnore; import net.sourceforge.cobertura.CoverageIgnore;
public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao { public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
static final String OO_SEVERITY_ERROR = "error";
static final String OO_SEVERITY_INFO = "information";
static final String OO_SEVERITY_WARN = "warning";
/** /**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(Map)} * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(Map)}
*/ */
@ -160,6 +168,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams); RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
} }
public static final long INDEX_STATUS_INDEXED = Long.valueOf(1L); public static final long INDEX_STATUS_INDEXED = Long.valueOf(1L);
public static final long INDEX_STATUS_INDEXING_FAILED = Long.valueOf(2L); public static final long INDEX_STATUS_INDEXING_FAILED = Long.valueOf(2L);
public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile"; public static final String NS_JPA_PROFILE = "https://github.com/jamesagnew/hapi-fhir/ns/jpa/profile";
@ -1562,6 +1571,20 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
return retVal.toString(); return retVal.toString();
} }
protected void validateDeleteConflictsEmptyOrThrowException(List<DeleteConflict> theDeleteConflicts) {
if (theDeleteConflicts.isEmpty()) {
return;
}
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
for (DeleteConflict next : theDeleteConflicts) {
String msg = "Unable to delete " + next.getTargetId().toUnqualifiedVersionless().getValue() + " because at least one resource has a reference to this resource. First reference found was resource " + next.getTargetId().toUnqualifiedVersionless().getValue() + " in path " + next.getSourcePath();
OperationOutcomeUtil.addIssue(getContext(), oo, OO_SEVERITY_ERROR, msg, null, "processing");
}
throw new ResourceVersionConflictException("Delete failed because of constraint failure", oo);
}
public BaseHasResource readEntity(IIdType theValueId) { public BaseHasResource readEntity(IIdType theValueId) {
throw new NotImplementedException(""); throw new NotImplementedException("");
} }

View File

@ -62,6 +62,7 @@ import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.entity.TagTypeEnum; import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor; import ca.uhn.fhir.jpa.interceptor.IJpaServerInterceptor;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.StopWatch; import ca.uhn.fhir.jpa.util.StopWatch;
import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
@ -81,7 +82,6 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
@ -91,31 +91,27 @@ import ca.uhn.fhir.util.ObjectUtil;
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> { public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
static final String OO_SEVERITY_ERROR = "error";
static final String OO_SEVERITY_INFO = "information";
static final String OO_SEVERITY_WARN = "warning";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class);
@Autowired
private DaoConfig myDaoConfig;
@PersistenceContext(type = PersistenceContextType.TRANSACTION) @PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager; protected EntityManager myEntityManager;
@Autowired @Autowired
protected PlatformTransactionManager myPlatformTransactionManager; protected PlatformTransactionManager myPlatformTransactionManager;
@Autowired private String myResourceName;
private DaoConfig myDaoConfig;
private Class<T> myResourceType;
@Autowired(required = false) @Autowired(required = false)
protected ISearchDao mySearchDao; protected ISearchDao mySearchDao;
private String myResourceName;
private Class<T> myResourceType;
private String mySecondaryPrimaryKeyParamName;
@Autowired() @Autowired()
protected ISearchResultDao mySearchResultDao; protected ISearchResultDao mySearchResultDao;
private String mySecondaryPrimaryKeyParamName;
@Override @Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
StopWatch w = new StopWatch(); StopWatch w = new StopWatch();
@ -184,16 +180,28 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
@Override @Override
public DaoMethodOutcome delete(IIdType theId) { public DaoMethodOutcome delete(IIdType theId) {
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
StopWatch w = new StopWatch();
ResourceTable savedEntity = delete(theId, deleteConflicts);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
ourLog.info("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return toMethodOutcome(savedEntity, null);
}
@Override
public ResourceTable delete(IIdType theId, List<DeleteConflict> deleteConflicts) {
if (theId == null || !theId.hasIdPart()) { if (theId == null || !theId.hasIdPart()) {
throw new InvalidRequestException("Can not perform delete, no ID provided"); throw new InvalidRequestException("Can not perform delete, no ID provided");
} }
StopWatch w = new StopWatch();
final ResourceTable entity = readEntityLatestVersion(theId); final ResourceTable entity = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) { if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
throw new InvalidRequestException("Trying to delete " + theId + " but this is not the current version"); throw new InvalidRequestException("Trying to delete " + theId + " but this is not the current version");
} }
validateOkToDeleteOrThrowResourceVersionConflictException(entity); validateOkToDelete(deleteConflicts, entity);
// Notify interceptors // Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theId, theId.getResourceType()); ActionRequestDetails requestDetails = new ActionRequestDetails(theId, theId.getResourceType());
@ -208,37 +216,41 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
((IJpaServerInterceptor) next).resourceDeleted(requestDetails, entity); ((IJpaServerInterceptor) next).resourceDeleted(requestDetails, entity);
} }
} }
return savedEntity;
ourLog.info("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return toMethodOutcome(savedEntity, null);
} }
@Override @Override
public DaoMethodOutcome deleteByUrl(String theUrl) { public DaoMethodOutcome deleteByUrl(String theUrl) {
return deleteByUrl(theUrl, false); StopWatch w = new StopWatch();
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
List<ResourceTable> deletedResources = deleteByUrl(theUrl, deleteConflicts);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
if (deletedResources.isEmpty()) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl));
}
ourLog.info("Processed delete on {} (matched {} resource(s)) in {}ms", new Object[] { theUrl, deletedResources.size(), w.getMillisAndRestart() });
return new DaoMethodOutcome();
} }
@Override @Override
public DaoMethodOutcome deleteByUrl(String theUrl, boolean theInTransaction) { public List<ResourceTable> deleteByUrl(String theUrl, List<DeleteConflict> deleteConflicts) {
StopWatch w = new StopWatch();
Set<Long> resource = processMatchUrl(theUrl, myResourceType); Set<Long> resource = processMatchUrl(theUrl, myResourceType);
if (resource.isEmpty()) { if (resource.size() > 1) {
if (!theInTransaction) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl));
} else {
return new DaoMethodOutcome();
}
} else if (resource.size() > 1) {
if (myDaoConfig.isAllowMultipleDelete() == false) { if (myDaoConfig.isAllowMultipleDelete() == false) {
throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size())); throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size()));
} }
} }
List<ResourceTable> retVal = new ArrayList<ResourceTable>();
for (Long pid : resource) { for (Long pid : resource) {
ResourceTable entity = myEntityManager.find(ResourceTable.class, pid); ResourceTable entity = myEntityManager.find(ResourceTable.class, pid);
retVal.add(entity);
validateOkToDeleteOrThrowResourceVersionConflictException(entity); validateOkToDelete(deleteConflicts, entity);
// Notify interceptors // Notify interceptors
IdDt idToDelete = entity.getIdDt(); IdDt idToDelete = entity.getIdDt();
@ -258,9 +270,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
} }
ourLog.info("Processed delete on {} (matched {} resource(s)) in {}ms", new Object[] { theUrl, resource.size(), w.getMillisAndRestart() }); return retVal;
return new DaoMethodOutcome();
} }
private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTime) { private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTime) {
@ -491,6 +501,49 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return retVal; return retVal;
} }
@Override
public MetaDt metaAddOperation(IIdType theResourceId, MetaDt theMetaAdd) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theResourceId, getResourceName());
notifyInterceptors(RestOperationTypeEnum.META_ADD, requestDetails);
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theResourceId);
if (entity == null) {
throw new ResourceNotFoundException(theResourceId);
}
List<TagDefinition> tags = toTagList(theMetaAdd);
//@formatter:off
for (TagDefinition nextDef : tags) {
boolean hasTag = false;
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
hasTag = true;
break;
}
}
if (!hasTag) {
entity.setHasTags(true);
TagDefinition def = getTag(nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay());
BaseTag newEntity = entity.addTag(def);
myEntityManager.persist(newEntity);
}
}
//@formatter:on
myEntityManager.merge(entity);
ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() });
return metaGetOperation(theResourceId);
}
// @Override // @Override
// public IBundleProvider everything(IIdType theId) { // public IBundleProvider everything(IIdType theId) {
// Search search = new Search(); // Search search = new Search();
@ -573,49 +626,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
// }; // };
// } // }
@Override
public MetaDt metaAddOperation(IIdType theResourceId, MetaDt theMetaAdd) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theResourceId, getResourceName());
notifyInterceptors(RestOperationTypeEnum.META_ADD, requestDetails);
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theResourceId);
if (entity == null) {
throw new ResourceNotFoundException(theResourceId);
}
List<TagDefinition> tags = toTagList(theMetaAdd);
//@formatter:off
for (TagDefinition nextDef : tags) {
boolean hasTag = false;
for (BaseTag next : new ArrayList<BaseTag>(entity.getTags())) {
if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
hasTag = true;
break;
}
}
if (!hasTag) {
entity.setHasTags(true);
TagDefinition def = getTag(nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay());
BaseTag newEntity = entity.addTag(def);
myEntityManager.persist(newEntity);
}
}
//@formatter:on
myEntityManager.merge(entity);
ourLog.info("Processed metaAddOperation on {} in {}ms", new Object[] { theResourceId, w.getMillisAndRestart() });
return metaGetOperation(theResourceId);
}
@Override @Override
public MetaDt metaDeleteOperation(IIdType theResourceId, MetaDt theMetaDel) { public MetaDt metaDeleteOperation(IIdType theResourceId, MetaDt theMetaDel) {
// Notify interceptors // Notify interceptors
@ -810,6 +820,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return entity; return entity;
} }
@Override
public void reindex(T theResource, ResourceTable theEntity) {
updateEntity(theResource, theEntity, false, null, true, false, theEntity.getUpdatedDate());
}
@Override @Override
public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) { public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) {
// Notify interceptors // Notify interceptors
@ -842,11 +857,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
ourLog.info("Processed remove tag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId.getValue(), w.getMillisAndRestart() }); ourLog.info("Processed remove tag {}/{} on {} in {}ms", new Object[] { theScheme, theTerm, theId.getValue(), w.getMillisAndRestart() });
} }
@Override
public void reindex(T theResource, ResourceTable theEntity) {
updateEntity(theResource, theEntity, false, null, true, false, theEntity.getUpdatedDate());
}
@Override @Override
public IBundleProvider search(Map<String, IQueryParameterType> theParams) { public IBundleProvider search(Map<String, IQueryParameterType> theParams) {
SearchParameterMap map = new SearchParameterMap(); SearchParameterMap map = new SearchParameterMap();
@ -924,9 +934,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return retVal; return retVal;
} }
private ArrayList<TagDefinition> toTagList(MetaDt theMeta) { private ArrayList<TagDefinition> toTagList(MetaDt theMeta) {
ArrayList<TagDefinition> retVal = new ArrayList<TagDefinition>(); ArrayList<TagDefinition> retVal = new ArrayList<TagDefinition>();
@ -943,6 +950,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
return retVal; return retVal;
} }
@Override @Override
public DaoMethodOutcome update(T theResource) { public DaoMethodOutcome update(T theResource) {
return update(theResource, null); return update(theResource, null);
@ -1032,7 +1042,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
} }
} }
protected void validateOkToDeleteOrThrowResourceVersionConflictException(ResourceTable theEntity) { protected void validateOkToDelete(List<DeleteConflict> theDeleteConflicts, ResourceTable theEntity) {
TypedQuery<ResourceLink> query = myEntityManager.createQuery("SELECT l FROM ResourceLink l WHERE l.myTargetResourcePid = :target_pid", ResourceLink.class); TypedQuery<ResourceLink> query = myEntityManager.createQuery("SELECT l FROM ResourceLink l WHERE l.myTargetResourcePid = :target_pid", ResourceLink.class);
query.setParameter("target_pid", theEntity.getId()); query.setParameter("target_pid", theEntity.getId());
query.setMaxResults(1); query.setMaxResults(1);
@ -1042,13 +1052,15 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
} }
ResourceLink link = resultList.get(0); ResourceLink link = resultList.get(0);
String targetId = theEntity.getIdDt().toUnqualifiedVersionless().getValue(); IdDt targetId = theEntity.getIdDt();
String sourceId = link.getSourceResource().getIdDt().toUnqualifiedVersionless().getValue(); IdDt sourceId = link.getSourceResource().getIdDt();
String sourcePath = link.getSourcePath(); String sourcePath = link.getSourcePath();
throw new ResourceVersionConflictException("Unable to delete " + targetId + " because at least one resource has a reference to this resource. First reference found was resource " + sourceId + " in path " + sourcePath); theDeleteConflicts.add(new DeleteConflict(sourceId, sourcePath, targetId));
} }
private void validateResourceType(BaseHasResource entity) { private void validateResourceType(BaseHasResource entity) {
validateResourceType(entity, myResourceName); validateResourceType(entity, myResourceName);
} }

View File

@ -37,6 +37,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.Include;
@ -108,14 +109,15 @@ public class FhirResourceDaoDstu2<T extends IResource> extends BaseHapiFhirResou
throw new InvalidRequestException("No ID supplied. ID is required when validating with mode=DELETE"); throw new InvalidRequestException("No ID supplied. ID is required when validating with mode=DELETE");
} }
final ResourceTable entity = readEntityLatestVersion(theId); final ResourceTable entity = readEntityLatestVersion(theId);
// Validate that there are no resources pointing to the candidate that
// would prevent deletion
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
validateOkToDelete(deleteConflicts, entity);
validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
OperationOutcome oo = new OperationOutcome(); OperationOutcome oo = new OperationOutcome();
try {
validateOkToDeleteOrThrowResourceVersionConflictException(entity);
oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDiagnostics("Ok to delete"); oo.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDiagnostics("Ok to delete");
} catch (ResourceVersionConflictException e) {
oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setDiagnostics(e.getMessage());
throw new ResourceVersionConflictException(e.getMessage(), oo);
}
return new MethodOutcome(new IdDt(theId.getValue()), oo); return new MethodOutcome(new IdDt(theId.getValue()), oo);
} }

View File

@ -23,9 +23,14 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -35,6 +40,7 @@ import javax.persistence.TypedQuery;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionDefinition;
@ -47,7 +53,9 @@ import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
@ -86,14 +94,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
@Autowired @Autowired
private PlatformTransactionManager myTxManager; private PlatformTransactionManager myTxManager;
private String extractTransactionUrlOrThrowException(Entry nextEntry, HTTPVerbEnum verb) {
String url = nextEntry.getRequest().getUrl();
if (isBlank(url)) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
}
return url;
}
private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) {
ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
@ -165,6 +165,36 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
return resp; return resp;
} }
private String extractTransactionUrlOrThrowException(Entry nextEntry, HTTPVerbEnum verb) {
String url = nextEntry.getRequest().getUrl();
if (isBlank(url)) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
}
return url;
}
/**
* This method is called for nested bundles (e.g. if we received a transaction with an entry that
* was a GET search, this method is called on the bundle for the search result, that will be placed in the
* outer bundle). This method applies the _summary and _content parameters to the output of
* that bundle.
*
* TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
*/
private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
IParser p = getContext().newJsonParser();
RestfulServerUtils.configureResponseParser(theRequestDetails, p);
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
}
private IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IResource> theClass) {
IFhirResourceDao<? extends IResource> retVal = getDao(theClass);
if (retVal == null) {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + getContext().getResourceDefinition(theClass).getName());
}
return retVal;
}
@Override @Override
public MetaDt metaGetOperation() { public MetaDt metaGetOperation() {
// Notify interceptors // Notify interceptors
@ -180,6 +210,31 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
return retVal; return retVal;
} }
private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
RuntimeResourceDefinition resType;
try {
resType = getContext().getResourceDefinition(theParts.getResourceType());
} catch (DataFormatException e) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
IFhirResourceDao<? extends IBaseResource> dao = null;
if (resType != null) {
dao = getDao(resType.getImplementingClass());
}
if (dao == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
// if (theParts.getResourceId() == null && theParts.getParams() == null) {
// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
// throw new InvalidRequestException(msg);
// }
return dao;
}
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
@Override @Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
@ -215,26 +270,51 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
Map<IdDt, IdDt> idSubstitutions = new HashMap<IdDt, IdDt>(); Map<IdDt, IdDt> idSubstitutions = new HashMap<IdDt, IdDt>();
Map<IdDt, DaoMethodOutcome> idToPersistedOutcome = new HashMap<IdDt, DaoMethodOutcome>(); Map<IdDt, DaoMethodOutcome> idToPersistedOutcome = new HashMap<IdDt, DaoMethodOutcome>();
/*
* 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(); Bundle response = new Bundle();
List<Entry> getEntries = new ArrayList<Entry>();
IdentityHashMap<Entry, Integer> originalRequestOrder = new IdentityHashMap<Bundle.Entry, 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().getValueAsEnum() == HTTPVerbEnum.GET) {
getEntries.add(theRequest.getEntry().get(i));
}
}
Collections.sort(theRequest.getEntry(), new TransactionSorter());
// TODO: process verbs in the correct order 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++) { for (int i = 0; i < theRequest.getEntry().size(); i++) {
if (i % 100 == 0) { if (i % 100 == 0) {
ourLog.info("Processed {} entries out of {}", i, theRequest.getEntry().size()); ourLog.info("Processed {} non-GET entries out of {}", i, theRequest.getEntry().size());
} }
Entry nextEntry = theRequest.getEntry().get(i); Entry nextReqEntry = theRequest.getEntry().get(i);
IResource res = nextEntry.getResource(); IResource res = nextReqEntry.getResource();
IdDt nextResourceId = null; IdDt nextResourceId = null;
if (res != null) { if (res != null) {
nextResourceId = res.getId(); nextResourceId = res.getId();
if (nextResourceId.hasIdPart() == false) { if (nextResourceId.hasIdPart() == false) {
if (isNotBlank(nextEntry.getFullUrl())) { if (isNotBlank(nextReqEntry.getFullUrl())) {
nextResourceId = new IdDt(nextEntry.getFullUrl()); nextResourceId = new IdDt(nextReqEntry.getFullUrl());
} }
} }
@ -263,12 +343,13 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
} }
HTTPVerbEnum verb = nextEntry.getRequest().getMethodElement().getValueAsEnum(); HTTPVerbEnum verb = nextReqEntry.getRequest().getMethodElement().getValueAsEnum();
if (verb == null) { if (verb == null) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextEntry.getRequest().getMethod())); throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextReqEntry.getRequest().getMethod()));
} }
String resourceType = res != null ? getContext().getResourceDefinition(res).getName() : null; String resourceType = res != null ? getContext().getResourceDefinition(res).getName() : null;
Entry nextRespEntry = response.getEntry().get(originalRequestOrder.get(nextReqEntry));
switch (verb) { switch (verb) {
case POST: { case POST: {
@ -277,24 +358,32 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
res.setId((String) null); res.setId((String) null);
DaoMethodOutcome outcome; DaoMethodOutcome outcome;
Entry newEntry = response.addEntry(); outcome = resourceDao.create(res, nextReqEntry.getRequest().getIfNoneExist(), false);
outcome = resourceDao.create(res, nextEntry.getRequest().getIfNoneExist(), false); handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res);
handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, newEntry, resourceType, res);
break; break;
} }
case DELETE: { case DELETE: {
// DELETE // DELETE
Entry newEntry = response.addEntry(); String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
String url = extractTransactionUrlOrThrowException(nextEntry, verb);
UrlParts parts = UrlUtil.parseUrl(url); UrlParts parts = UrlUtil.parseUrl(url);
ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb.getCode(), url); ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb.getCode(), url);
int status = Constants.STATUS_HTTP_204_NO_CONTENT;
if (parts.getResourceId() != null) { if (parts.getResourceId() != null) {
dao.delete(new IdDt(parts.getResourceType(), parts.getResourceId())); ResourceTable deleted = dao.delete(new IdDt(parts.getResourceType(), parts.getResourceId()), deleteConflicts);
if (deleted != null) {
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless());
}
} else { } else {
dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), true); 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;
}
} }
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_204_NO_CONTENT)); nextRespEntry.getResponse().setStatus(toStatusString(status));
break; break;
} }
case PUT: { case PUT: {
@ -303,9 +392,8 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
DaoMethodOutcome outcome; DaoMethodOutcome outcome;
Entry newEntry = response.addEntry();
String url = extractTransactionUrlOrThrowException(nextEntry, verb); String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
UrlParts parts = UrlUtil.parseUrl(url); UrlParts parts = UrlUtil.parseUrl(url);
if (isNotBlank(parts.getResourceId())) { if (isNotBlank(parts.getResourceId())) {
@ -316,80 +404,32 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
outcome = resourceDao.update(res, parts.getResourceType() + '?' + parts.getParams(), false); outcome = resourceDao.update(res, parts.getResourceType() + '?' + parts.getParams(), false);
} }
handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, newEntry, resourceType, res); handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res);
break; break;
} }
case GET: {
// SEARCH/READ/VREAD
RequestDetails requestDetails = new RequestDetails();
requestDetails.setServletRequest(theRequestDetails.getServletRequest());
requestDetails.setRequestType(RequestTypeEnum.GET);
requestDetails.setServer(theRequestDetails.getServer());
String url = extractTransactionUrlOrThrowException(nextEntry, verb);
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(nextEntry.getRequest().getIfMatch())) {
requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextEntry.getRequest().getIfMatch());
}
if (isNotBlank(nextEntry.getRequest().getIfNoneExist())) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextEntry.getRequest().getIfNoneExist());
}
if (isNotBlank(nextEntry.getRequest().getIfNoneMatch())) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextEntry.getRequest().getIfNoneMatch());
}
if (method instanceof BaseResourceReturningMethodBinding) {
try {
ResourceOrDstu1Bundle responseData = ((BaseResourceReturningMethodBinding) method).invokeServer(theRequestDetails.getServer(), requestDetails, new byte[0]);
Entry newEntry = response.addEntry();
IBaseResource resource = responseData.getResource();
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
resource = filterNestedBundle(requestDetails, resource);
}
newEntry.setResource((IResource) resource);
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
} catch (NotModifiedException e) {
Entry newEntry = response.addEntry();
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
}
} else {
throw new IllegalArgumentException("Unable to handle GET " + url);
}
}
} }
} }
FhirTerser terser = getContext().newTerser(); /*
* 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 * Perform ID substitutions and then index each resource we have saved
*/ */
FhirTerser terser = getContext().newTerser();
for (DaoMethodOutcome nextOutcome : idToPersistedOutcome.values()) { for (DaoMethodOutcome nextOutcome : idToPersistedOutcome.values()) {
IResource nextResource = (IResource) nextOutcome.getResource(); IResource nextResource = (IResource) nextOutcome.getResource();
if (nextResource == null) { if (nextResource == null) {
@ -443,6 +483,74 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
ourLog.info("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); 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++) {
Entry nextReqEntry = getEntries.get(i);
Integer originalOrder = originalRequestOrder.get(nextReqEntry);
Entry nextRespEntry = response.getEntry().get(originalOrder);
RequestDetails requestDetails = new RequestDetails();
requestDetails.setServletRequest(theRequestDetails.getServletRequest());
requestDetails.setRequestType(RequestTypeEnum.GET);
requestDetails.setServer(theRequestDetails.getServer());
String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerbEnum.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((IResource) 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; long delay = System.currentTimeMillis() - start;
ourLog.info(theActionName + " completed in {}ms", new Object[] { delay }); ourLog.info(theActionName + " completed in {}ms", new Object[] { delay });
@ -450,53 +558,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
return response; return response;
} }
/**
* This method is called for nested bundles (e.g. if we received a transaction with an entry that
* was a GET search, this method is called on the bundle for the search result, that will be placed in the
* outer bundle). This method applies the _summary and _content parameters to the output of
* that bundle.
*
* TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
*/
private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
IParser p = getContext().newJsonParser();
RestfulServerUtils.configureResponseParser(theRequestDetails, p);
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
}
private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
RuntimeResourceDefinition resType;
try {
resType = getContext().getResourceDefinition(theParts.getResourceType());
} catch (DataFormatException e) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
IFhirResourceDao<? extends IBaseResource> dao = null;
if (resType != null) {
dao = getDao(resType.getImplementingClass());
}
if (dao == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
// if (theParts.getResourceId() == null && theParts.getParams() == null) {
// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
// throw new InvalidRequestException(msg);
// }
return dao;
}
private IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IResource> theClass) {
IFhirResourceDao<? extends IResource> retVal = getDao(theClass);
if (retVal == null) {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + getContext().getResourceDefinition(theClass).getName());
}
return retVal;
}
private static void handleTransactionCreateOrUpdateOutcome(Map<IdDt, IdDt> idSubstitutions, Map<IdDt, DaoMethodOutcome> idToPersistedOutcome, IdDt nextResourceId, DaoMethodOutcome outcome, private static void handleTransactionCreateOrUpdateOutcome(Map<IdDt, IdDt> idSubstitutions, Map<IdDt, DaoMethodOutcome> idToPersistedOutcome, IdDt nextResourceId, DaoMethodOutcome outcome,
Entry newEntry, String theResourceType, IResource theRes) { Entry newEntry, String theResourceType, IResource theRes) {
IdDt newId = (IdDt) outcome.getId().toUnqualifiedVersionless(); IdDt newId = (IdDt) outcome.getId().toUnqualifiedVersionless();
@ -532,4 +593,47 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle> {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
} }
//@formatter:off
/**
* Transaction Order, per the spec:
*
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any GET interactions
*/
//@formatter:off
public class TransactionSorter implements Comparator<Entry> {
@Override
public int compare(Entry theO1, Entry theO2) {
int o1 = toOrder(theO1);
int o2 = toOrder(theO2);
return o1 - o2;
}
private int toOrder(Entry theO1) {
int o1 = 0;
if (theO1.getRequest().getMethodElement().getValueAsEnum() != null) {
switch (theO1.getRequest().getMethodElement().getValueAsEnum()) {
case DELETE:
o1 = 1;
break;
case POST:
o1 = 2;
break;
case PUT:
o1 = 3;
break;
case GET:
o1 = 4;
break;
}
}
return o1;
}
}
} }

View File

@ -23,6 +23,7 @@ import java.util.Collection;
*/ */
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -32,6 +33,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.jpa.entity.BaseHasResource; import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TagTypeEnum; import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.composite.MetaDt;
@ -57,14 +59,28 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
*/ */
DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing); DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing);
/**
* This method throws an exception if there are delete conflicts
*/
DaoMethodOutcome delete(IIdType theResource); DaoMethodOutcome delete(IIdType theResource);
/**
* This method does not throw an exception if there are delete conflicts, but populates them
* in the provided list
*/
ResourceTable delete(IIdType theResource, List<DeleteConflict> theDeleteConflictsListToPopulate);
/**
* This method throws an exception if there are delete conflicts
*/
DaoMethodOutcome deleteByUrl(String theString); DaoMethodOutcome deleteByUrl(String theString);
/** /**
* @param theTransaction Is this being called in a bundle? If so, don't throw an exception if no matches * This method does not throw an exception if there are delete conflicts, but populates them
* in the provided list
* @return
*/ */
DaoMethodOutcome deleteByUrl(String theUrl, boolean theTransaction); List<ResourceTable> deleteByUrl(String theUrl, List<DeleteConflict> theDeleteConflictsListToPopulate);
TagList getAllResourceTags(); TagList getAllResourceTags();

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.jpa.util;
import ca.uhn.fhir.model.primitive.IdDt;
public class DeleteConflict {
private final IdDt mySourceId;
private final String mySourcePath;
private final IdDt myTargetId;
public DeleteConflict(IdDt theSourceId, String theSourcePath, IdDt theTargetId) {
mySourceId = theSourceId;
mySourcePath = theSourcePath;
myTargetId = theTargetId;
}
public IdDt getSourceId() {
return mySourceId;
}
public String getSourcePath() {
return mySourcePath;
}
public IdDt getTargetId() {
return myTargetId;
}
}

View File

@ -36,6 +36,7 @@ public abstract class BaseJpaDstu2SystemTest extends BaseJpaDstu2Test {
when(myRequestDetails.getServer()).thenReturn(myServer); when(myRequestDetails.getServer()).thenReturn(myServer);
HttpServletRequest servletRequest = mock(HttpServletRequest.class); HttpServletRequest servletRequest = mock(HttpServletRequest.class);
when(myRequestDetails.getServletRequest()).thenReturn(servletRequest); when(myRequestDetails.getServletRequest()).thenReturn(servletRequest);
when(myRequestDetails.getFhirServerBase()).thenReturn("http://example.com/base");
when(servletRequest.getHeaderNames()).thenReturn(mock(Enumeration.class)); when(servletRequest.getHeaderNames()).thenReturn(mock(Enumeration.class));
when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("/Patient")); when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("/Patient"));
} }

View File

@ -685,8 +685,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
myOrganizationDao.delete(orgId); myOrganizationDao.delete(orgId);
fail(); fail();
} catch (ResourceVersionConflictException e) { } catch (ResourceVersionConflictException e) {
assertThat(e.getMessage(), containsString("Unable to delete Organization/" + orgId.getIdPart() assertThat(e.getMessage(), containsString("Delete failed because of constraint"));
+ " because at least one resource has a reference to this resource. First reference found was resource Patient/" + patId.getIdPart() + " in path Patient.managingOrganization"));
} }
myPatientDao.delete(patId); myPatientDao.delete(patId);

View File

@ -215,7 +215,7 @@ public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test {
String ooString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome); String ooString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info(ooString); ourLog.info(ooString);
assertThat(ooString, containsString("Unable to delete "+orgId.getValue()+" because at least one resource has a reference to this resource. First reference found was resource " + patId.getValue() + " in path Patient.managingOrganization")); assertThat(ooString, containsString("Unable to delete Organization"));
pat.setId(patId); pat.setId(patId);
pat.getManagingOrganization().setReference(""); pat.getManagingOrganization().setReference("");

View File

@ -589,6 +589,124 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest {
} }
/**
* See #253 Test that the order of deletes is version independent
*/
@Test
public void testTransactionDeleteIsOrderIndependantTargetFirst() {
String methodName = "testTransactionDeleteIsOrderIndependantTargetFirst";
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue(methodName);
IIdType pid = myPatientDao.create(p1).getId().toUnqualifiedVersionless();
ourLog.info("Created patient, got it: {}", pid);
Observation o1 = new Observation();
o1.getSubject().setReference(pid);
IIdType oid1 = myObservationDao.create(o1).getId().toUnqualifiedVersionless();
Observation o2 = new Observation();
o2.addIdentifier().setValue(methodName);
o2.getSubject().setReference(pid);
IIdType oid2 = myObservationDao.create(o2).getId().toUnqualifiedVersionless();
myPatientDao.read(pid);
myObservationDao.read(oid1);
// The target is Patient, so try with it first in the bundle
Bundle request = new Bundle();
request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl(pid.getValue());
request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl(oid1.getValue());
request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl("Observation?identifier=" + methodName);
Bundle resp = mySystemDao.transaction(myRequestDetails, request);
assertEquals(3, resp.getEntry().size());
assertEquals("204 No Content", resp.getEntry().get(0).getResponse().getStatus());
assertEquals("204 No Content", resp.getEntry().get(1).getResponse().getStatus());
assertEquals("204 No Content", resp.getEntry().get(2).getResponse().getStatus());
try {
myPatientDao.read(pid);
fail();
} catch (ResourceGoneException e) {
// good
}
try {
myObservationDao.read(oid1);
fail();
} catch (ResourceGoneException e) {
// good
}
try {
myObservationDao.read(oid2);
fail();
} catch (ResourceGoneException e) {
// good
}
}
/**
* See #253 Test that the order of deletes is version independent
*/
@Test
public void testTransactionDeleteIsOrderIndependantTargetLast() {
String methodName = "testTransactionDeleteIsOrderIndependantTargetFirst";
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue(methodName);
IIdType pid = myPatientDao.create(p1).getId().toUnqualifiedVersionless();
ourLog.info("Created patient, got it: {}", pid);
Observation o1 = new Observation();
o1.getSubject().setReference(pid);
IIdType oid1 = myObservationDao.create(o1).getId().toUnqualifiedVersionless();
Observation o2 = new Observation();
o2.addIdentifier().setValue(methodName);
o2.getSubject().setReference(pid);
IIdType oid2 = myObservationDao.create(o2).getId().toUnqualifiedVersionless();
myPatientDao.read(pid);
myObservationDao.read(oid1);
// The target is Patient, so try with it last in the bundle
Bundle request = new Bundle();
request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl(oid1.getValue());
request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl("Observation?identifier=" + methodName);
request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl(pid.getValue());
Bundle resp = mySystemDao.transaction(myRequestDetails, request);
assertEquals(3, resp.getEntry().size());
assertEquals("204 No Content", resp.getEntry().get(0).getResponse().getStatus());
assertEquals("204 No Content", resp.getEntry().get(1).getResponse().getStatus());
assertEquals("204 No Content", resp.getEntry().get(2).getResponse().getStatus());
try {
myPatientDao.read(pid);
fail();
} catch (ResourceGoneException e) {
// good
}
try {
myObservationDao.read(oid1);
fail();
} catch (ResourceGoneException e) {
// good
}
try {
myObservationDao.read(oid2);
fail();
} catch (ResourceGoneException e) {
// good
}
}
@Test @Test
public void testTransactionDeleteMatchUrlWithOneMatch() { public void testTransactionDeleteMatchUrlWithOneMatch() {
String methodName = "testTransactionDeleteMatchUrlWithOneMatch"; String methodName = "testTransactionDeleteMatchUrlWithOneMatch";
@ -670,7 +788,10 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest {
request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl("Patient?identifier=urn%3Asystem%7C" + methodName); request.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl("Patient?identifier=urn%3Asystem%7C" + methodName);
// try { // try {
mySystemDao.transaction(myRequestDetails, request); Bundle resp = mySystemDao.transaction(myRequestDetails, request);
assertEquals(1, resp.getEntry().size());
assertEquals("404 Not Found", resp.getEntry().get(0).getResponse().getStatus());
// fail(); // fail();
// } catch (ResourceNotFoundException e) { // } catch (ResourceNotFoundException e) {
// assertThat(e.getMessage(), containsString("resource matching URL \"Patient?")); // assertThat(e.getMessage(), containsString("resource matching URL \"Patient?"));
@ -762,6 +883,88 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest {
assertEquals("201 Created", resp.getEntry().get(1).getResponse().getStatus()); assertEquals("201 Created", resp.getEntry().get(1).getResponse().getStatus());
} }
@Test
public void testTransactionOrdering() {
String methodName = "testTransactionOrdering";
//@formatter:off
/*
* Transaction Order, per the spec:
*
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any GET interactions
*
* This test creates a transaction bundle that includes
* these four operations in the reverse order and verifies
* that they are invoked correctly.
*/
//@formatter:off
int pass = 0;
IdDt patientPlaceholderId = IdDt.newRandomUuid();
Bundle req = testTransactionOrderingCreateBundle(methodName, pass, patientPlaceholderId);
Bundle resp = mySystemDao.transaction(myRequestDetails, req);
testTransactionOrderingValidateResponse(pass, resp);
pass = 1;
patientPlaceholderId = IdDt.newRandomUuid();
req = testTransactionOrderingCreateBundle(methodName, pass, patientPlaceholderId);
resp = mySystemDao.transaction(myRequestDetails, req);
testTransactionOrderingValidateResponse(pass, resp);
}
private void testTransactionOrderingValidateResponse(int pass, Bundle resp) {
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals(4, resp.getEntry().size());
assertEquals("200 OK", resp.getEntry().get(0).getResponse().getStatus());
if (pass == 0) {
assertEquals("201 Created", resp.getEntry().get(1).getResponse().getStatus());
assertThat(resp.getEntry().get(1).getResponse().getLocation(), startsWith("Observation/"));
assertThat(resp.getEntry().get(1).getResponse().getLocation(), endsWith("_history/1"));
} else {
assertEquals("200 OK", resp.getEntry().get(1).getResponse().getStatus());
assertThat(resp.getEntry().get(1).getResponse().getLocation(), startsWith("Observation/"));
assertThat(resp.getEntry().get(1).getResponse().getLocation(), endsWith("_history/2"));
}
assertEquals("201 Created", resp.getEntry().get(2).getResponse().getStatus());
assertThat(resp.getEntry().get(2).getResponse().getLocation(), startsWith("Patient/"));
if (pass == 0) {
assertEquals("404 Not Found", resp.getEntry().get(3).getResponse().getStatus());
} else {
assertEquals("204 No Content", resp.getEntry().get(3).getResponse().getStatus());
}
Bundle respGetBundle = (Bundle) resp.getEntry().get(0).getResource();
assertEquals(1, respGetBundle.getEntry().size());
assertEquals("testTransactionOrdering" + pass, ((Patient)respGetBundle.getEntry().get(0).getResource()).getNameFirstRep().getFamilyFirstRep().getValue());
assertThat(respGetBundle.getLink("self").getUrl(), endsWith("/Patient?identifier=testTransactionOrdering"));
}
private Bundle testTransactionOrderingCreateBundle(String methodName, int pass, IdDt patientPlaceholderId) {
Bundle req = new Bundle();
req.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl("Patient?identifier=" + methodName);
Observation obs = new Observation();
obs.getSubject().setReference(patientPlaceholderId);
obs.addIdentifier().setValue(methodName);
obs.getCode().setText(methodName + pass);
req.addEntry().setResource(obs).getRequest().setMethod(HTTPVerbEnum.PUT).setUrl("Observation?identifier=" + methodName);
Patient pat = new Patient();
pat.addIdentifier().setValue(methodName);
pat.addName().addFamily(methodName + pass);
req.addEntry().setResource(pat).setFullUrl(patientPlaceholderId.getValue()).getRequest().setMethod(HTTPVerbEnum.POST).setUrl("Patient");
req.addEntry().getRequest().setMethod(HTTPVerbEnum.DELETE).setUrl("Patient?identifier=" + methodName);
return req;
}
@Test @Test
public void testTransactionReadAndSearch() { public void testTransactionReadAndSearch() {
String methodName = "testTransactionReadAndSearch"; String methodName = "testTransactionReadAndSearch";
@ -829,6 +1032,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest {
details = detailsCapt.getValue(); details = detailsCapt.getValue();
assertEquals("Patient", details.getResourceType()); assertEquals("Patient", details.getResourceType());
} }
@Test @Test

View File

@ -245,6 +245,12 @@
codes (specifically, ValueSets which defined concepts containing codes (specifically, ValueSets which defined concepts containing
child concepts did not result in Enum values for the child concepts) child concepts did not result in Enum values for the child concepts)
</action> </action>
<action type="fix" fix="253">
In the JPA server, order of transaction processing should be
DELETE, POST, PUT, GET, and the order should not matter
within entries with the same verb. Thanks to Bill de Beaubien
for reporting!
</action>
</release> </release>
<release version="1.2" date="2015-09-18"> <release version="1.2" date="2015-09-18">
<action type="add"> <action type="add">