diff --git a/examples/src/main/java/example/PagingPatientProvider.java b/examples/src/main/java/example/PagingPatientProvider.java index 5f9e34025f0..efcfaa7ff46 100644 --- a/examples/src/main/java/example/PagingPatientProvider.java +++ b/examples/src/main/java/example/PagingPatientProvider.java @@ -13,6 +13,8 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.IResourceProvider; +import javax.annotation.Nonnull; + @SuppressWarnings("null") // START SNIPPET: provider public class PagingPatientProvider implements IResourceProvider { @@ -43,7 +45,8 @@ public class PagingPatientProvider implements IResourceProvider { return matchingResourceIds.size(); } - @Override + @Nonnull + @Override public List getResources(int theFromIndex, int theToIndex) { int end = Math.max(theToIndex, matchingResourceIds.size() - 1); List idsToReturn = matchingResourceIds.subList(theFromIndex, end); diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java index 36585e6bdb0..6315e607d69 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java @@ -1765,14 +1765,30 @@ public class GenericJaxRsClientDstu3Test { @Test public void testTransactionWithString() { - org.hl7.fhir.dstu3.model.Bundle req = new org.hl7.fhir.dstu3.model.Bundle(); + Bundle req = new Bundle(); + req.setType(BundleType.TRANSACTION); + Patient patient = new Patient(); - patient.addName().setFamily("PAT_FAMILY"); - req.addEntry().setResource(patient); + patient.setId("C01"); + patient.addName().setFamily("Smith").addGiven("John"); + req.addEntry() + .setFullUrl("Patient/C01") + .setResource(patient).getRequest().setMethod(HTTPVerb.PUT).setUrl("Patient/C01"); + Observation observation = new Observation(); - observation.getCode().setText("OBS_TEXT"); - req.addEntry().setResource(observation); - String reqString = ourCtx.newJsonParser().encodeResourceToString(req); + observation.setId("C02"); + observation.setStatus(Observation.ObservationStatus.FINAL); + observation.setEffective(new DateTimeType("2019-02-21T13:35:00-05:00")); + observation.getSubject().setReference("Patient/C01"); + observation.getCode().addCoding().setSystem("http://loinc.org").setCode("3141-9").setDisplay("Body Weight Measured"); + observation.setValue(new Quantity(null, 190, "http://unitsofmeaure.org", "{lb_av}", "{lb_av}")); + req.addEntry() + .setFullUrl("Observation/C02") + .setResource(observation).getRequest().setMethod(HTTPVerb.PUT).setUrl("Observation/C02"); + String reqString = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(req); + ourLog.info(reqString); + reqString = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(req); + ourLog.info(reqString); org.hl7.fhir.dstu3.model.Bundle resp = new org.hl7.fhir.dstu3.model.Bundle(); resp.addEntry().getResponse().setLocation("Patient/1/_history/1"); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/.editorconfig b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/.editorconfig new file mode 100644 index 00000000000..e69de29bb2d diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java index 11d6d71004b..5f61a46a253 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java @@ -44,6 +44,7 @@ import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; +import javax.annotation.Nonnull; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.TypedQuery; @@ -222,6 +223,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider { return new InstantDt(mySearchEntity.getCreated()); } + @Nonnull @Override public List getResources(final int theFromIndex, final int theToIndex) { ensureDependenciesInjected(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaSearchFirstPageBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaSearchFirstPageBundleProvider.java index 28f88d0994d..06936e4991d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaSearchFirstPageBundleProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaSearchFirstPageBundleProvider.java @@ -36,6 +36,7 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionTemplate; +import javax.annotation.Nonnull; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -56,6 +57,7 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl myTxManager = theTxManager; } + @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java index 6c2a26ba450..60864857104 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java @@ -99,6 +99,7 @@ public class TestUtil { if (!isTransient) { boolean hasColumn = nextField.getAnnotation(Column.class) != null; boolean hasJoinColumn = nextField.getAnnotation(JoinColumn.class) != null; + boolean hasEmbeddedId = nextField.getAnnotation(EmbeddedId.class) != null; OneToMany oneToMany = nextField.getAnnotation(OneToMany.class); OneToOne oneToOne = nextField.getAnnotation(OneToOne.class); boolean isOtherSideOfOneToManyMapping = oneToMany != null && isNotBlank(oneToMany.mappedBy()); @@ -107,7 +108,8 @@ public class TestUtil { hasColumn || hasJoinColumn || isOtherSideOfOneToManyMapping || - isOtherSideOfOneToOneMapping, "Non-transient has no @Column or @JoinColumn: " + nextField); + isOtherSideOfOneToOneMapping || + hasEmbeddedId, "Non-transient has no @Column or @JoinColumn or @EmbeddedId: " + nextField); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java index 1ea61ad2067..72f0d32ee7e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java @@ -2,8 +2,14 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; import org.junit.AfterClass; @@ -11,6 +17,9 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + +import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.*; public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test { @@ -60,6 +69,88 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test { } + + @Test + public void testDeleteCircularReferenceInTransaction() throws IOException { + + // Create two resources with a circular reference + Organization org1 = new Organization(); + org1.setId(IdType.newRandomUuid()); + Organization org2 = new Organization(); + org2.setId(IdType.newRandomUuid()); + org1.getPartOf().setReference(org2.getId()); + org2.getPartOf().setReference(org1.getId()); + + // Upload them in a transaction + Bundle createTransaction = new Bundle(); + createTransaction.setType(Bundle.BundleType.TRANSACTION); + createTransaction + .addEntry() + .setResource(org1) + .setFullUrl(org1.getId()) + .getRequest() + .setMethod(Bundle.HTTPVerb.POST) + .setUrl("Organization"); + createTransaction + .addEntry() + .setResource(org2) + .setFullUrl(org2.getId()) + .getRequest() + .setMethod(Bundle.HTTPVerb.POST) + .setUrl("Organization"); + + Bundle createResponse = mySystemDao.transaction(mySrd, createTransaction); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createResponse)); + + IdType orgId1 = new IdType(createResponse.getEntry().get(0).getResponse().getLocation()).toUnqualifiedVersionless(); + IdType orgId2 = new IdType(createResponse.getEntry().get(1).getResponse().getLocation()).toUnqualifiedVersionless(); + + // Nope, can't delete 'em! + try { + myOrganizationDao.delete(orgId1); + fail(); + } catch (ResourceVersionConflictException e) { + // good + } + try { + myOrganizationDao.delete(orgId2); + fail(); + } catch (ResourceVersionConflictException e) { + // good + } + + // Now in a transaction + Bundle deleteTransaction = new Bundle(); + deleteTransaction.setType(Bundle.BundleType.TRANSACTION); + deleteTransaction.addEntry() + .getRequest() + .setMethod(Bundle.HTTPVerb.DELETE) + .setUrl(orgId1.getValue()); + deleteTransaction.addEntry() + .getRequest() + .setMethod(Bundle.HTTPVerb.DELETE) + .setUrl(orgId2.getValue()); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(deleteTransaction)); + mySystemDao.transaction(mySrd, deleteTransaction); + + // Make sure they were deleted + try { + myOrganizationDao.read(orgId1); + fail(); + } catch (ResourceGoneException e) { + // good + } + try { + myOrganizationDao.read(orgId2); + fail(); + } catch (ResourceGoneException e) { + // good + } + + + } + + @Test public void testResourceIsConsideredDeletedIfOnlyResourceTableEntryIsDeleted() { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java index 5bcb99d40d4..888897c5113 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java @@ -3,6 +3,8 @@ package ca.uhn.fhir.rest.api.server; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Date; import java.util.List; @@ -97,7 +99,7 @@ public interface IBundleProvider { * additional 20 resources which matched a client's _include specification. *

