From ddddfe9d4601de5c6cc23cd76c1f2f58bd24777b Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Mon, 13 Dec 2021 19:26:45 +0100 Subject: [PATCH] [WIP] Replace Local File Loading with Direct Upload The local file loading has the security implication that an attacker can load arbitary files located at the server. Also removed dependencies to lambok, various JSON parsing libs and commons-lang3. Also migrated to Java 17. --- .github/workflows/ci.yml | 62 +++- Dockerfile | 26 +- README.md | 44 +-- docker-compose.yml | 22 ++ docker/docker.template.yml | 6 - docker/start.sh | 6 - pom.xml | 332 +++++++++--------- .../Icd10DictionaryApplication.java | 3 + .../api/IcdCodeRestController.java | 39 +- .../icd10dictionary/dao/IcdCodeDao.java | 6 +- .../dao/IcdCodeDaoPostgres.java | 75 ++-- .../datasource/DatasourceConfiguration.java | 18 - .../samply/icd10dictionary/model/IcdCode.java | 17 +- .../icd10dictionary/model/ValueSet.java | 13 +- .../icd10dictionary/model/ValueSetEntry.java | 11 +- .../model/ValueSetExpansion.java | 14 +- .../icd10dictionary/service/CodeSystem.java | 15 +- .../icd10dictionary/service/Concept.java | 18 +- .../service/LoadIcdCodeService.java | 98 +++--- .../icd10dictionary/service/Property.java | 12 +- .../service/SearchIcdCodeService.java | 8 +- src/main/resources/application.yml | 9 +- 22 files changed, 401 insertions(+), 453 deletions(-) create mode 100644 docker-compose.yml delete mode 100644 docker/docker.template.yml delete mode 100644 docker/start.sh delete mode 100644 src/main/java/de/samply/icd10dictionary/datasource/DatasourceConfiguration.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d280a96..949d3dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - develop tags: - v[0-9]+.[0-9]+.[0-9]+** pull_request: @@ -15,18 +16,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Cache Local Maven Repo - uses: actions/cache@v2.1.2 + uses: actions/cache@v2 with: path: ~/.m2/repository - key: maven-repo + key: maven-repo-test-${{ hashFiles('pom.xml') }} - name: Cache SonarCloud packages uses: actions/cache@v1 @@ -54,28 +57,26 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Cache Local Maven Repo - uses: actions/cache@v2.1.2 + uses: actions/cache@v2 with: - path: | - ~/.m2/repository - key: maven-repo + path: ~/.m2/repository + key: maven-repo-build-${{ hashFiles('pom.xml') }} - name: Build with Maven run: mvn -B package -DskipTests -Dmaven.javadoc.skip=true - - name: Login to GitHub Docker Registry + - name: Login to DockerHub uses: docker/login-action@v1 with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -83,9 +84,38 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - - name: Build and push image + - name: Docker meta + id: docker-meta + uses: docker/metadata-action@v3 + with: + images: | + samply/icd-dictionary + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,format=long + + - name: Build and push uses: docker/build-push-action@v2 with: context: . - tags: ghcr.io/samply/icd10-dictionary:${{ github.sha }} + platforms: linux/amd64,linux/arm64 push: true + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: samply/icd-dictionary:sha-${{ github.sha }} + format: template + template: '@/contrib/sarif.tpl' + output: trivy-results.sarif + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@codeql-bundle-20211208 + with: + sarif_file: trivy-results.sarif diff --git a/Dockerfile b/Dockerfile index a5e1bdc..c4aa6f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,8 @@ -FROM openjdk:14-ea-alpine +FROM openjdk:17 -ENV SPRING_PROFILES_ACTIVE=docker -ARG JAR_FILE=target/icd10-dictionary.jar +COPY target/icd10-dictionary.jar /app/ -ENV ICD_DB_HOST=localhost -ENV ICD_DB_PORT=5432 -ENV ICD_DB_NAME=icd10 -ENV ICD_DB_USER=postgres -ENV ICD_DB_PASSWORD=password -ENV ICD_POOL_SIZE=30 +WORKDIR /app +USER 1001 -RUN apk update -RUN apk upgrade -RUN apk add bash -RUN apk add gettext - -COPY ${JAR_FILE} app.jar -COPY ./docker/start.sh . -RUN chmod +x ./start.sh -RUN mkdir config -COPY ./docker/docker.template.yml config - -CMD ["./start.sh"] +CMD ["java", "-jar", "icd10-dictionary.jar"] diff --git a/README.md b/README.md index 3d349f6..32f2121 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,18 @@ http://localhost:8080/fhir/ValueSet/$expand?url=http://hl7.org/fhir/sid/icd-10-g ## Default parameters -| variable | Docker | default value | -|---------------------------|---------------------------|--------------------------| -| application port | | 8080 | -| database host | ICD_DB_HOST | localhost | -| database port | ICD_DB_PORT | 5432 | -| database name | ICD_DB_NAME | icd10 | -| database user | ICD_DB_USER | postgres | -| database password | ICD_DB_PASSWORD | password | -| pool size | ICD_POOL_SIZE | 30 | +| Env Var | Default Value | +|----------------------------|----------------------------------------| +| | 8080 | +| SPRING_DATASOURCE_URL | jdbc:postgresql://localhost:5432/icd10 | +| SPRING_DATASOURCE_USERNAME | icd10 | +| SPRING_DATASOURCE_PASSWORD | icd10 | ## Start postgres -For testing purposes one can start a postgres database with Docker using following comand: +For testing purposes one can start a postgres database with Docker using following command: ``` -docker network create -d bridge icd-net -docker run --name icd-postgres -d --network=icd-net -e POSTGRES_PASSWORD=password -p 5432:5432 postgres:alpine -``` -Then create a database by executing a bash and using PSQL -``` -docker exec -it icd-postgres bin/bash -psql -U postgres -CREATE DATABASE icd10; +docker run -e POSTGRES_DB=icd10 -e POSTGRES_USER=icd10 -e POSTGRES_PASSWORD=icd10 -p 5432:5432 postgres ``` ## Preparing data @@ -56,15 +46,9 @@ java -jar fhir-claml-0.0.1-SNAPSHOT.jar -valueset http://hl7.org/fhir/sid/icd-10-gm/vs ``` -4.) Run the ICD-10 dictionary (as executable jar) and load the data by using the endpoint "/api/v1/icd/load" with the file path to the FHIR .json-file as body - e.g. -``` -http://localhost:8080/api/v1/icd/load - -C:\Users\xyz\icd-service\codesystem-icd10gm-2020.json +4.) Run the ICD-10 dictionary (as executable jar) and load the data by using the endpoint "/api/v1/icd/load": ``` -Remark: When working with Docker the file must be copied to a suitable location inside the container -``` -docker cp C:\Users\xyz\icd-service\codesystem-icd10gm-2020.json [CONTAINER-ID]:/var/tmp/icd10 +curl -d @codesystem-example.json -H Content-Type:application/json http://localhost:8080/api/v1/icd/load ``` ## Docker @@ -75,16 +59,12 @@ docker build -t icd-dictionary . ``` The command for starting the container is something like ``` -docker run --rm -d -e "ICD_DB_HOST=icd-postgres" -p 8080:8080 --network=icd-net --name icd-dictionary icd-dictionary +docker run --rm -d -p 8080:8080 --name icd-dictionary icd-dictionary ``` -## Developers - -This project uses lombok. Though it is not neccessary it is recomended to install a suitable lombok plugin for your IDE (e.g. for IntelliJ Idea install https://plugins.jetbrains.com/plugin/6317-lombok). - ## License -Copyright 2020 The Samply Development Community +Copyright 2020 - 2021 The Samply Community Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fa4b966 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.2' +services: + postgres: + image: "postgres" + environment: + POSTGRES_DB: icd10 + POSTGRES_USER: icd10 + POSTGRES_PASSWORD: icd10 + volumes: + - "data:/var/lib/postgresql/data" + + icd-dictionary: + build: . + environment: + SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/icd10" + ports: + - "8080:8080" + depends_on: + - postgres + +volumes: + data: diff --git a/docker/docker.template.yml b/docker/docker.template.yml deleted file mode 100644 index 39e97e4..0000000 --- a/docker/docker.template.yml +++ /dev/null @@ -1,6 +0,0 @@ -app: - datasource: - jdbc-url: jdbc:postgresql://${ICD_DB_HOST}:${ICD_DB_PORT}/${ICD_DB_NAME}?currentSchema=samply - username: ${ICD_DB_USER} - password: ${ICD_DB_PASSWORD} - pool-size: ${ICD_POOL_SIZE} diff --git a/docker/start.sh b/docker/start.sh deleted file mode 100644 index 14c9385..0000000 --- a/docker/start.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -echo 'started start.sh' -envsubst < ./config/docker.template.yml > ./config/application.yml - -exec java -jar app.jar diff --git a/pom.xml b/pom.xml index 3588ff6..bf5ef5f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,171 +1,171 @@ - - 4.0.0 - - - de.samply - parent - 11.1.1 - - - icd10-dictionary - 0.4.0-SNAPSHOT - - icd10-dictionary - REST Service for ICD10 lookups - https://github.com/samply/maven-parent/icd10-dictionary - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - - Bjoern Kroll - Bjoern.Kroll@uni-luebeck.de - University Medical Center Schleswig-Holstein - http://www.uksh.de/en - - - - - 11 - 2.5.6 - samply - https://sonarcloud.io - - - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - - + + 4.0.0 + + + de.samply + parent + 11.1.1 + + + icd10-dictionary + 0.4.0-SNAPSHOT + + icd10-dictionary + REST Service for ICD10 lookups + https://github.com/samply/maven-parent/icd10-dictionary + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + Bjoern Kroll + Bjoern.Kroll@uni-luebeck.de + University Medical Center Schleswig-Holstein + http://www.uksh.de/en + + + + + 2.6.1 + 2.15.0 + 3.1.2 + 3.3.0 + samply + https://sonarcloud.io + + + - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-jdbc - - - - org.postgresql - postgresql - runtime - - - - org.flywaydb - flyway-core - - - - - javax.json - javax.json-api - 1.1.4 - - - - - org.glassfish - javax.json - 1.1 - runtime - - - - - javax.json.bind - javax.json.bind-api - 1.0.0-RC2 - - - - - org.eclipse - yasson - 1.0.0-RC1 - runtime - - - - org.projectlombok - lombok - provided - - - - org.apache.commons - commons-lang3 - 3.10 - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.junit.vintage - junit-vintage-engine - - - + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + - - - icd10-dictionary - - - - org.apache.maven.plugins - maven-compiler-plugin - - 11 - 11 - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} - - de.samply.icd10dictionary.Icd10DictionaryApplication - - - - - repackage - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - 8 - - - - + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + org.postgresql + postgresql + runtime + + + + org.flywaydb + flyway-core + + + + + org.apache.logging.log4j + log4j-to-slf4j + ${log4j2.version} + + + + + org.apache.logging.log4j + log4j-api + ${log4j2.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + icd10-dictionary + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${version.maven.plugin.checkstyle} + + + com.puppycrawl.tools + checkstyle + 9.1 + + + + google_checks.xml + UTF-8 + true + true + false + warning + + + + validate + validate + + check + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + de.samply.icd10dictionary.Icd10DictionaryApplication + + + + + repackage + + + + + + diff --git a/src/main/java/de/samply/icd10dictionary/Icd10DictionaryApplication.java b/src/main/java/de/samply/icd10dictionary/Icd10DictionaryApplication.java index ec54db5..1706b84 100644 --- a/src/main/java/de/samply/icd10dictionary/Icd10DictionaryApplication.java +++ b/src/main/java/de/samply/icd10dictionary/Icd10DictionaryApplication.java @@ -3,6 +3,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Main entry point. + */ @SpringBootApplication public class Icd10DictionaryApplication { diff --git a/src/main/java/de/samply/icd10dictionary/api/IcdCodeRestController.java b/src/main/java/de/samply/icd10dictionary/api/IcdCodeRestController.java index 2d4e601..3ce6cd7 100644 --- a/src/main/java/de/samply/icd10dictionary/api/IcdCodeRestController.java +++ b/src/main/java/de/samply/icd10dictionary/api/IcdCodeRestController.java @@ -3,11 +3,11 @@ import de.samply.icd10dictionary.model.IcdCode; import de.samply.icd10dictionary.model.ValueSet; import de.samply.icd10dictionary.model.ValueSetEntry; +import de.samply.icd10dictionary.model.ValueSetExpansion; +import de.samply.icd10dictionary.service.CodeSystem; import de.samply.icd10dictionary.service.LoadIcdCodeService; import de.samply.icd10dictionary.service.SearchIcdCodeService; import java.util.List; -import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -18,6 +18,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +/** + * READ API. + */ @RestController @CrossOrigin public class IcdCodeRestController { @@ -40,23 +43,20 @@ public ResponseEntity check() { } /** - * Loads ICD-10 catalog from specified location 'clamlFileUri' on server. + * Loads ICD-10 catalog from specified {@code codeSystem}. * - * @param clamlFileUri Location of content file + * @param codeSystem the CodeSystem * @return ResponseEntity Http status */ @PostMapping("api/v1/icd/load") - public ResponseEntity loadFromFile(@RequestBody String clamlFileUri) { + public ResponseEntity load(@RequestBody CodeSystem codeSystem) { try { - LoadIcdCodeService.ErrorCode errorCode = this.loadIcdCodeService.load(clamlFileUri); + LoadIcdCodeService.ErrorCode errorCode = this.loadIcdCodeService.load(codeSystem); switch (errorCode) { case OK: break; - case FILE_NOT_FOUND: - return new ResponseEntity<>("File not found", HttpStatus.BAD_REQUEST); case DB_NOT_EMPTY: return new ResponseEntity<>("Database not empty", HttpStatus.CONFLICT); - case OTHER: default: return new ResponseEntity<>("Unspecified error", HttpStatus.BAD_REQUEST); } @@ -64,7 +64,7 @@ public ResponseEntity loadFromFile(@RequestBody String clamlFileUri) { return new ResponseEntity<>(e.getLocalizedMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } - return new ResponseEntity<>("File imported", HttpStatus.OK); + return new ResponseEntity<>("Data imported", HttpStatus.OK); } /** @@ -76,7 +76,7 @@ public ResponseEntity loadFromFile(@RequestBody String clamlFileUri) { */ @GetMapping("fhir/ValueSet/$expand") public ValueSet expand(@RequestParam String url, @RequestParam String filter) { - if (!StringUtils.equalsIgnoreCase(url, URL_ICD_10_GM)) { + if (!URL_ICD_10_GM.equalsIgnoreCase(url)) { return new ValueSet(); } List icdCodes = this.searchIcdCodeService.retrieveCodesByQueryText(filter); @@ -84,19 +84,8 @@ public ValueSet expand(@RequestParam String url, @RequestParam String filter) { } private ValueSet createValueSet(List icdCodes) { - ValueSet valueSet = new ValueSet(); - List entries = - icdCodes.stream() - .map( - icdCode -> { - ValueSetEntry entry = new ValueSetEntry(); - entry.setCode(icdCode.getCode()); - entry.setDisplay(icdCode.getDisplay()); - return entry; - }) - .collect(Collectors.toList()); - valueSet.getExpansion().setContains(entries); - - return valueSet; + return new ValueSet(new ValueSetExpansion(icdCodes.stream() + .map(icdCode -> new ValueSetEntry(icdCode.code(), icdCode.display())) + .toList())); } } diff --git a/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDao.java b/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDao.java index d1af85e..915b570 100644 --- a/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDao.java +++ b/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDao.java @@ -1,13 +1,15 @@ package de.samply.icd10dictionary.dao; import de.samply.icd10dictionary.model.IcdCode; - import java.util.List; import java.util.Optional; +/** + * Main DAO. + */ public interface IcdCodeDao { - int insert(IcdCode icdCode); + void insert(IcdCode icdCode); Optional selectIcdCodeByCode(String codeParam); diff --git a/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDaoPostgres.java b/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDaoPostgres.java index 8a61668..0113c4a 100644 --- a/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDaoPostgres.java +++ b/src/main/java/de/samply/icd10dictionary/dao/IcdCodeDaoPostgres.java @@ -3,85 +3,76 @@ import de.samply.icd10dictionary.model.IcdCode; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; - -@SuppressWarnings({"SqlDialectInspection", "SqlNoDataSourceInspection"}) -@Repository("postgres") +/** + * Postgres implementation. + */ +@Repository public class IcdCodeDaoPostgres implements IcdCodeDao { private final JdbcTemplate jdbcTemplate; - @Autowired public IcdCodeDaoPostgres(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override - public int insert(IcdCode icdCode) { - final String sql = - "INSERT INTO IcdCode (code, kind, display, definition, parentCode, childCodes) " - + "VALUES (?, ?, ?, ?, ?, ?)"; + public void insert(IcdCode icdCode) { this.jdbcTemplate.update( - sql, - icdCode.getCode(), - icdCode.getKind(), - icdCode.getDisplay(), - icdCode.getDefinition(), - icdCode.getParentCode(), - icdCode.getChildCodes()); - return 0; + "INSERT INTO IcdCode (code, kind, display, definition, parentCode, childCodes) " + + "VALUES (?, ?, ?, ?, ?, ?)", + icdCode.code(), + icdCode.kind(), + icdCode.display(), + icdCode.definition(), + icdCode.parentCode(), + icdCode.childCodes()); } @Override public Optional selectIcdCodeByCode(String codeParam) { - final String sql = - "SELECT code, kind, display, definition, parentCode, childCodes " - + "FROM IcdCode " - + "WHERE code = ?"; IcdCode icdCode = this.jdbcTemplate.queryForObject( - sql, - new Object[] {codeParam}, - new int[] {1}, + "SELECT code, kind, display, definition, parentCode, childCodes " + + "FROM IcdCode " + + "WHERE code = ?", + new Object[]{codeParam}, + new int[]{1}, ((resultSet, i) -> createIcdCode(resultSet))); return Optional.ofNullable(icdCode); } @Override public int deleteByCode(String code) { - final String sql = "DELETE FROM IcdCode WHERE id = ?"; - this.jdbcTemplate.update(sql, code); + this.jdbcTemplate.update("DELETE FROM IcdCode WHERE id = ?", code); return 0; } @Override public int deleteAll() { - final String sql = "DELETE FROM IcdCode"; - this.jdbcTemplate.update(sql); + this.jdbcTemplate.update("DELETE FROM IcdCode"); return 0; } @Override public int count() { - final String sql = "SELECT count(*) FROM IcdCode "; - List countList = this.jdbcTemplate.query(sql, (resultSet, i) -> resultSet.getInt(1)); + List countList = this.jdbcTemplate.query( + "SELECT count(*) FROM IcdCode ", + (resultSet, i) -> resultSet.getInt(1)); return countList.get(0); } @Override public List retrieveCodesByQueryText(String queryText) { - if (StringUtils.isBlank(queryText)) { - return new ArrayList<>(); + if (queryText == null || queryText.isBlank()) { + return List.of(); } - final String sql = + return this.jdbcTemplate.query( "SELECT code, kind, display, definition, parentCode, childCodes " + "FROM IcdCode " + "WHERE childCodes = ''" @@ -90,20 +81,20 @@ public List retrieveCodesByQueryText(String queryText) { + " array_to_string(" + " array(select unnest || ':*' " + " from unnest(" - + " regexp_split_to_array(plainto_tsquery('german', '" + queryText + "')::text, " + + " regexp_split_to_array(plainto_tsquery('german', ?)::text, " + " '\\s&\\s')" + " )" + " ), ' & '" - + " )::tsquery)"; - return this.jdbcTemplate.query(sql, new String[]{"%" + queryText + "%"}, new int[]{1}, - ((resultSet, i) -> createIcdCode(resultSet))); + + " )::tsquery)", + ((resultSet, i) -> createIcdCode(resultSet)), + "%" + queryText + "%"); } @Override public List retrieveAll() { - final String sql = - "SELECT code, kind, display, definition, parentCode, childCodes FROM IcdCode "; - return this.jdbcTemplate.query(sql, ((resultSet, i) -> createIcdCode(resultSet))); + return this.jdbcTemplate.query( + "SELECT code, kind, display, definition, parentCode, childCodes FROM IcdCode ", + ((resultSet, i) -> createIcdCode(resultSet))); } private IcdCode createIcdCode(ResultSet resultSet) throws SQLException { diff --git a/src/main/java/de/samply/icd10dictionary/datasource/DatasourceConfiguration.java b/src/main/java/de/samply/icd10dictionary/datasource/DatasourceConfiguration.java deleted file mode 100644 index 20f5a6d..0000000 --- a/src/main/java/de/samply/icd10dictionary/datasource/DatasourceConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.samply.icd10dictionary.datasource; - -import javax.sql.DataSource; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - - -@Configuration -public class DatasourceConfiguration { - - @Bean(name = "icdDataSource") - @ConfigurationProperties("app.datasource") - public DataSource icdDataSource() { - return DataSourceBuilder.create().build(); - } -} diff --git a/src/main/java/de/samply/icd10dictionary/model/IcdCode.java b/src/main/java/de/samply/icd10dictionary/model/IcdCode.java index c1c93f4..6469adc 100644 --- a/src/main/java/de/samply/icd10dictionary/model/IcdCode.java +++ b/src/main/java/de/samply/icd10dictionary/model/IcdCode.java @@ -1,16 +1,9 @@ package de.samply.icd10dictionary.model; -import lombok.AllArgsConstructor; -import lombok.Data; +/** + * A row of the IcdCode database table. + */ +public record IcdCode(String code, String kind, String display, String definition, + String parentCode, String childCodes) { -@Data -@AllArgsConstructor -public class IcdCode { - - private String code; - private String kind; - private String display; - private String definition; - private String parentCode; - private String childCodes; } diff --git a/src/main/java/de/samply/icd10dictionary/model/ValueSet.java b/src/main/java/de/samply/icd10dictionary/model/ValueSet.java index c52a558..fe133f5 100644 --- a/src/main/java/de/samply/icd10dictionary/model/ValueSet.java +++ b/src/main/java/de/samply/icd10dictionary/model/ValueSet.java @@ -1,10 +1,11 @@ package de.samply.icd10dictionary.model; -import lombok.Data; -import lombok.NoArgsConstructor; +/** + * FHIR ValueSet. + */ +public record ValueSet(ValueSetExpansion expansion) { -@Data -@NoArgsConstructor -public class ValueSet { - private ValueSetExpansion expansion = new ValueSetExpansion(); + public ValueSet() { + this(new ValueSetExpansion()); + } } diff --git a/src/main/java/de/samply/icd10dictionary/model/ValueSetEntry.java b/src/main/java/de/samply/icd10dictionary/model/ValueSetEntry.java index 5fdb17c..3e76920 100644 --- a/src/main/java/de/samply/icd10dictionary/model/ValueSetEntry.java +++ b/src/main/java/de/samply/icd10dictionary/model/ValueSetEntry.java @@ -1,11 +1,8 @@ package de.samply.icd10dictionary.model; -import lombok.Data; -import lombok.NoArgsConstructor; +/** + * FHIR ValueSet. + */ +public record ValueSetEntry(String code, String display) { -@Data -@NoArgsConstructor -public class ValueSetEntry { - private String code; - private String display; } diff --git a/src/main/java/de/samply/icd10dictionary/model/ValueSetExpansion.java b/src/main/java/de/samply/icd10dictionary/model/ValueSetExpansion.java index f46277d..1477c5c 100644 --- a/src/main/java/de/samply/icd10dictionary/model/ValueSetExpansion.java +++ b/src/main/java/de/samply/icd10dictionary/model/ValueSetExpansion.java @@ -1,13 +1,13 @@ package de.samply.icd10dictionary.model; -import java.util.ArrayList; import java.util.List; -import lombok.Data; -import lombok.NoArgsConstructor; +/** + * FHIR ValueSet. + */ +public record ValueSetExpansion(List contains) { -@Data -@NoArgsConstructor -public class ValueSetExpansion { - private List contains = new ArrayList<>(); + ValueSetExpansion() { + this(List.of()); + } } diff --git a/src/main/java/de/samply/icd10dictionary/service/CodeSystem.java b/src/main/java/de/samply/icd10dictionary/service/CodeSystem.java index 1f8af34..7adc9fc 100644 --- a/src/main/java/de/samply/icd10dictionary/service/CodeSystem.java +++ b/src/main/java/de/samply/icd10dictionary/service/CodeSystem.java @@ -1,13 +1,14 @@ package de.samply.icd10dictionary.service; -import java.util.ArrayList; import java.util.List; -import lombok.Data; -import lombok.NoArgsConstructor; +/** + * FHIR CodeSystem. + */ +public record CodeSystem(List concept) { -@Data -@NoArgsConstructor -public class CodeSystem { - private List concept = new ArrayList<>(); + @Override + public List concept() { + return concept == null ? List.of() : concept; + } } diff --git a/src/main/java/de/samply/icd10dictionary/service/Concept.java b/src/main/java/de/samply/icd10dictionary/service/Concept.java index c7ca951..9fe9cb0 100644 --- a/src/main/java/de/samply/icd10dictionary/service/Concept.java +++ b/src/main/java/de/samply/icd10dictionary/service/Concept.java @@ -1,16 +1,14 @@ package de.samply.icd10dictionary.service; -import java.util.ArrayList; import java.util.List; -import lombok.Data; -import lombok.NoArgsConstructor; +/** + * FHIR CodeSystem. + */ +public record Concept(String code, String display, String definition, List property) { -@Data -@NoArgsConstructor -public class Concept { - private String code; - private String display; - private String definition; - private List property = new ArrayList<>(); + @Override + public List property() { + return property == null ? List.of() : property; + } } diff --git a/src/main/java/de/samply/icd10dictionary/service/LoadIcdCodeService.java b/src/main/java/de/samply/icd10dictionary/service/LoadIcdCodeService.java index fc73640..ddf6d00 100644 --- a/src/main/java/de/samply/icd10dictionary/service/LoadIcdCodeService.java +++ b/src/main/java/de/samply/icd10dictionary/service/LoadIcdCodeService.java @@ -2,96 +2,88 @@ import de.samply.icd10dictionary.dao.IcdCodeDao; import de.samply.icd10dictionary.model.IcdCode; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.util.Collection; import java.util.Optional; import java.util.stream.Collectors; -import javax.json.bind.Jsonb; -import javax.json.bind.JsonbBuilder; import org.apache.tomcat.util.buf.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +/** + * Data load service. + */ @Service public class LoadIcdCodeService { + private static final Logger logger = LoggerFactory.getLogger(LoadIcdCodeService.class); + private final IcdCodeDao icdCodeDao; - @Autowired - public LoadIcdCodeService(@Qualifier("postgres") IcdCodeDao icdCodeDao) { + public LoadIcdCodeService(IcdCodeDao icdCodeDao) { this.icdCodeDao = icdCodeDao; } /** - * Reads Codesystem (CLAML) from the local filesystem and writes codes to database. + * Imports the given CodeSystem into the database. * - * @param filename the filename of the codesystem - * @return ErrorCode + * @param codeSystem the CodeSystem + * @return {@code DB_NOT_EMPTY} if the database contains already some codes */ - public ErrorCode load(String filename) { - if (this.icdCodeDao.count() > 0) { + public ErrorCode load(CodeSystem codeSystem) { + if (icdCodeDao.count() > 0) { return ErrorCode.DB_NOT_EMPTY; } - Jsonb jsonb = JsonbBuilder.create(); - CodeSystem codeSystem; - try { - codeSystem = jsonb.fromJson(new FileInputStream(filename), CodeSystem.class); - } catch (FileNotFoundException e) { - return ErrorCode.FILE_NOT_FOUND; - } - createIcdCodes(codeSystem); return ErrorCode.OK; } private void createIcdCodes(CodeSystem codeSystem) { - codeSystem.getConcept().forEach(concept -> this.icdCodeDao.insert(createIcdCode(concept))); + codeSystem.concept() + .stream().map(LoadIcdCodeService::createIcdCode) + .forEach(optionalIcdCode -> { + if (optionalIcdCode.isPresent()) { + logger.debug("load ICD cod {}", optionalIcdCode.get()); + icdCodeDao.insert(optionalIcdCode.get()); + } else { + logger.warn("skip non-buildable ICD code"); + } + }); } - private IcdCode createIcdCode(Concept concept) { - return new IcdCode( - concept.getCode(), - determineKind(concept), - concept.getDefinition(), - concept.getDisplay(), - determineParentCode(concept), - StringUtils.join(determineChildCodes(concept), ',')); + private static Optional createIcdCode(Concept concept) { + return findProperty(concept, "kind") + .map(kind -> new IcdCode( + concept.code(), + kind, + concept.definition(), + concept.display(), + findProperty(concept, "parent").orElse(null), + StringUtils.join(determineChildCodes(concept), ','))); } - private Collection determineChildCodes(Concept concept) { - return concept.getProperty().stream() - .filter(property -> property.getCode().equals("child")) - .map(Property::getValueCode) + private static Collection determineChildCodes(Concept concept) { + return concept.property().stream() + .filter(property -> property.code().equals("child")) + .map(Property::valueCode) .collect(Collectors.toList()); } - private String determineParentCode(Concept concept) { - return findProperty(concept, "parent"); - } - - private String determineKind(Concept concept) { - return findProperty(concept, "kind"); - } - - private String findProperty(Concept concept, String propertyCode) { - Optional parentPropertyMayBe = - concept.getProperty().stream() - .filter(property -> property.getCode().equals(propertyCode)) - .findFirst(); - if (parentPropertyMayBe.isEmpty()) { - return null; - } else { - return parentPropertyMayBe.get().getValueCode(); - } + private static Optional findProperty(Concept concept, String propertyCode) { + return concept.property().stream() + .filter(property -> property.code().equals(propertyCode)) + .findFirst() + .map(Property::valueCode); } + /** + * Error codes. + */ public enum ErrorCode { OK, FILE_NOT_FOUND, DB_NOT_EMPTY, - OTHER; + OTHER } } diff --git a/src/main/java/de/samply/icd10dictionary/service/Property.java b/src/main/java/de/samply/icd10dictionary/service/Property.java index 6307874..faa4d2c 100644 --- a/src/main/java/de/samply/icd10dictionary/service/Property.java +++ b/src/main/java/de/samply/icd10dictionary/service/Property.java @@ -1,12 +1,8 @@ package de.samply.icd10dictionary.service; -import lombok.Data; -import lombok.NoArgsConstructor; +/** + * FHIR CodeSystem. + */ +public record Property(String code, String valueString, String valueCode) { -@Data -@NoArgsConstructor -public class Property { - private String code; - private String valueString; - private String valueCode; } diff --git a/src/main/java/de/samply/icd10dictionary/service/SearchIcdCodeService.java b/src/main/java/de/samply/icd10dictionary/service/SearchIcdCodeService.java index bcacf4b..86ee9dd 100644 --- a/src/main/java/de/samply/icd10dictionary/service/SearchIcdCodeService.java +++ b/src/main/java/de/samply/icd10dictionary/service/SearchIcdCodeService.java @@ -3,17 +3,17 @@ import de.samply.icd10dictionary.dao.IcdCodeDao; import de.samply.icd10dictionary.model.IcdCode; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +/** + * Main Service. + */ @Service public class SearchIcdCodeService { private final IcdCodeDao icdCodeDao; - @Autowired - public SearchIcdCodeService(@Qualifier("postgres") IcdCodeDao icdCodeDao) { + public SearchIcdCodeService(IcdCodeDao icdCodeDao) { this.icdCodeDao = icdCodeDao; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5db3e9d..8292796 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,5 @@ -app: +spring: datasource: - jdbc-url: jdbc:postgresql://localhost:5432/icd10?currentSchema=samply - username: postgres - password: password - pool-size: 30 + url: jdbc:postgresql://localhost:5432/icd10?currentSchema=samply + username: icd10 + password: icd10