Implement sorting on _id and clean up create/update semantics for DSTU2

This commit is contained in:
James Agnew 2015-03-11 12:49:47 -04:00
parent b4911788a3
commit a2e562de64
7 changed files with 672 additions and 450 deletions

View File

@ -22,18 +22,29 @@ package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.IGenericClient;
public interface ICreateTyped extends IClientExecutable<ICreateTyped, MethodOutcome> {
/**
* If you want the explicitly state an ID for your created resource, put that ID here. You generally do not
* need to invoke this method, so that the server will assign the ID itself.
*
* <p>
* Note that creating a resource by ID is no longer supported as of FHIR DSTU2. You should use the {@link IGenericClient#update()} operation
* to create-by-ID in DSTU2.
* </p>
*/
ICreateTyped withId(String theId);
/**
* If you want the explicitly state an ID for your created resource, put that ID here. You generally do not
* need to invoke this method, so that the server will assign the ID itself.
*
* <p>
* Note that creating a resource by ID is no longer supported as of FHIR DSTU2. You should use the {@link IGenericClient#update()} operation
* to create-by-ID in DSTU2.
* </p>
*/
ICreateTyped withId(IdDt theId);

View File

@ -35,5 +35,6 @@ ca.uhn.fhir.jpa.dao.BaseFhirSystemDao.transactionMissingUrl=Unable to perform {0
ca.uhn.fhir.jpa.dao.BaseFhirSystemDao.transactionInvalidUrl=Unable to perform {0}, URL provided is invalid: {1}
ca.uhn.fhir.jpa.dao.FhirResourceDao.duplicateCreateForcedId=Can not create entity with ID[{0}], a resource with this ID already exists
ca.uhn.fhir.jpa.dao.FhirResourceDao.failedToCreateWithClientAssignedNumericId=Can not create entity with ID[{0}], this server does not allow clients to assign numeric IDs
ca.uhn.fhir.jpa.dao.FhirResourceDao.failedToCreateWithClientAssignedNumericId=Can not create resource with ID[{0}], no resource with this ID exists and clients may only assign IDs which begin with a non-numeric character on this server
ca.uhn.fhir.jpa.dao.FhirResourceDao.failedToCreateWithClientAssignedId=Can not create resource with ID[{0}], ID must not be supplied on a create (POST) operation
ca.uhn.fhir.jpa.dao.FhirResourceDao.unableToDeleteNotFound=Unable to find resource matching URL "{0}". Deletion failed.

View File

@ -65,6 +65,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
@ -690,16 +691,24 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
@Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing) {
StopWatch w = new StopWatch();
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
if (isNotBlank(theResource.getId().getIdPart())) {
if (theResource.getId().isIdPartValidLong()) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart()));
if (getContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU1)) {
if (theResource.getId().isIdPartValidLong()) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart()));
}
} else {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getId().getIdPart()));
}
}
return doCreate(theResource, theIfNoneExist, thePerformIndexing);
}
private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing) {
StopWatch w = new StopWatch();
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
if (isNotBlank(theIfNoneExist)) {
Set<Long> match = processMatchUrl(theIfNoneExist, myResourceType);
if (match.size() > 1) {
@ -867,6 +876,20 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return;
}
if ("_id".equals(theSort.getParamName())) {
From<?, ?> forcedIdJoin = theFrom.join("myForcedId", JoinType.LEFT);
if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
theOrders.add(theBuilder.asc(forcedIdJoin.get("myForcedId")));
theOrders.add(theBuilder.asc(theFrom.get("myId")));
}else {
theOrders.add(theBuilder.desc(forcedIdJoin.get("myForcedId")));
theOrders.add(theBuilder.desc(theFrom.get("myId")));
}
createSort(theBuilder, theFrom, theSort.getChain(), theOrders, null);
return;
}
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceType);
RuntimeSearchParam param = resourceDef.getSearchParam(theSort.getParamName());
if (param == null) {
@ -1632,7 +1655,14 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (resourceId == null || isBlank(resourceId.getIdPart())) {
throw new InvalidRequestException("Can not update a resource with no ID");
}
entity = readEntityLatestVersion(resourceId);
try {
entity = readEntityLatestVersion(resourceId);
} catch (ResourceNotFoundException e) {
if (Character.isDigit(theResource.getId().getIdPart().charAt(0))) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart()));
}
return doCreate(theResource, null, true);
}
}
if (resourceId.hasVersionIdPart() && resourceId.getVersionIdPartAsLong().longValue() != entity.getVersion()) {

View File

@ -0,0 +1,142 @@
package ca.uhn.fhir.jpa.dao;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import org.hamcrest.core.StringContains;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.Device;
import ca.uhn.fhir.model.dstu.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu.resource.Encounter;
import ca.uhn.fhir.model.dstu.resource.Location;
import ca.uhn.fhir.model.dstu.resource.Observation;
import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
public class FhirResourceDaoDstu1Test {
private static ClassPathXmlApplicationContext ourCtx;
private static IFhirResourceDao<Device> ourDeviceDao;
private static IFhirResourceDao<DiagnosticReport> ourDiagnosticReportDao;
private static IFhirResourceDao<Encounter> ourEncounterDao;
private static FhirContext ourFhirCtx;
private static IFhirResourceDao<Location> ourLocationDao;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu1Test.class);
private static IFhirResourceDao<Observation> ourObservationDao;
private static IFhirResourceDao<Organization> ourOrganizationDao;
private static IFhirResourceDao<Patient> ourPatientDao;
@Test
public void testCreateDuplicateIdFails() {
String methodName = "testCreateDuplocateIdFailsText";
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.setId("Patient/" + methodName);
IdDt id = ourPatientDao.create(p).getId();
ourLog.info("Created patient, got it: {}", id);
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.addName().addFamily("Hello");
p.setId("Patient/" + methodName);
try {
ourPatientDao.create(p);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("Can not create entity with ID[" + methodName + "], a resource with this ID already exists"));
}
}
@Test
public void testCreateNumericIdFails() {
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("testCreateNumericIdFails");
p.addName().addFamily("Hello");
p.setId("Patient/123");
try {
ourPatientDao.create(p);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("clients may only assign IDs which begin with a non-numeric"));
}
}
@Test
public void testCreateWithInvalidReferenceFailsGracefully() {
Patient patient = new Patient();
patient.addName().addFamily("testSearchResourceLinkWithChainWithMultipleTypes01");
patient.setManagingOrganization(new ResourceReferenceDt("Patient/99999999"));
try {
ourPatientDao.create(patient);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), StringContains.containsString("99999 not found"));
}
}
@Test
public void testUpdateRejectsIdWhichPointsToForcedId() throws InterruptedException {
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue("testUpdateRejectsIdWhichPointsToForcedId01");
p1.addName().addFamily("Tester").addGiven("testUpdateRejectsIdWhichPointsToForcedId01");
p1.setId("ABABA");
IdDt p1id = ourPatientDao.create(p1).getId();
assertEquals("ABABA", p1id.getIdPart());
Patient p2 = new Patient();
p2.addIdentifier().setSystem("urn:system").setValue("testUpdateRejectsIdWhichPointsToForcedId02");
p2.addName().addFamily("Tester").addGiven("testUpdateRejectsIdWhichPointsToForcedId02");
IdDt p2id = ourPatientDao.create(p2).getId();
long p1longId = p2id.getIdPartAsLong() - 1;
try {
ourPatientDao.read(new IdDt("Patient/" + p1longId));
fail();
} catch (ResourceNotFoundException e) {
// good
}
try {
p1.setId(new IdDt("Patient/" + p1longId));
ourPatientDao.update(p1);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("clients may only assign IDs which begin with a non-numeric"));
}
}
@AfterClass
public static void afterClass() {
ourCtx.close();
}
@SuppressWarnings("unchecked")
@BeforeClass
public static void beforeClass() {
ourCtx = new ClassPathXmlApplicationContext("hapi-fhir-server-resourceproviders-dstu1.xml", "fhir-jpabase-spring-test-config.xml");
ourPatientDao = ourCtx.getBean("myPatientDaoDstu1", IFhirResourceDao.class);
ourObservationDao = ourCtx.getBean("myObservationDaoDstu1", IFhirResourceDao.class);
ourDiagnosticReportDao = ourCtx.getBean("myDiagnosticReportDaoDstu1", IFhirResourceDao.class);
ourDeviceDao = ourCtx.getBean("myDeviceDaoDstu1", IFhirResourceDao.class);
ourOrganizationDao = ourCtx.getBean("myOrganizationDaoDstu1", IFhirResourceDao.class);
ourLocationDao = ourCtx.getBean("myLocationDaoDstu1", IFhirResourceDao.class);
ourEncounterDao = ourCtx.getBean("myEncounterDaoDstu1", IFhirResourceDao.class);
ourFhirCtx = ourCtx.getBean(FhirContext.class);
}
}

