diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3131-add-operation-lookup-to-remoteterminologyservice.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3131-add-operation-lookup-to-remoteterminologyservice.yaml new file mode 100644 index 00000000000..338009e842b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3131-add-operation-lookup-to-remoteterminologyservice.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 3131 +title: "Provided a Remote Terminology Service implementation for the $lookup Operation." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml new file mode 100644 index 00000000000..82fdd9c5792 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml @@ -0,0 +1,6 @@ +--- +type: fix +jira: SMILE-3152 +issue: 3138 +title: "Previously, the package registry would not work correctly when externalized binary storage was enabled. This has been corrected." + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/version.yaml new file mode 100644 index 00000000000..c86c3b6785f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2021-11-18" +codename: "Raccoon" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md index 4f770485f44..cfca23c55d4 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md @@ -346,7 +346,7 @@ Sort specifications can be passed into handler methods by adding a parameter of Example URL to invoke this method: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_sort=given +http://fhir.example.com/Patient?identifier=urn:foo|123&_sort=given ``` @@ -364,7 +364,7 @@ of resources fetched from the database. Example URL to invoke this method: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10 ``` # Paging @@ -388,17 +388,17 @@ for more information. Example URL to invoke this method for the first page: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10&_offset=0 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10&_offset=0 ``` or just ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10 ``` Example URL to invoke this method for the second page: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10&_offset=10 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10&_offset=10 ``` Note that if the paging provider is configured to be database backed, `_offset=0` behaves differently than no `_offset`. This diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java index e8f4da57e90..d2f000fea58 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java @@ -20,20 +20,31 @@ package ca.uhn.fhir.jpa.binstore; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException; +import ca.uhn.fhir.util.BinaryUtil; +import ca.uhn.fhir.util.HapiExtensions; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.google.common.hash.HashingInputStream; import com.google.common.io.ByteStreams; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseBinary; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; +import java.io.IOException; import java.io.InputStream; import java.security.SecureRandom; +import java.util.Optional; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { private final SecureRandom myRandom; @@ -41,6 +52,8 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { private final int ID_LENGTH = 100; private int myMaximumBinarySize = Integer.MAX_VALUE; private int myMinimumBinarySize; + @Autowired + private FhirContext myFhirContext; BaseBinaryStorageSvcImpl() { myRandom = new SecureRandom(); @@ -104,7 +117,6 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { }; } - String provideIdForNewBlob(String theBlobIdOrNull) { String id = theBlobIdOrNull; if (isBlank(theBlobIdOrNull)) { @@ -112,4 +124,32 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { } return id; } + + @Override + public byte[] fetchDataBlobFromBinary(IBaseBinary theBaseBinary) throws IOException { + IPrimitiveType dataElement = BinaryUtil.getOrCreateData(myFhirContext, theBaseBinary); + byte[] value = dataElement.getValue(); + if (value == null) { + Optional attachmentId = getAttachmentId((IBaseHasExtensions) dataElement); + if (attachmentId.isPresent()) { + value = fetchBlob(theBaseBinary.getIdElement(), attachmentId.get()); + } else { + throw new InternalErrorException("Unable to load binary blob data for " + theBaseBinary.getIdElement()); + } + } + return value; + } + + @SuppressWarnings("unchecked") + private Optional getAttachmentId(IBaseHasExtensions theBaseBinary) { + return theBaseBinary + .getExtension() + .stream() + .filter(t -> HapiExtensions.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl())) + .filter(t -> t.getValue() instanceof IPrimitiveType) + .map(t -> (IPrimitiveType) t.getValue()) + .map(t -> t.getValue()) + .filter(t -> isNotBlank(t)) + .findFirst(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java index b283d46b918..9100f144d21 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.binstore; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IIdType; import javax.annotation.Nonnull; @@ -101,4 +103,13 @@ public interface IBinaryStorageSvc { * @return The payload as a byte array */ byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException; + + /** + * Fetch the byte[] contents of a given Binary resource's `data` element. If the data is a standard base64encoded string that is embedded, return it. + * Otherwise, attempt to load the externalized binary blob via the the externalized binary storage service. + * + * @param theResourceId The resource ID The ID of the Binary resource you want to extract data bytes from + * @return The binary data blob as a byte array + */ + byte[] fetchDataBlobFromBinary(IBaseBinary theResource) throws IOException; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java index 1d798cd7b26..4cdd7c38f94 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.binstore; * #L% */ +import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IIdType; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -81,4 +83,9 @@ public class NullBinaryStorageSvcImpl implements IBinaryStorageSvc { public byte[] fetchBlob(IIdType theResourceId, String theBlobId) { throw new UnsupportedOperationException(); } + + @Override + public byte[] fetchDataBlobFromBinary(IBaseBinary theResource) throws IOException { + throw new UnsupportedOperationException(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 6a8db13d73d..59790c4197c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; +import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc; import ca.uhn.fhir.jpa.dao.data.INpmPackageDao; import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao; @@ -123,6 +124,9 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac @Autowired private PartitionSettings myPartitionSettings; + @Autowired(required = false)//It is possible that some implementers will not create such a bean. + private IBinaryStorageSvc myBinaryStorageSvc; + @Override @Transactional public NpmPackage loadPackageFromCacheOnly(String theId, @Nullable String theVersion) { @@ -172,13 +176,37 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac private IHapiPackageCacheManager.PackageContents loadPackageContents(NpmPackageVersionEntity thePackageVersion) { IFhirResourceDao binaryDao = getBinaryDao(); IBaseBinary binary = binaryDao.readByPid(new ResourcePersistentId(thePackageVersion.getPackageBinary().getId())); + try { + byte[] content = fetchBlobFromBinary(binary); + PackageContents retVal = new PackageContents() + .setBytes(content) + .setPackageId(thePackageVersion.getPackageId()) + .setVersion(thePackageVersion.getVersionId()) + .setLastModified(thePackageVersion.getUpdatedTime()); + return retVal; + } catch (IOException e) { + throw new InternalErrorException("Failed to load package. There was a problem reading binaries", e); + } + } - PackageContents retVal = new PackageContents() - .setBytes(binary.getContent()) - .setPackageId(thePackageVersion.getPackageId()) - .setVersion(thePackageVersion.getVersionId()) - .setLastModified(thePackageVersion.getUpdatedTime()); - return retVal; + /** + * Helper method which will attempt to use the IBinaryStorageSvc to resolve the binary blob if available. If + * the bean is unavailable, fallback to assuming we are using an embedded base64 in the data element. + * @param theBinary the Binary who's `data` blob you want to retrieve + * @return a byte array containing the blob. + * + * @throws IOException + */ + private byte[] fetchBlobFromBinary(IBaseBinary theBinary) throws IOException { + if (myBinaryStorageSvc != null) { + return myBinaryStorageSvc.fetchDataBlobFromBinary(theBinary); + } else { + byte[] value = BinaryUtil.getOrCreateData(myCtx, theBinary).getValue(); + if (value == null) { + throw new InternalErrorException("Failed to fetch blob from Binary/" + theBinary.getIdElement()); + } + return value; + } } @SuppressWarnings("unchecked") @@ -487,14 +515,12 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac private IBaseResource loadPackageEntity(NpmPackageVersionResourceEntity contents) { try { - - ResourcePersistentId binaryPid = new ResourcePersistentId(contents.getResourceBinary().getId()); - IBaseBinary binary = getBinaryDao().readByPid(binaryPid); - byte[] resourceContentsBytes = BinaryUtil.getOrCreateData(myCtx, binary).getValue(); - String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); - - FhirContext packageContext = getFhirContext(contents.getFhirVersion()); - return EncodingEnum.detectEncoding(resourceContents).newParser(packageContext).parseResource(resourceContents); + ResourcePersistentId binaryPid = new ResourcePersistentId(contents.getResourceBinary().getId()); + IBaseBinary binary = getBinaryDao().readByPid(binaryPid); + byte[] resourceContentsBytes= fetchBlobFromBinary(binary); + String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); + FhirContext packageContext = getFhirContext(contents.getFhirVersion()); + return EncodingEnum.detectEncoding(resourceContents).newParser(packageContext).parseResource(resourceContents); } catch (Exception e) { throw new RuntimeException("Failed to load package resource " + contents, e); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java index 8fa1fac4348..fa6f055c222 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java @@ -172,6 +172,4 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { return requestValidator; } - - } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java index 4f98f112a5c..02f484b19de 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java @@ -1,6 +1,8 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc; +import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil; @@ -70,4 +72,10 @@ public class TestJPAConfig { public BatchJobHelper batchJobHelper(JobExplorer theJobExplorer) { return new BatchJobHelper(theJobExplorer); } + + @Bean + @Lazy + public IBinaryStorageSvc binaryStorage() { + return new MemoryBinaryStorageSvcImpl(); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java index ed387fed101..eada70a648e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java @@ -270,6 +270,78 @@ public class NpmR4Test extends BaseJpaR4Test { }); } + @Test + public void testInstallR4PackageWithExternalizedBinaries() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + myInterceptorService.registerInterceptor(myBinaryStorageInterceptor); + byte[] bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", bytes); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); + assertEquals(1, outcome.getResourcesInstalled().get("CodeSystem")); + + // Be sure no further communication with the server + JettyUtil.closeServer(myServer); + + // Make sure we can fetch the package by ID and Version + NpmPackage pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", "0.12.0"); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", pkg.description()); + + // Make sure we can fetch the package by ID + pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", null); + assertEquals("0.12.0", pkg.version()); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", pkg.description()); + + // Make sure DB rows were saved + runInTransaction(() -> { + NpmPackageEntity pkgEntity = myPackageDao.findByPackageId("hl7.fhir.uv.shorthand").orElseThrow(() -> new IllegalArgumentException()); + assertEquals("hl7.fhir.uv.shorthand", pkgEntity.getPackageId()); + + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals("hl7.fhir.uv.shorthand", versionEntity.getPackageId()); + assertEquals("0.12.0", versionEntity.getVersionId()); + assertEquals(3001, versionEntity.getPackageSizeBytes()); + assertEquals(true, versionEntity.isCurrentVersion()); + assertEquals("hl7.fhir.uv.shorthand", versionEntity.getPackageId()); + assertEquals("4.0.1", versionEntity.getFhirVersionId()); + assertEquals(FhirVersionEnum.R4, versionEntity.getFhirVersion()); + + NpmPackageVersionResourceEntity resource = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand").getContent().get(0); + assertEquals("http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand", resource.getCanonicalUrl()); + assertEquals("0.12.0", resource.getCanonicalVersion()); + assertEquals("ImplementationGuide-hl7.fhir.uv.shorthand.json", resource.getFilename()); + assertEquals("4.0.1", resource.getFhirVersionId()); + assertEquals(FhirVersionEnum.R4, resource.getFhirVersion()); + assertEquals(6155, resource.getResSizeBytes()); + }); + + // Fetch resource by URL + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + assertThat(myFhirCtx.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand\",\"version\":\"0.12.0\"")); + }); + + // Fetch resource by URL with version + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand|0.12.0"); + assertThat(myFhirCtx.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand\",\"version\":\"0.12.0\"")); + }); + + // Search for the installed resource + runInTransaction(() -> { + SearchParameterMap map = SearchParameterMap.newSynchronous(); + map.add(StructureDefinition.SP_URL, new UriParam("http://hl7.org/fhir/uv/shorthand/CodeSystem/shorthand-code-system")); + IBundleProvider result = myCodeSystemDao.search(map); + assertEquals(1, result.sizeOrThrowNpe()); + IBaseResource resource = result.getResources(0, 1).get(0); + assertEquals("CodeSystem/shorthand-code-system/_history/1", resource.getIdElement().toString()); + }); + + myInterceptorService.unregisterInterceptor(myBinaryStorageInterceptor); + } + @Test public void testNumericIdsInstalledWithNpmPrefix() throws Exception { myDaoConfig.setAllowExternalReferences(true); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java index a3988d12323..d3c9becccbf 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java @@ -30,6 +30,7 @@ public class BinaryStorageEntity { @Id @Column(name = "BLOB_ID", length = 200, nullable = false) + //N.B GGG: Note that the `blob id` is the same as the `externalized binary id`. private String myBlobId; @Column(name = "RESOURCE_ID", length = 100, nullable = false) private String myResourceId; diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java index 1bbc78645a9..621a9ddcd0e 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java @@ -21,10 +21,13 @@ package ca.uhn.fhir.jpa.config; */ import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc; +import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.orm.jpa.JpaTransactionManager; @@ -43,6 +46,12 @@ public class TestJpaConfig { return daoConfig().getModelConfig(); } + @Bean + @Lazy + public IBinaryStorageSvc binaryStorage() { + return new MemoryBinaryStorageSvcImpl(); + } + @Bean @Primary public JpaTransactionManager hapiTransactionManager(EntityManagerFactory entityManagerFactory) { diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java index 3ca64e792b3..05db35085e5 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java @@ -135,10 +135,6 @@ public class TestJpaDstu3Config extends BaseJavaConfigDstu3 { return requestValidator; } - @Bean - public IBinaryStorageSvc binaryStorage() { - return new MemoryBinaryStorageSvcImpl(); - } @Bean public DefaultProfileValidationSupport validationSupportChainDstu3() { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java index cd5b7197a51..dd804cc16f5 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java @@ -1,6 +1,7 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; @@ -9,8 +10,8 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.JsonUtil; import ca.uhn.fhir.util.ParametersUtil; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import org.checkerframework.framework.qual.InvisibleQualifier; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -82,7 +83,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup */ private String extractCodeSystemForCode(ValueSet theValueSet, String theCode) { if (theValueSet.getCompose() == null || theValueSet.getCompose().getInclude() == null - || theValueSet.getCompose().getInclude().isEmpty()) { + || theValueSet.getCompose().getInclude().isEmpty()) { return null; } @@ -111,11 +112,9 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup } catch (IOException theE) { ourLog.error("IOException trying to serialize ValueSet to json: " + theE); } - return null; } - private String getVersionedCodeSystem(ValueSet.ConceptSetComponent theComponent) { String codeSystem = theComponent.getSystem(); if ( ! codeSystem.contains("|") && theComponent.hasVersion()) { @@ -124,7 +123,6 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup return codeSystem; } - @Override public IBaseResource fetchCodeSystem(String theSystem) { IGenericClient client = provideClient(); @@ -143,6 +141,167 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup return null; } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) { + Validate.notBlank(theCode, "theCode must be provided"); + + IGenericClient client = provideClient(); + FhirContext fhirContext = client.getFhirContext(); + FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion(); + + switch (fhirVersion) { + case DSTU3: + case R4: + IBaseParameters params = ParametersUtil.newInstance(fhirContext); + ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode); + if (!StringUtils.isEmpty(theSystem)) { + ParametersUtil.addParameterToParametersString(fhirContext, params, "system", theSystem); + } + if (!StringUtils.isEmpty(theDisplayLanguage)) { + ParametersUtil.addParameterToParametersString(fhirContext, params, "language", theDisplayLanguage); + } + Class codeSystemClass = myCtx.getResourceDefinition("CodeSystem").getImplementingClass(); + IBaseParameters outcome = client + .operation() + .onType((Class) codeSystemClass) + .named("$lookup") + .withParameters(params) + .useHttpGet() + .execute(); + if (outcome != null && !outcome.isEmpty()) { + switch (fhirVersion) { + case DSTU3: + return generateLookupCodeResultDSTU3(theCode, theSystem, (org.hl7.fhir.dstu3.model.Parameters)outcome); + case R4: + return generateLookupCodeResultR4(theCode, theSystem, (org.hl7.fhir.r4.model.Parameters)outcome); + } + } + break; + default: + throw new UnsupportedOperationException("Unsupported FHIR version '" + fhirVersion.getFhirVersionString() + + "'. Only DSTU3 and R4 are supported."); + } + return null; + } + + private LookupCodeResult generateLookupCodeResultDSTU3(String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) { + // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding + // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM). + LookupCodeResult result = new LookupCodeResult(); + result.setSearchedForCode(theCode); + result.setSearchedForSystem(theSystem); + result.setFound(true); + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent : outcomeDSTU3.getParameter()) { + switch (parameterComponent.getName()) { + case "property": + org.hl7.fhir.dstu3.model.Property part = parameterComponent.getChildByName("part"); + // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved + if (part != null && part.hasValues() && part.getValues().size() >= 2) { + String key = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString(); + String value = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString(); + if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) { + result.getProperties().add(new StringConceptProperty(key, value)); + } + } + break; + case "designation": + ConceptDesignation conceptDesignation = new ConceptDesignation(); + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) { + switch(designationComponent.getName()) { + case "language": + conceptDesignation.setLanguage(designationComponent.getValue().toString()); + break; + case "use": + org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding)designationComponent.getValue(); + if (coding != null) { + conceptDesignation.setUseSystem(coding.getSystem()); + conceptDesignation.setUseCode(coding.getCode()); + conceptDesignation.setUseDisplay(coding.getDisplay()); + } + break; + case "value": + conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString())); + break; + } + } + result.getDesignations().add(conceptDesignation); + break; + case "name": + result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "version": + result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "display": + result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "abstract": + result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString()))); + break; + } + } + return result; + } + + private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, org.hl7.fhir.r4.model.Parameters outcomeR4) { + // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding + // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM). + LookupCodeResult result = new LookupCodeResult(); + result.setSearchedForCode(theCode); + result.setSearchedForSystem(theSystem); + result.setFound(true); + for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) { + switch (parameterComponent.getName()) { + case "property": + org.hl7.fhir.r4.model.Property part = parameterComponent.getChildByName("part"); + // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved + if (part != null && part.hasValues() && part.getValues().size() >= 2) { + String key = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString(); + String value = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString(); + if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) { + result.getProperties().add(new StringConceptProperty(key, value)); + } + } + break; + case "designation": + ConceptDesignation conceptDesignation = new ConceptDesignation(); + for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) { + switch(designationComponent.getName()) { + case "language": + conceptDesignation.setLanguage(designationComponent.getValue().toString()); + break; + case "use": + org.hl7.fhir.r4.model.Coding coding = (org.hl7.fhir.r4.model.Coding)designationComponent.getValue(); + if (coding != null) { + conceptDesignation.setUseSystem(coding.getSystem()); + conceptDesignation.setUseCode(coding.getCode()); + conceptDesignation.setUseDisplay(coding.getDisplay()); + } + break; + case "value": + conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString())); + break; + } + } + result.getDesignations().add(conceptDesignation); + break; + case "name": + result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "version": + result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "display": + result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "abstract": + result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString()))); + break; + } + } + return result; + } + @Override public IBaseResource fetchValueSet(String theValueSetUrl) { IGenericClient client = provideClient(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java index 68b743c3183..74b2568f20c 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java @@ -3,12 +3,14 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.parser.IJsonLikeParser; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.client.api.IClientInterceptor; import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpResponse; @@ -22,12 +24,14 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -44,14 +48,18 @@ import static org.hamcrest.Matchers.lessThan; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class RemoteTerminologyServiceValidationSupportTest { - private static final String DISPLAY = "DISPLAY"; + private static final String LANGUAGE = "en"; private static final String CODE_SYSTEM = "CODE_SYS"; + private static final String CODE_SYSTEM_VERSION = "2.1"; + private static final String CODE_SYSTEM_VERSION_AS_TEXT = "v2.1.12"; private static final String CODE = "CODE"; private static final String VALUE_SET_URL = "http://value.set/url"; private static final String ERROR_MESSAGE = "This is an error message"; + private static FhirContext ourCtx = FhirContext.forR4(); @RegisterExtension @@ -88,7 +96,50 @@ public class RemoteTerminologyServiceValidationSupportTest { } @Test - public void testValidateCode_SystemCodeDisplayUrl_Success() { + public void testLookupOperation_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, + DISPLAY, null); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, myCodeSystemProvider.myNextReturnParams.getParameter("name").toString()); + assertTrue(Boolean.parseBoolean(myCodeSystemProvider.myNextReturnParams.getParameter("result").primitiveValue())); + } + + @Test + public void testLookupOperationWithAllParams_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, + DISPLAY, null); + addAdditionalReturnParameters(); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, myCodeSystemProvider.myNextReturnParams.getParameter("name").toString()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + assertTrue(Boolean.parseBoolean(myCodeSystemProvider.myNextReturnParams.getParameter("result").primitiveValue())); + + validateExtraCodeSystemParams(); + } + + @Test + public void testLookupCode_BlankCode_ThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, + "", null); + }); + } + + @Test + public void testValidateCode_ValueSet_Success() { createNextValueSetReturnParameters(true, DISPLAY, null); IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, VALUE_SET_URL); @@ -104,6 +155,62 @@ public class RemoteTerminologyServiceValidationSupportTest { assertEquals(null, myValueSetProvider.myLastValueSet); } + @Test + public void testValidateCodeWithAllParams_CodeSystem_Success() { + createNextCodeSystemReturnParameters(true, DISPLAY, null); + addAdditionalReturnParameters(); + + IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, null); + assertEquals(CODE, outcome.getCode()); + assertEquals(DISPLAY, outcome.getDisplay()); + assertEquals(null, outcome.getSeverity()); + assertEquals(null, outcome.getMessage()); + + validateExtraCodeSystemParams(); + } + + private void validateExtraCodeSystemParams() { + assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + for (Parameters.ParametersParameterComponent param : myCodeSystemProvider.myNextReturnParams.getParameter()) { + String paramName = param.getName(); + if (paramName.equals("result")) { + assertEquals(true, ((BooleanType)param.getValue()).booleanValue()); + } else if (paramName.equals("display")) { + assertEquals(DISPLAY, param.getValue().toString()); + } else if (paramName.equals("property")) { + for (Parameters.ParametersParameterComponent propertyComponent : param.getPart()) { + switch(propertyComponent.getName()) { + case "name": + assertEquals("birthDate", propertyComponent.getValue().toString()); + break; + case "value": + assertEquals("1930-01-01", propertyComponent.getValue().toString()); + break; + } + } + } else if (paramName.equals("designation")) { + for (Parameters.ParametersParameterComponent designationComponent : param.getPart()) { + switch(designationComponent.getName()) { + case "language": + assertEquals(LANGUAGE, designationComponent.getValue().toString()); + break; + case "use": + Coding coding = (Coding)designationComponent.getValue(); + assertNotNull(coding, "Coding value returned via designation use should NOT be NULL!"); + assertEquals("code", coding.getCode()); + assertEquals("system", coding.getSystem()); + assertEquals("display", coding.getDisplay()); + break; + case "value": + assertEquals("some value", designationComponent.getValue().toString()); + break; + } + } + } + } + } + @Test public void testValidateCode_SystemCodeDisplayUrl_Error() { createNextValueSetReturnParameters(false, null, ERROR_MESSAGE); @@ -132,7 +239,6 @@ public class RemoteTerminologyServiceValidationSupportTest { assertEquals(null, outcome.getMessage()); assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); - assertEquals(DISPLAY, myCodeSystemProvider.myLastDisplay.getValue()); assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); } @@ -375,6 +481,18 @@ public class RemoteTerminologyServiceValidationSupportTest { } } + private void createNextCodeSystemLookupReturnParameters(boolean theResult, String theVersion, String theVersionAsText, + String theDisplay, String theMessage) { + myCodeSystemProvider.myNextReturnParams = new Parameters(); + myCodeSystemProvider.myNextReturnParams.addParameter("result", theResult); + myCodeSystemProvider.myNextReturnParams.addParameter("version", theVersion); + myCodeSystemProvider.myNextReturnParams.addParameter("name", theVersionAsText); + myCodeSystemProvider.myNextReturnParams.addParameter("display", theDisplay); + if (theMessage != null) { + myCodeSystemProvider.myNextReturnParams.addParameter("message", theMessage); + } + } + private void createNextValueSetReturnParameters(boolean theResult, String theDisplay, String theMessage) { myValueSetProvider.myNextReturnParams = new Parameters(); myValueSetProvider.myNextReturnParams.addParameter("result", theResult); @@ -384,6 +502,23 @@ public class RemoteTerminologyServiceValidationSupportTest { } } + private void addAdditionalReturnParameters() { + // property + Parameters.ParametersParameterComponent param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("property"); + param.addPart().setName("name").setValue(new StringType("birthDate")); + param.addPart().setName("value").setValue(new StringType("1930-01-01")); + // designation + param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("designation"); + param.addPart().setName("language").setValue(new CodeType("en")); + Parameters.ParametersParameterComponent codingParam = param.addPart().setName("use"); + Coding coding = new Coding(); + coding.setCode("code"); + coding.setSystem("system"); + coding.setDisplay("display"); + codingParam.setValue(coding); + param.addPart().setName("value").setValue(new StringType("some value")); + } + private static class MyCodeSystemProvider implements IResourceProvider { private UriParam myLastUrlParam; @@ -391,8 +526,10 @@ public class RemoteTerminologyServiceValidationSupportTest { private int myInvocationCount; private UriType myLastUrl; private CodeType myLastCode; - private StringType myLastDisplay; + private Coding myLastCoding; + private StringType myLastVersion; private Parameters myNextReturnParams; + private IValidationSupport.LookupCodeResult myNextLookupCodeResult; @Operation(name = "validate-code", idempotent = true, returnParameters = { @OperationParam(name = "result", type = BooleanType.class, min = 1), @@ -409,11 +546,34 @@ public class RemoteTerminologyServiceValidationSupportTest { myInvocationCount++; myLastUrl = theCodeSystemUrl; myLastCode = theCode; - myLastDisplay = theDisplay; return myNextReturnParams; } + @Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= { + @OperationParam(name="name", type=StringType.class, min=1), + @OperationParam(name="version", type=StringType.class, min=0), + @OperationParam(name="display", type=StringType.class, min=1), + @OperationParam(name="abstract", type=BooleanType.class, min=1), + }) + public Parameters lookup( + HttpServletRequest theServletRequest, + @OperationParam(name="code", min=0, max=1) CodeType theCode, + @OperationParam(name="system", min=0, max=1) UriType theSystem, + @OperationParam(name="coding", min=0, max=1) Coding theCoding, + @OperationParam(name="version", min=0, max=1) StringType theVersion, + @OperationParam(name="displayLanguage", min=0, max=1) CodeType theDisplayLanguage, + @OperationParam(name="property", min = 0, max = OperationParam.MAX_UNLIMITED) List theProperties, + RequestDetails theRequestDetails + ) { + myInvocationCount++; + myLastCode = theCode; + myLastUrl = theSystem; + myLastCoding = theCoding; + myLastVersion = theVersion; + return myNextReturnParams; + } + @Search public List find(@RequiredParam(name = "url") UriParam theUrlParam) { myLastUrlParam = theUrlParam; @@ -429,8 +589,6 @@ public class RemoteTerminologyServiceValidationSupportTest { private static class MyValueSetProvider implements IResourceProvider { - - private Parameters myNextReturnParams; private List myNextReturnValueSets; private UriType myLastUrl; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyServiceValidationSupportDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyServiceValidationSupportDstu3Test.java new file mode 100644 index 00000000000..9d435406dd8 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyServiceValidationSupportDstu3Test.java @@ -0,0 +1,214 @@ +package org.hl7.fhir.dstu3.hapi.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.DateType; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.UriType; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class RemoteTerminologyServiceValidationSupportDstu3Test { + private static final String DISPLAY = "DISPLAY"; + private static final String LANGUAGE = "en"; + private static final String CODE_SYSTEM = "CODE_SYS"; + private static final String CODE_SYSTEM_VERSION = "2.1"; + private static final String CODE_SYSTEM_VERSION_AS_TEXT = "v2.1.12"; + private static final String CODE = "CODE"; + + private static FhirContext ourCtx = FhirContext.forDstu3(); + + @RegisterExtension + public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx); + + private RemoteTerminologyServiceValidationSupport mySvc; + private MyCodeSystemProvider myCodeSystemProvider; + + @BeforeEach + public void before() { + myCodeSystemProvider = new MyCodeSystemProvider(); + myRestfulServerExtension.getRestfulServer().registerProvider(myCodeSystemProvider); + String baseUrl = "http://localhost:" + myRestfulServerExtension.getPort(); + mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx); + mySvc.setBaseUrl(baseUrl); + mySvc.addClientInterceptor(new LoggingInterceptor(true)); + } + + @Test + public void testLookupOperation_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, DISPLAY); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.asStringValue()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + for (Parameters.ParametersParameterComponent param : myCodeSystemProvider.myNextReturnParams.getParameter()) { + String paramName = param.getName(); + if (paramName.equals("result")) { + assertEquals(true, ((BooleanType)param.getValue()).booleanValue()); + } else if (paramName.equals("version")) { + assertEquals(CODE_SYSTEM_VERSION, param.getValue().toString()); + } else if (paramName.equals("display")) { + assertEquals(DISPLAY, param.getValue().toString()); + } else if (paramName.equals("name")) { + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, param.getValue().toString()); + } + } + } + + @Test + public void testLookupOperationWithAllParams_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, DISPLAY, LANGUAGE); + addAdditionalCodeSystemLookupReturnParameters(); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE, LANGUAGE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.asStringValue()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + for (Parameters.ParametersParameterComponent param : myCodeSystemProvider.myNextReturnParams.getParameter()) { + String paramName = param.getName(); + if (paramName.equals("result")) { + assertEquals(true, ((BooleanType)param.getValue()).booleanValue()); + } else if (paramName.equals("version")) { + assertEquals(CODE_SYSTEM_VERSION, param.getValue().toString()); + } else if (paramName.equals("display")) { + assertEquals(DISPLAY, param.getValue().toString()); + } else if (paramName.equals("name")) { + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, param.getValue().toString()); + } else if (paramName.equals("language")) { + assertEquals(LANGUAGE, param.getValue().toString()); + } else if (paramName.equals("property")) { + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent propertyComponent : param.getPart()) { + switch(propertyComponent.getName()) { + case "name": + assertEquals("birthDate", propertyComponent.getValue().toString()); + break; + case "value": + assertEquals("1930-01-01", ((DateType)propertyComponent.getValue()).asStringValue()); + break; + } + } + } else if (paramName.equals("designation")) { + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : param.getPart()) { + switch(designationComponent.getName()) { + case "language": + assertEquals(LANGUAGE, designationComponent.getValue().toString()); + break; + case "use": + Coding coding = (Coding)designationComponent.getValue(); + assertNotNull(coding, "Coding value returned via designation use should NOT be NULL!"); + assertEquals("code", coding.getCode()); + assertEquals("system", coding.getSystem()); + assertEquals("display", coding.getDisplay()); + break; + case "value": + assertEquals("some value", designationComponent.getValue().toString()); + break; + } + } + } + } + } + + private void createNextCodeSystemLookupReturnParameters(boolean theResult, String theVersion, String theVersionAsText, + String theDisplay) { + createNextCodeSystemLookupReturnParameters(theResult, theVersion, theVersionAsText, theDisplay, null); + } + + private void createNextCodeSystemLookupReturnParameters(boolean theResult, String theVersion, String theVersionAsText, + String theDisplay, String theLanguage) { + myCodeSystemProvider.myNextReturnParams = new Parameters(); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("result").setValue(new BooleanType(theResult)); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("version").setValue(new StringType(theVersion)); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("name").setValue(new StringType(theVersionAsText)); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("display").setValue(new StringType(theDisplay)); + if (!StringUtils.isBlank(theLanguage)) { + myCodeSystemProvider.myNextReturnParams.addParameter().setName("language").setValue(new StringType(theLanguage)); + } + } + + private void addAdditionalCodeSystemLookupReturnParameters() { + // property + Parameters.ParametersParameterComponent param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("property"); + param.addPart().setName("name").setValue(new StringType("birthDate")); + param.addPart().setName("value").setValue(new DateType("1930-01-01")); + // designation + param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("designation"); + param.addPart().setName("language").setValue(new CodeType("en")); + Parameters.ParametersParameterComponent codingParam = param.addPart().setName("use"); + Coding coding = new Coding(); + coding.setCode("code"); + coding.setSystem("system"); + coding.setDisplay("display"); + codingParam.setValue(coding); + param.addPart().setName("value").setValue(new StringType("some value")); + } + + private static class MyCodeSystemProvider implements IResourceProvider { + private int myInvocationCount; + private UriType myLastUrl; + private CodeType myLastCode; + private Coding myLastCoding; + private StringType myLastVersion; + private CodeType myLastDisplayLanguage; + private Parameters myNextReturnParams; + + @Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= { + @OperationParam(name="name", type=StringType.class, min=1), + @OperationParam(name="version", type=StringType.class, min=0), + @OperationParam(name="display", type=StringType.class, min=1), + @OperationParam(name="abstract", type=BooleanType.class, min=1), + }) + public Parameters lookup( + HttpServletRequest theServletRequest, + @OperationParam(name="code", min=0, max=1) CodeType theCode, + @OperationParam(name="system", min=0, max=1) UriType theSystem, + @OperationParam(name="coding", min=0, max=1) Coding theCoding, + @OperationParam(name="version", min=0, max=1) StringType theVersion, + @OperationParam(name="displayLanguage", min=0, max=1) CodeType theDisplayLanguage, + @OperationParam(name="property", min = 0, max = OperationParam.MAX_UNLIMITED) List theProperties, + RequestDetails theRequestDetails + ) { + myInvocationCount++; + myLastCode = theCode; + myLastUrl = theSystem; + myLastCoding = theCoding; + myLastVersion = theVersion; + myLastDisplayLanguage = theDisplayLanguage; + return myNextReturnParams; + } + + @Override + public Class getResourceType() { + return CodeSystem.class; + } + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/RemoteTerminologyServiceValidationSupportR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/RemoteTerminologyServiceValidationSupportR5Test.java new file mode 100644 index 00000000000..78362aaabb1 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/RemoteTerminologyServiceValidationSupportR5Test.java @@ -0,0 +1,35 @@ +package org.hl7.fhir.r5.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RemoteTerminologyServiceValidationSupportR5Test { + private static final String ANY_NONBLANK_VALUE = "anything"; + private static FhirContext ourCtx = FhirContext.forR5(); + @RegisterExtension + public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx); + + private RemoteTerminologyServiceValidationSupport mySvc; + + @BeforeEach + public void before() { + String baseUrl = "http://localhost:" + myRestfulServerExtension.getPort(); + mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx); + mySvc.setBaseUrl(baseUrl); + } + + @Test + public void testLookupCode_R5_ThrowsException() { + Assertions.assertThrows(UnsupportedOperationException.class, () -> { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode( + new ValidationSupportContext(FhirContext.forR5().getValidationSupport()), ANY_NONBLANK_VALUE, ANY_NONBLANK_VALUE); + }); + } +}