diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..74c342f --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,24 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/README.md b/README.md index 13d8fe4..b2f1e2e 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,20 @@ Again, browse to the following link to use the server (note that the port 8080 m [http://localhost:8080/](http://localhost:8080/) -If you would like it to be hosted at eg. hapi-fhir-jpaserver, eg. http://localhost:8080/hapi-fhir-jpaserver/ - then rename the WAR file to ```hapi-fhir-jpaserver.war```. +You will then be able access the JPA server e.g. using http://localhost:8080/fhir/metadata. + +If you would like it to be hosted at eg. hapi-fhir-jpaserver, eg. http://localhost:8080/hapi-fhir-jpaserver/ or http://localhost:8080/hapi-fhir-jpaserver/fhir/metadata - then rename the WAR file to ```hapi-fhir-jpaserver.war``` and adjust the overlay configuration accordingly e.g. + +```yaml + tester: + - + id: home + name: Local Tester + server_address: 'http://localhost:8080/hapi-fhir-jpaserver/fhir' + refuse_to_fetch_third_party_urls: false + fhir_version: R4 +``` + ## Deploy with docker compose @@ -321,3 +334,28 @@ elasticsearch.password=SomePassword elasticsearch.required_index_status=YELLOW elasticsearch.schema_management_strategy=CREATE ``` + +## Enabling LastN + +Set `hapi.fhir.lastn_enabled=true` in the [application.yaml](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml) file to enable the $lastn operation on this server. Note that the $lastn operation relies on Elasticsearch, so for $lastn to work, indexing must be enabled using Elasticsearch. + +## Example of a Dockerfile based on distroless images (for lower footprint and improved security) + +```code +FROM maven:3.6.3-jdk-11-slim as build-hapi +WORKDIR /tmp/hapi-fhir-jpaserver-starter + +COPY pom.xml . +RUN mvn -ntp dependency:go-offline + +COPY src/ /tmp/hapi-fhir-jpaserver-starter/src/ +RUN mvn clean package spring-boot:repackage -Pboot + +FROM gcr.io/distroless/java:11 + +COPY --from=build-hapi /tmp/hapi-fhir-jpaserver-starter/target/ROOT.war /app/main.war + +EXPOSE 8080 +WORKDIR /app +CMD ["main.war"] +``` diff --git a/pom.xml b/pom.xml index 432f5ed..f1d04ff 100644 --- a/pom.xml +++ b/pom.xml @@ -184,37 +184,30 @@ org.webjars Eonasdan-bootstrap-datetimepicker - 4.17.43 org.webjars font-awesome - 5.8.2 org.webjars.bower awesome-bootstrap-checkbox - 1.0.1 org.webjars jstimezonedetect - 1.0.6 org.webjars select2 - 4.0.3 org.webjars.bower jquery - 3.3.1 org.webjars.bower moment - 2.15.1 diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java index ed8b479..064ee13 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java @@ -7,8 +7,10 @@ import ca.uhn.fhir.rest.api.EncodingEnum; import java.util.ArrayList; import java.util.List; +import java.util.Map; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.hl7.fhir.r4.model.Bundle; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -56,12 +58,14 @@ public class AppProperties { private Boolean narrative_enabled = true; private Validation validation = new Validation(); - private List tester = ImmutableList.of(new Tester()); + private Map tester = ImmutableMap.of("home", new Tester()); private Logger logger = new Logger(); private Subscription subscription = new Subscription(); private Cors cors = null; private Partitioning partitioning = null; - private List implementationGuides = null; + private Map implementationGuides = null; + + private Boolean lastn_enabled = false; public Integer getDefer_indexing_for_codesystems_of_size() { return defer_indexing_for_codesystems_of_size; @@ -71,11 +75,11 @@ public class AppProperties { this.defer_indexing_for_codesystems_of_size = defer_indexing_for_codesystems_of_size; } - public List getImplementationGuides() { + public Map getImplementationGuides() { return implementationGuides; } - public void setImplementationGuides(List implementationGuides) { + public void setImplementationGuides(Map implementationGuides) { this.implementationGuides = implementationGuides; } @@ -372,11 +376,11 @@ public class AppProperties { this.reuse_cached_search_results_millis = reuse_cached_search_results_millis; } - public List getTester() { + public Map getTester() { return tester; } - public void setTester(List tester) { + public void setTester(Map tester) { this.tester = tester; } @@ -390,6 +394,14 @@ public class AppProperties { this.narrative_enabled = narrative_enabled; } + public Boolean getLastn_enabled() { + return lastn_enabled; + } + + public void setLastn_enabled(Boolean lastn_enabled) { + this.lastn_enabled = lastn_enabled; + } + public static class Cors { private Boolean allow_Credentials = true; private List allowed_origin = ImmutableList.of("*"); @@ -456,7 +468,6 @@ public class AppProperties { public static class Tester { - private String id = "home"; private String name = "Local Tester"; private String server_address = "http://localhost:8080/fhir"; private Boolean refuse_to_fetch_third_party_urls = true; @@ -470,14 +481,6 @@ public class AppProperties { this.fhir_version = fhir_version; } - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - public String getName() { return name; } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java b/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java index ad0ceda..66ec107 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java @@ -42,10 +42,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.web.cors.CorsConfiguration; import javax.servlet.ServletException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; public class BaseJpaRestfulServer extends RestfulServer { @@ -357,15 +354,21 @@ public class BaseJpaRestfulServer extends RestfulServer { } if (appProperties.getImplementationGuides() != null) { - List guides = appProperties.getImplementationGuides(); - for (AppProperties.ImplementationGuide guide : guides) { + Map guides = appProperties.getImplementationGuides(); + for (Map.Entry guide : guides.entrySet()) { packageInstallerSvc.install(new PackageInstallationSpec() - .setPackageUrl(guide.getUrl()) - .setName(guide.getName()) - .setVersion(guide.getVersion()) + .setPackageUrl(guide.getValue().getUrl()) + .setName(guide.getValue().getName()) + .setVersion(guide.getValue().getVersion()) .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL)); } } + + if (appProperties.getLastn_enabled()) { + daoConfig.setLastNEnabled(true); + } + + } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/EnvironmentHelper.java b/src/main/java/ca/uhn/fhir/jpa/starter/EnvironmentHelper.java index 4dbc122..6dd8311 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/EnvironmentHelper.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/EnvironmentHelper.java @@ -1,5 +1,8 @@ package ca.uhn.fhir.jpa.starter; +import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder; +import org.hibernate.search.elasticsearch.cfg.ElasticsearchIndexStatus; +import org.hibernate.search.elasticsearch.cfg.IndexSchemaManagementStrategy; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; @@ -35,9 +38,66 @@ public class EnvironmentHelper { properties.put(values[0], values[1]); }); } + + if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null + && environment.getProperty("elasticsearch.enabled", Boolean.class) == true ){ + ElasticsearchHibernatePropertiesBuilder builder = new ElasticsearchHibernatePropertiesBuilder(); + ElasticsearchIndexStatus requiredIndexStatus = environment.getProperty("elasticsearch.required_index_status", ElasticsearchIndexStatus.class); + if (requiredIndexStatus == null) { + builder.setRequiredIndexStatus(ElasticsearchIndexStatus.YELLOW); + } else { + builder.setRequiredIndexStatus(requiredIndexStatus); + } + + builder.setRestUrl(getElasticsearchServerUrl(environment)); + builder.setUsername(getElasticsearchServerUsername(environment)); + builder.setPassword(getElasticsearchServerPassword(environment)); + IndexSchemaManagementStrategy indexSchemaManagementStrategy = environment.getProperty("elasticsearch.schema_management_strategy", IndexSchemaManagementStrategy.class); + if (indexSchemaManagementStrategy == null) { + builder.setIndexSchemaManagementStrategy(IndexSchemaManagementStrategy.CREATE); + } else { + builder.setIndexSchemaManagementStrategy(indexSchemaManagementStrategy); + } + // pretty_print_json_log: false + Boolean refreshAfterWrite = environment.getProperty("elasticsearch.debug.refresh_after_write", Boolean.class); + if (refreshAfterWrite == null) { + builder.setDebugRefreshAfterWrite(false); + } else { + builder.setDebugRefreshAfterWrite(refreshAfterWrite); + } + // pretty_print_json_log: false + Boolean prettyPrintJsonLog = environment.getProperty("elasticsearch.debug.pretty_print_json_log", Boolean.class); + if (prettyPrintJsonLog == null) { + builder.setDebugPrettyPrintJsonLog(false); + } else { + builder.setDebugPrettyPrintJsonLog(prettyPrintJsonLog); + } + builder.apply(properties); + } + return properties; } + public static String getElasticsearchServerUrl(ConfigurableEnvironment environment) { + return environment.getProperty("elasticsearch.rest_url", String.class); + } + + public static String getElasticsearchServerUsername(ConfigurableEnvironment environment) { + return environment.getProperty("elasticsearch.username"); + } + + public static String getElasticsearchServerPassword(ConfigurableEnvironment environment) { + return environment.getProperty("elasticsearch.password"); + } + + public static Boolean isElasticsearchEnabled(ConfigurableEnvironment environment) { + if (environment.getProperty("elasticsearch.enabled", Boolean.class) != null) { + return environment.getProperty("elasticsearch.enabled", Boolean.class); + } else { + return false; + } + } + public static Map getPropertiesStartingWith(ConfigurableEnvironment aEnv, String aKeyPrefix) { Map result = new HashMap<>(); diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigDstu3.java b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigDstu3.java index 02d51ea..07406b2 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigDstu3.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigDstu3.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.starter; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl; import ca.uhn.fhir.jpa.starter.annotations.OnDSTU3Condition; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; @@ -63,4 +64,18 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 { return retVal; } + @Bean() + public ElasticsearchSvcImpl elasticsearchSvc() { + if (EnvironmentHelper.isElasticsearchEnabled(configurableEnvironment)) { + String elasticsearchUrl = EnvironmentHelper.getElasticsearchServerUrl(configurableEnvironment); + String elasticsearchHost = elasticsearchUrl.substring(elasticsearchUrl.indexOf("://")+3, elasticsearchUrl.lastIndexOf(":")); + String elasticsearchUsername = EnvironmentHelper.getElasticsearchServerUsername(configurableEnvironment); + String elasticsearchPassword = EnvironmentHelper.getElasticsearchServerPassword(configurableEnvironment); + int elasticsearchPort = Integer.parseInt(elasticsearchUrl.substring(elasticsearchUrl.lastIndexOf(":")+1)); + return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchPort, elasticsearchUsername, elasticsearchPassword); + } else { + return null; + } + } + } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java index 3049a86..4711030 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR4.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.starter; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.jpa.config.BaseJavaConfigR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl; import ca.uhn.fhir.jpa.starter.annotations.OnR4Condition; import ca.uhn.fhir.jpa.starter.cql.StarterCqlR4Config; import org.springframework.beans.factory.annotation.Autowired; @@ -65,4 +66,18 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 { return retVal; } + @Bean() + public ElasticsearchSvcImpl elasticsearchSvc() { + if (EnvironmentHelper.isElasticsearchEnabled(configurableEnvironment)) { + String elasticsearchUrl = EnvironmentHelper.getElasticsearchServerUrl(configurableEnvironment); + String elasticsearchHost = elasticsearchUrl.substring(elasticsearchUrl.indexOf("://")+3, elasticsearchUrl.lastIndexOf(":")); + String elasticsearchUsername = EnvironmentHelper.getElasticsearchServerUsername(configurableEnvironment); + String elasticsearchPassword = EnvironmentHelper.getElasticsearchServerPassword(configurableEnvironment); + int elasticsearchPort = Integer.parseInt(elasticsearchUrl.substring(elasticsearchUrl.lastIndexOf(":")+1)); + return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchPort, elasticsearchUsername, elasticsearchPassword); + } else { + return null; + } + } + } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR5.java b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR5.java index 28e5ef5..6e5bd98 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR5.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigR5.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.starter; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.jpa.config.BaseJavaConfigR5; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl; import ca.uhn.fhir.jpa.starter.annotations.OnR5Condition; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -66,4 +67,19 @@ public class FhirServerConfigR5 extends BaseJavaConfigR5 { return retVal; } + @Bean() + public ElasticsearchSvcImpl elasticsearchSvc() { + if (EnvironmentHelper.isElasticsearchEnabled(configurableEnvironment)) { + String elasticsearchUrl = EnvironmentHelper.getElasticsearchServerUrl(configurableEnvironment); + String elasticsearchHost = elasticsearchUrl.substring(elasticsearchUrl.indexOf("://")+3, elasticsearchUrl.lastIndexOf(":")); + String elasticsearchUsername = EnvironmentHelper.getElasticsearchServerUsername(configurableEnvironment); + String elasticsearchPassword = EnvironmentHelper.getElasticsearchServerPassword(configurableEnvironment); + int elasticsearchPort = Integer.parseInt(elasticsearchUrl.substring(elasticsearchUrl.lastIndexOf(":")+1)); + return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchPort, elasticsearchUsername, elasticsearchPassword); + } else { + return null; + } + } + + } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java index 45a7808..7adf8bf 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/FhirTesterConfig.java @@ -36,15 +36,15 @@ public class FhirTesterConfig { @Bean public TesterConfig testerConfig(AppProperties appProperties) { TesterConfig retVal = new TesterConfig(); - appProperties.getTester().stream().forEach(t -> { + appProperties.getTester().entrySet().stream().forEach(t -> { retVal .addServer() - .withId(t.getId()) - .withFhirVersion(t.getFhir_version()) - .withBaseUrl(t.getServer_address()) - .withName(t.getName()); + .withId(t.getKey()) + .withFhirVersion(t.getValue().getFhir_version()) + .withBaseUrl(t.getValue().getServer_address()) + .withName(t.getValue().getName()); retVal.setRefuseToFetchThirdPartyUrls( - t.getRefuse_to_fetch_third_party_urls()); + t.getValue().getRefuse_to_fetch_third_party_urls()); }); return retVal; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 70e1add..f70c8e0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,6 +1,7 @@ spring: datasource: url: 'jdbc:h2:file:./target/database/h2' + #url: jdbc:h2:mem:test_mem username: sa password: null driverClassName: org.h2.Driver @@ -32,14 +33,16 @@ hapi: ### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5 fhir_version: R4 # defer_indexing_for_codesystems_of_size: 101 -# implementationguides: -# - -# url: https://build.fhir.org/ig/hl7dk/dk-medcom/branches/corrections/package.tgz -# name: dk.fhir.ig.medcom-core -# version: 0.8.0 -# - -# name: hl7.fhir.uv.ips -# version: 0.3.0 + #implementationguides: + #example from registry (packages.fhir.org) + #swiss: + #name: swiss.mednet.fhir + #version: 0.8.0 + #example not from registry + #ips_1_0_0: + #url: https://build.fhir.org/ig/HL7/fhir-ips/package.tgz + #name: hl7.fhir.uv.ips + #version: 1.0.0 #supported_resource_types: # - Patient @@ -91,14 +94,14 @@ hapi: # retain_cached_searches_mins: 60 # reuse_cached_search_results_millis: 60000 tester: - - - id: home + + home: name: Local Tester server_address: 'http://localhost:8080/fhir' refuse_to_fetch_third_party_urls: false fhir_version: R4 - - - id: global + + global: name: Global Tester server_address: "http://hapi.fhir.org/baseR4" refuse_to_fetch_third_party_urls: false @@ -122,6 +125,7 @@ hapi: # startTlsEnable: # startTlsRequired: # quitWait: +# lastn_enabled: true # diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java new file mode 100644 index 0000000..c3fb7f7 --- /dev/null +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java @@ -0,0 +1,144 @@ +package ca.uhn.fhir.jpa.starter; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import pl.allegro.tech.embeddedelasticsearch.EmbeddedElastic; +import pl.allegro.tech.embeddedelasticsearch.PopularProperties; + +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class, properties = + { + "spring.batch.job.enabled=false", + "spring.datasource.url=jdbc:h2:mem:dbr4", + "hapi.fhir.fhir_version=r4", + "hapi.fhir.lastn_enabled=true", + "elasticsearch.enabled=true", + // Because the port is set randomly, we will set the rest_url using the Initializer. + // "elasticsearch.rest_url='http://localhost:9200'", + "elasticsearch.username=SomeUsername", + "elasticsearch.password=SomePassword" + }) +@ContextConfiguration(initializers = ElasticsearchLastNR4IT.Initializer.class) +public class ElasticsearchLastNR4IT { + + private IGenericClient ourClient; + private FhirContext ourCtx; + + private static final String ELASTIC_VERSION = "6.5.4"; + private static EmbeddedElastic embeddedElastic; + + @Autowired + private ElasticsearchSvcImpl myElasticsearchSvc; + + @BeforeAll + public static void beforeClass() { + + embeddedElastic = null; + try { + embeddedElastic = EmbeddedElastic.builder() + .withElasticVersion(ELASTIC_VERSION) + .withSetting(PopularProperties.TRANSPORT_TCP_PORT, 0) + .withSetting(PopularProperties.HTTP_PORT, 0) + .withSetting(PopularProperties.CLUSTER_NAME, UUID.randomUUID()) + .withStartTimeout(60, TimeUnit.SECONDS) + .build() + .start(); + } catch (IOException | InterruptedException e) { + throw new ConfigurationException(e); + } + } + + @PreDestroy + public void stop() { + embeddedElastic.stop(); + } + + @LocalServerPort + private int port; + + @Test + void testLastN() throws IOException { + + Patient pt = new Patient(); + pt.addName().setFamily("Lastn").addGiven("Arthur"); + IIdType id = ourClient.create().resource(pt).execute().getId().toUnqualifiedVersionless(); + + Observation obs = new Observation(); + obs.getSubject().setReferenceElement(id); + String observationCode = "testobservationcode"; + String codeSystem = "http://testobservationcodesystem"; + obs.getCode().addCoding().setCode(observationCode).setSystem(codeSystem); + obs.setValue(new StringType(observationCode)); + Date effectiveDtm = new GregorianCalendar().getTime(); + obs.setEffective(new DateTimeType(effectiveDtm)); + obs.getCategoryFirstRep().addCoding().setCode("testcategorycode").setSystem("http://testcategorycodesystem"); + IIdType obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); + + myElasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX); + + Parameters output = ourClient.operation().onType(Observation.class).named("lastn") + .withParameter(Parameters.class, "max", new IntegerType(1)) + .andParameter("subject", new StringType("Patient/" + id.getIdPart())) + .execute(); + Bundle b = (Bundle) output.getParameter().get(0).getResource(); + assertEquals(1, b.getTotal()); + assertEquals(obsId, b.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless()); + } + + @BeforeEach + void beforeEach() { + + ourCtx = FhirContext.forR4(); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000); + String ourServerBase = "http://localhost:" + port + "/fhir/"; + ourClient = ourCtx.newRestfulGenericClient(ourServerBase); + ourClient.registerInterceptor(new LoggingInterceptor(true)); + } + + static class Initializer + implements ApplicationContextInitializer { + + @Override + public void initialize( + ConfigurableApplicationContext configurableApplicationContext) { + // Since the port is dynamically generated, replace the URL with one that has the correct port + TestPropertyValues.of("elasticsearch.rest_url=http://localhost:" + embeddedElastic.getHttpPort()) + .applyTo(configurableApplicationContext.getEnvironment()); + } + + } +}