* Note that if this bundle provider was loaded using a - * page ID (i.e. via {@link ca.uhn.fhir.rest.server.IPagingProvider#retrieveResultList(String, String)} + * page ID (i.e. via {@link ca.uhn.fhir.rest.server.IPagingProvider#retrieveResultList(RequestDetails, String, String)} * because {@link #getNextPageId()} provided a value on the * previous page, then the indexes should be ignored and the * whole page returned. @@ -107,6 +109,7 @@ public interface IBundleProvider { * @param theToIndex The high index (exclusive) to return * @return A list of resources. The size of this list must be at least theToIndex - theFromIndex. */ + @Nonnull List getResources(int theFromIndex, int theToIndex); /** @@ -126,6 +129,7 @@ public interface IBundleProvider { * the search, and not to the individual page. *

*/ + @Nullable String getUuid(); /** @@ -144,6 +148,19 @@ public interface IBundleProvider { * _include's or OperationOutcome). May return {@literal null} if the total size is not * known or would be too expensive to calculate. */ + @Nullable Integer size(); + /** + * This method returns true if the bundle provider knows that at least + * one result exists. + */ + default boolean isEmpty() { + Integer size = size(); + if (size != null) { + return size > 0; + } + return getResources(0, 1).isEmpty(); + } + } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviderWithNamedPages.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviderWithNamedPages.java index ca22dbdce41..69fcc4b2846 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviderWithNamedPages.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviderWithNamedPages.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.rest.server; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; +import javax.annotation.Nonnull; import java.util.List; /** @@ -84,6 +85,7 @@ public class BundleProviderWithNamedPages extends SimpleBundleProvider { return this; } + @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { return (List) getList(); // indexes are ignored for this provider type diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java index 2fd1a33e951..404d26b265f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java @@ -29,6 +29,8 @@ import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.util.CoverageIgnore; +import javax.annotation.Nonnull; + /** * Utility methods for working with {@link IBundleProvider} */ @@ -46,6 +48,7 @@ public class BundleProviders { public static IBundleProvider newEmptyList() { final InstantDt published = InstantDt.withCurrentTime(); return new IBundleProvider() { + @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { return Collections.emptyList(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java index d4e35a24e9f..813d05ad5cb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import javax.annotation.Nonnull; import java.util.Collections; import java.util.Date; import java.util.List; @@ -78,6 +79,7 @@ public class SimpleBundleProvider implements IBundleProvider { myPublished = thePublished; } + @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { return (List) myList.subList(theFromIndex, Math.min(theToIndex, myList.size())); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java index 6fa6442843f..a69938de708 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java @@ -168,6 +168,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { return resources.getPublished(); } + @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { List retVal = resources.getResources(theFromIndex, theToIndex); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java index 849a1bba2df..dbe6897d299 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java @@ -51,6 +51,8 @@ import static org.junit.Assert.*; import ca.uhn.fhir.test.utilities.JettyUtil; +import javax.annotation.Nonnull; + public class SearchDstu2Test { private static CloseableHttpClient ourClient; @@ -556,6 +558,7 @@ public class SearchDstu2Test { return ourReturnPublished; } + @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { throw new IllegalStateException(); diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchHl7OrgDstu2Test.java index b1decee4aec..38e61682465 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchHl7OrgDstu2Test.java @@ -23,6 +23,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import javax.annotation.Nonnull; import java.util.List; import java.util.concurrent.TimeUnit; @@ -120,6 +121,7 @@ public class SearchHl7OrgDstu2Test { return ourReturnPublished; } + @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { throw new IllegalStateException(); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 982eb546ed8..b1fb593b3b7 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -239,6 +239,11 @@ been corrected. Note that at this time, we do not index canonical references at all (as we were previously doing it incorrectly). This will be improved soon. + + IBundleProvider now has an isEmpty() method that can be used to check whether any + results exist. A default implementation has been provided, so this is not + a breaking change. +