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 extends IBaseBinary> 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 extends IBaseResource>) 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 extends IBaseResource> 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);
+ });
+ }
+}