View File

@ -49,7 +49,7 @@ public class FhirSystemDaoDstu2Test {
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.setId("Patient/" + methodName);
IdDt id = ourPatientDao.create(p).getId();
IdDt id = ourPatientDao.update(p).getId();
ourLog.info("Created patient, got it: {}", id);
p = new Patient();
@ -91,7 +91,7 @@ public class FhirSystemDaoDstu2Test {
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.setId("Patient/" + methodName);
IdDt idv1 = ourPatientDao.create(p).getId();
IdDt idv1 = ourPatientDao.update(p).getId();
ourLog.info("Created patient, got id: {}", idv1);
p = new Patient();
@ -301,7 +301,7 @@ public class FhirSystemDaoDstu2Test {
Patient p2 = new Patient();
p2.addIdentifier().setSystem("urn:system").setValue(methodName);
p2.setId("Patient/" + methodName);
IdDt id2 = ourPatientDao.create(p2).getId();
IdDt id2 = ourPatientDao.update(p2).getId();
ourLog.info("Created patient, got it: {}", id2);
Bundle request = new Bundle();
@ -356,7 +356,7 @@ public class FhirSystemDaoDstu2Test {
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.setId("Patient/" + methodName);
IdDt id = ourPatientDao.create(p).getId();
IdDt id = ourPatientDao.update(p).getId();
ourLog.info("Created patient, got it: {}", id);
Bundle request = new Bundle();
@ -516,7 +516,7 @@ public class FhirSystemDaoDstu2Test {
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.setId("Patient/" + methodName);
IdDt id = ourPatientDao.create(p).getId();
IdDt id = ourPatientDao.update(p).getId();
ourLog.info("Created patient, got it: {}", id);
p = new Patient();

View File

@ -415,36 +415,6 @@ public class ResourceProviderDstu2Test {
}
}
@Test
public void testCreateWithClientSuppliedId() {
deleteToken("Patient", Patient.SP_IDENTIFIER, "urn:system:rpdstu2", "testCreateWithId01");
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system:rpdstu2").setValue("testCreateWithId01");
IdDt p1Id = ourClient.create().resource(p1).withId("testCreateWithIdRpDstu2").execute().getId();
assertThat(p1Id.getValue(), containsString("Patient/testCreateWithIdRpDstu2/_history"));
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:system:rpdstu2", "testCreateWithId01")).encodedJson().prettyPrint().execute();
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getResource().getId().getIdPart());
/*
* ensure that trying to create the same ID again fails appropriately
*/
try {
ourClient.create().resource(p1).withId("testCreateWithIdRpDstu2").execute().getId();
fail();
} catch (UnprocessableEntityException e) {
// good
}
Bundle history = ourClient.history(null, (String) null, null, null);
assertEquals("Expected[" + p1Id.getIdPart() + "] but was " + history.getEntries().get(0).getResource().getId(), p1Id.getIdPart(), history.getEntries().get(0).getResource().getId().getIdPart());
assertNotNull(history.getEntries().get(0).getResource());
}
@Test
public void testDeepChaining() {
delete("Location", Location.SP_NAME, "testDeepChainingL1");