From d07e856597228641c8c7e64d339038bd41b23f03 Mon Sep 17 00:00:00 2001 From: Luke Sikina Date: Tue, 18 Jun 2024 10:00:22 -0400 Subject: [PATCH] [ALS-6276] Search improvements --- JSONS.md | 705 ++++++++++++++++++ db/schema.sql | 1 + docker-compose.yml | 2 - pom.xml | 5 + .../dictionary/concept/ConceptController.java | 8 +- .../dictionary/concept/ConceptRepository.java | 20 +- .../dictionary/concept/ConceptRowMapper.java | 31 +- .../concept/model/CategoricalConcept.java | 6 +- .../dictionary/concept/model/ConceptType.java | 11 +- .../concept/model/ContinuousConcept.java | 6 +- .../dictionary/facet/FacetRepository.java | 2 +- .../filter/FilterQueryGenerator.java | 35 +- .../dictionary/util/MapExtractor.java | 2 +- src/main/resources/application.properties | 6 +- .../concept/ConceptControllerTest.java | 32 +- .../concept/ConceptRepositoryTest.java | 64 +- .../concept/ConceptServiceTest.java | 6 +- .../dictionary/concept/model/ConceptTest.java | 19 +- .../concept/model/ConceptTypeTest.java | 17 + 19 files changed, 888 insertions(+), 90 deletions(-) create mode 100644 JSONS.md create mode 100644 src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTypeTest.java diff --git a/JSONS.md b/JSONS.md new file mode 100644 index 0000000..d71dff1 --- /dev/null +++ b/JSONS.md @@ -0,0 +1,705 @@ +# JSON for Dictionary API + +## Concepts + +**List, no filter, page 0, page size 10** + +Request: +```bash +curl --location 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/concepts/?page_number=0&page_size=5' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '{"facets": [], "search": ""}' +``` + +Response: +```json +{ + "totalPages": 9, + "totalElements": 90, + "pageable": { + "pageNumber": 0, + "pageSize": 10, + "sort": { + "unsorted": true, + "sorted": false, + "empty": true + }, + "offset": 0, + "unpaged": false, + "paged": true + }, + "numberOfElements": 10, + "first": true, + "last": false, + "size": 10, + "content": [ + { + "conceptPath": "\\ACT Diagnosis ICD-10\\", + "name": "", + "display": "", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\", + "name": "", + "display": "", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\", + "name": "", + "display": "", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\", + "name": "", + "display": "", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\J45.5 Severe persistent asthma\\", + "name": "", + "display": "", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\J45.5 Severe persistent asthma\\J45.51 Severe persistent asthma with (acute) exacerbation\\", + "name": "J45.51 Severe persistent asthma with (acute) exacerbation", + "display": "J45.51 Severe persistent asthma with (acute) exacerbation", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\J45.5 Severe persistent asthma\\J45.52 Severe persistent asthma with status asthmaticus\\", + "name": "J45.52 Severe persistent asthma with status asthmaticus", + "display": "J45.52 Severe persistent asthma with status asthmaticus", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\J45.9 Other and unspecified asthma\\", + "name": "", + "display": "", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\J45.9 Other and unspecified asthma\\J45.90 Unspecified asthma\\", + "name": "", + "display": "", + "dataset": "1", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\J45.9 Other and unspecified asthma\\J45.90 Unspecified asthma\\J45.901 Unspecified asthma with (acute) exacerbation\\", + "name": "J45.901 Unspecified asthma with (acute) exacerbation", + "display": "J45.901 Unspecified asthma with (acute) exacerbation", + "dataset": "1", + "values": [], + "children": null, + "meta": null + } + ], + "number": 0, + "sort": { + "unsorted": true, + "sorted": false, + "empty": true + }, + "empty": false +} +``` + +**List, no filter, page 10, page size 5** + +Request: +```bash +curl --location 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/concepts/?page_number=5&page_size=10' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '{"facets": [], "search": ""}' +``` + +Response: +```json +{ + "totalPages": 18, + "totalElements": 90, + "pageable": { + "pageNumber": 10, + "pageSize": 5, + "sort": { + "unsorted": true, + "sorted": false, + "empty": true + }, + "offset": 50, + "unpaged": false, + "paged": true + }, + "numberOfElements": 5, + "first": false, + "last": false, + "size": 5, + "content": [ + { + "conceptPath": "\\phs000007\\pht000022\\", + "name": "pht000022", + "display": "ex0_20s", + "dataset": "phs000007", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\phs000007\\pht000022\\phv00004260\\", + "name": "", + "display": "", + "dataset": "phs000007", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\phs000007\\pht000022\\phv00004260\\FM219\\", + "name": "phv00004260", + "display": "FM219", + "dataset": "phs000007", + "min": 0, + "max": 0, + "meta": null + }, + { + "conceptPath": "\\phs000007\\pht000033\\", + "name": "pht000033", + "display": "ex1_4s", + "dataset": "phs000007", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\phs000007\\pht000033\\phv00008849\\", + "name": "", + "display": "", + "dataset": "phs000007", + "values": [], + "children": null, + "meta": null + } + ], + "number": 10, + "sort": { + "unsorted": true, + "sorted": false, + "empty": true + }, + "empty": false +} +``` + +**List, filter by study ID facet = phs002715** + +Request: +```bash +curl --location 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/concepts' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '{"facets": [{ + "name": "phs002715", + "count": 44, + "category": "study_ids_dataset_ids" + }], "search": ""}' +``` + +Response: +```json +{ + "totalPages": 1, + "totalElements": 3, + "pageable": { + "pageNumber": 0, + "pageSize": 10, + "sort": { + "unsorted": true, + "sorted": false, + "empty": true + }, + "offset": 0, + "paged": true, + "unpaged": false + }, + "numberOfElements": 3, + "first": true, + "last": true, + "size": 10, + "content": [ + { + "conceptPath": "\\phs002715\\", + "name": "", + "display": "", + "dataset": "phs002715", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\phs002715\\age\\", + "name": "AGE_CATEGORY", + "display": "age", + "dataset": "phs002715", + "values": [], + "children": null, + "meta": null + }, + { + "conceptPath": "\\phs002715\\nsrr_ever_smoker\\", + "name": "nsrr_ever_smoker", + "display": "nsrr_ever_smoker", + "dataset": "phs002715", + "values": [], + "children": null, + "meta": null + } + ], + "number": 0, + "sort": { + "unsorted": true, + "sorted": false, + "empty": true + }, + "empty": false +} +``` + +**Detail, GIC concept** + +Request: +```bash +curl --location 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/concepts/detail/1' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '\ACT Diagnosis ICD-10\J00-J99 Diseases of the respiratory system (J00-J99)\J40-J47 Chronic lower respiratory diseases (J40-J47)\J45 Asthma\J45.5 Severe persistent asthma\J45.52 Severe persistent asthma with status asthmaticus\' +``` + +Response: +```json +{ + "type": "Categorical", + "conceptPath": "\\ACT Diagnosis ICD-10\\J00-J99 Diseases of the respiratory system (J00-J99)\\J40-J47 Chronic lower respiratory diseases (J40-J47)\\J45 Asthma\\J45.5 Severe persistent asthma\\J45.52 Severe persistent asthma with status asthmaticus\\", + "name": "J45.52 Severe persistent asthma with status asthmaticus", + "display": "J45.52 Severe persistent asthma with status asthmaticus", + "dataset": "1", + "values": [], + "children": null, + "meta": { + "values": "J45.52 Severe persistent asthma with status asthmaticus", + "description": "Approximate Synonyms:\nSevere persistent allergic asthma in status asthmaticus\nSevere persistent allergic asthma with status asthmaticus\nSevere persistent asthma in status asthmaticus\nSevere persistent asthma with allergic rhinitis in status asthmaticus\nSevere persistent asthma with allergic rhinitis with status asthmaticus" + } +} +``` + +**Detail, BDC concept** + +Request: +```bash +curl --location 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/concepts/detail/phs000007' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '\phs000007\pht000033\phv00008849\D080\' +``` + +Response: +```json +{ + "type": "Continuous", + "conceptPath": "\\phs000007\\pht000033\\phv00008849\\D080\\", + "name": "phv00008849", + "display": "D080", + "dataset": "phs000007", + "min": 0, + "max": 0, + "meta": { + "unique_identifier": "no", + "stigmatizing": "no", + "bdc_open_access": "yes", + "values": "5", + "description": "# 12 OZ CUPS OF CAFFEINATED COLA/DAY", + "free_text": "no" + } +} +``` + +## Facets + +**List, no filter** + +Request: +```bash +curl --location 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/facets/?page_number=0&page_size=10' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '{"facets": [], "search": ""}' +``` + +Response: +```json +[ + { + "name": "study_ids_dataset_ids", + "display": "Study IDs/Dataset IDs", + "description": "", + "facets": [ + { + "name": "1", + "display": "GIC", + "description": null, + "count": 44, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "2", + "display": "National Health and Nutrition Examination Survey", + "description": null, + "count": 7, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "3", + "display": "1000 Genomes Project", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs002715", + "display": "NSRR CFS", + "description": null, + "count": 3, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs000284", + "display": "CFS", + "description": null, + "count": 7, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs000007", + "display": "FHS", + "description": null, + "count": 10, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs002385", + "display": "HCT_for_SCD", + "description": null, + "count": 4, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs003463", + "display": "RECOVER_Adult", + "description": null, + "count": 3, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs003543", + "display": "NSRR_HSHC", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs002808", + "display": "nuMoM2b", + "description": null, + "count": 9, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs003566", + "display": "SPRINT", + "description": null, + "count": 3, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs001963", + "display": "DEMENTIA-SEQ", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + } + ] + }, + { + "name": "nsrr_harmonized", + "display": "Common Data Element Collection", + "description": "", + "facets": [ + { + "name": "LOINC", + "display": "LOINC", + "description": null, + "count": 1, + "children": null, + "category": "nsrr_harmonized" + }, + { + "name": "PhenX", + "display": "PhenX", + "description": null, + "count": 1, + "children": null, + "category": "nsrr_harmonized" + }, + { + "name": "gad_7", + "display": "Generalized Anxiety Disorder Assessment (GAD-7)", + "description": null, + "count": 0, + "children": null, + "category": "nsrr_harmonized" + }, + { + "name": "taps_tool", + "display": "NIDA CTN Common Data Elements = TAPS Tool", + "description": null, + "count": 1, + "children": null, + "category": "nsrr_harmonized" + } + ] + } +] +``` + +**List, filter by study ID facet = phs002715** + +Request: +```bash +curl --location 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/facets/' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '{"facets": [{ + "name": "phs002715", + "count": 44, + "category": "study_ids_dataset_ids" + }], "search": ""}' +``` + +Response: +```json +[ + { + "name": "study_ids_dataset_ids", + "display": "Study IDs/Dataset IDs", + "description": "", + "facets": [ + { + "name": "1", + "display": "GIC", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "2", + "display": "National Health and Nutrition Examination Survey", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "3", + "display": "1000 Genomes Project", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs002715", + "display": "NSRR CFS", + "description": null, + "count": 3, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs000284", + "display": "CFS", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs000007", + "display": "FHS", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs002385", + "display": "HCT_for_SCD", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs003463", + "display": "RECOVER_Adult", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs003543", + "display": "NSRR_HSHC", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs002808", + "display": "nuMoM2b", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs003566", + "display": "SPRINT", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + }, + { + "name": "phs001963", + "display": "DEMENTIA-SEQ", + "description": null, + "count": 0, + "children": null, + "category": "study_ids_dataset_ids" + } + ] + }, + { + "name": "nsrr_harmonized", + "display": "Common Data Element Collection", + "description": "", + "facets": [ + { + "name": "LOINC", + "display": "LOINC", + "description": null, + "count": 0, + "children": null, + "category": "nsrr_harmonized" + }, + { + "name": "PhenX", + "display": "PhenX", + "description": null, + "count": 0, + "children": null, + "category": "nsrr_harmonized" + }, + { + "name": "gad_7", + "display": "Generalized Anxiety Disorder Assessment (GAD-7)", + "description": null, + "count": 0, + "children": null, + "category": "nsrr_harmonized" + }, + { + "name": "taps_tool", + "display": "NIDA CTN Common Data Elements = TAPS Tool", + "description": null, + "count": 0, + "children": null, + "category": "nsrr_harmonized" + } + ] + } +] +``` + +**Detail** + +Request: +```bash +curl --location --request GET 'https://nhanes-dev.hms.harvard.edu/picsure/proxy/dictionary-api/facets/study_ids_dataset_ids/1' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Token ADD_TOKEN_HERE' \ +--data '{"facets": [{ + "name": "phs002715", + "count": 44, + "category": "study_ids_dataset_ids" + }], "search": ""}' +``` + +Response: +```json +{ + "name": "1", + "display": "GIC", + "description": null, + "count": null, + "children": null, + "category": "study_ids_dataset_ids" +} + +``` \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index 7d787dd..65fb0f6 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -37,6 +37,7 @@ CREATE TABLE dict.concept_node ( CONCEPT_TYPE VARCHAR(32) NOT NULL DEFAULT 'Interior', CONCEPT_PATH VARCHAR(10000) NOT NULL DEFAULT 'INVALID', PARENT_ID INT, + SEARCHABLE_FIELDS TSVECTOR, PRIMARY KEY (CONCEPT_NODE_ID), CONSTRAINT fk_parent FOREIGN KEY (PARENT_ID) REFERENCES dict.CONCEPT_NODE(CONCEPT_NODE_ID), CONSTRAINT fk_study FOREIGN KEY (DATASET_ID) REFERENCES dict.dataset(DATASET_ID) diff --git a/docker-compose.yml b/docker-compose.yml index 0468a8d..e13adbb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,8 +18,6 @@ services: - dictionary-db restart: always env_file: .env - ports: - - "8080:8080" networks: - dictionary - hpdsNet diff --git a/pom.xml b/pom.xml index ccad679..92114a3 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,11 @@ spring-boot-starter-test test + + org.json + json + 20240303 + org.springframework.boot spring-boot-testcontainers diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptController.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptController.java index 0402b2d..59aa992 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptController.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptController.java @@ -43,20 +43,20 @@ public ResponseEntity> listConcepts( return ResponseEntity.ok(pageResp); } - @GetMapping(path = "/concepts/detail/{dataset}/{conceptPath}") + @PostMapping(path = "/concepts/detail/{dataset}") public ResponseEntity conceptDetail( @PathVariable(name = "dataset") String dataset, - @PathVariable(name = "conceptPath") String conceptPath + @RequestBody() String conceptPath ) { return conceptService.conceptDetail(dataset, conceptPath) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } - @GetMapping(path = "/concepts/tree/{dataset}/{conceptPath}") + @PostMapping(path = "/concepts/tree/{dataset}") public ResponseEntity conceptTree( @PathVariable(name = "dataset") String dataset, - @PathVariable(name = "conceptPath") String conceptPath, + @RequestBody() String conceptPath, @RequestParam(name = "depth", required = false, defaultValue = "2") Integer depth ) { if (depth < 0 || depth > MAX_DEPTH) { diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepository.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepository.java index 6307c8e..3743710 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepository.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepository.java @@ -40,13 +40,15 @@ public List getConcepts(Filter filter, Pageable pageable) { concept_node.*, ds.REF as dataset, continuous_min.VALUE as min, continuous_max.VALUE as max, - categorical_values.VALUE as values + categorical_values.VALUE as values, + meta_description.VALUE AS description FROM concept_node LEFT JOIN dataset AS ds ON concept_node.dataset_id = ds.dataset_id - LEFT JOIN concept_node_meta AS continuous_min ON concept_node.concept_node_id = continuous_min.concept_node_id AND continuous_min.KEY = 'MIN' - LEFT JOIN concept_node_meta AS continuous_max ON concept_node.concept_node_id = continuous_max.concept_node_id AND continuous_max.KEY = 'MAX' - LEFT JOIN concept_node_meta AS categorical_values ON concept_node.concept_node_id = categorical_values.concept_node_id AND categorical_values.KEY = 'VALUES' + LEFT JOIN concept_node_meta AS meta_description ON concept_node.concept_node_id = meta_description.concept_node_id AND meta_description.KEY = 'description' + LEFT JOIN concept_node_meta AS continuous_min ON concept_node.concept_node_id = continuous_min.concept_node_id AND continuous_min.KEY = 'min' + LEFT JOIN concept_node_meta AS continuous_max ON concept_node.concept_node_id = continuous_max.concept_node_id AND continuous_max.KEY = 'max' + LEFT JOIN concept_node_meta AS categorical_values ON concept_node.concept_node_id = categorical_values.concept_node_id AND categorical_values.KEY = 'values' WHERE concept_node.concept_node_id IN """; QueryParamPair filterQ = filterGen.generateFilterQuery(filter, pageable); @@ -69,13 +71,15 @@ public Optional getConcept(String dataset, String conceptPath) { concept_node.*, ds.REF as dataset, continuous_min.VALUE as min, continuous_max.VALUE as max, - categorical_values.VALUE as values + categorical_values.VALUE as values, + meta_description.VALUE AS description FROM concept_node LEFT JOIN dataset AS ds ON concept_node.dataset_id = ds.dataset_id - LEFT JOIN concept_node_meta AS continuous_min ON concept_node.concept_node_id = continuous_min.concept_node_id AND continuous_min.KEY = 'MIN' - LEFT JOIN concept_node_meta AS continuous_max ON concept_node.concept_node_id = continuous_max.concept_node_id AND continuous_max.KEY = 'MAX' - LEFT JOIN concept_node_meta AS categorical_values ON concept_node.concept_node_id = categorical_values.concept_node_id AND categorical_values.KEY = 'VALUES' + LEFT JOIN concept_node_meta AS meta_description ON concept_node.concept_node_id = meta_description.concept_node_id AND meta_description.KEY = 'description' + LEFT JOIN concept_node_meta AS continuous_min ON concept_node.concept_node_id = continuous_min.concept_node_id AND continuous_min.KEY = 'min' + LEFT JOIN concept_node_meta AS continuous_max ON concept_node.concept_node_id = continuous_max.concept_node_id AND continuous_max.KEY = 'max' + LEFT JOIN concept_node_meta AS categorical_values ON concept_node.concept_node_id = categorical_values.concept_node_id AND categorical_values.KEY = 'values' WHERE concept_node.concept_path = :conceptPath AND ds.REF = :dataset diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRowMapper.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRowMapper.java index 7499eab..fc632d0 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRowMapper.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRowMapper.java @@ -1,8 +1,11 @@ package edu.harvard.dbmi.avillach.dictionary.concept; import edu.harvard.dbmi.avillach.dictionary.concept.model.*; +import org.json.JSONArray; +import org.json.JSONException; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import java.sql.ResultSet; import java.sql.SQLException; @@ -14,7 +17,7 @@ public class ConceptRowMapper implements RowMapper { @Override public Concept mapRow(ResultSet rs, int rowNum) throws SQLException { - return switch (ConceptType.valueOf(rs.getString("concept_type"))) { + return switch (ConceptType.toConcept(rs.getString("concept_type"))) { case Categorical -> mapCategorical(rs); case Continuous -> mapContinuous(rs); }; @@ -23,8 +26,8 @@ public Concept mapRow(ResultSet rs, int rowNum) throws SQLException { private CategoricalConcept mapCategorical(ResultSet rs) throws SQLException { return new CategoricalConcept( rs.getString("concept_path"), rs.getString("name"), - rs.getString("display"), rs.getString("dataset"), - List.of(rs.getString("values").split(",")), + rs.getString("display"), rs.getString("dataset"), rs.getString("description"), + rs.getString("values") == null ? List.of() : List.of(rs.getString("values").split(",")), null, null ); @@ -33,9 +36,27 @@ private CategoricalConcept mapCategorical(ResultSet rs) throws SQLException { private ContinuousConcept mapContinuous(ResultSet rs) throws SQLException { return new ContinuousConcept( rs.getString("concept_path"), rs.getString("name"), - rs.getString("display"), rs.getString("dataset"), - rs.getInt("min"), rs.getInt("max"), + rs.getString("display"), rs.getString("dataset"), rs.getString("description"), + parseMin(rs.getString("values")), parseMax(rs.getString("values")), null ); } + + private Integer parseMin(String valuesArr) { + try { + JSONArray arr = new JSONArray(valuesArr); + return arr.length() == 2 ? arr.getInt(0) : 0; + } catch (JSONException ex) { + return 0; + } + } + + private Integer parseMax(String valuesArr) { + try { + JSONArray arr = new JSONArray(valuesArr); + return arr.length() == 2 ? arr.getInt(1) : 0; + } catch (JSONException ex) { + return 0; + } + } } diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/CategoricalConcept.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/CategoricalConcept.java index f83e81b..26b8485 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/CategoricalConcept.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/CategoricalConcept.java @@ -1,12 +1,13 @@ package edu.harvard.dbmi.avillach.dictionary.concept.model; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.annotation.Nullable; import java.util.List; import java.util.Map; public record CategoricalConcept( - String conceptPath, String name, String display, String dataset, + String conceptPath, String name, String display, String dataset, String description, List values, @@ -19,10 +20,11 @@ public record CategoricalConcept( ) implements Concept { public CategoricalConcept(CategoricalConcept core, Map meta) { - this(core.conceptPath, core.name, core.display, core.dataset, core.values, core.children, core.meta); + this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.values, core.children, meta); } + @JsonProperty("type") @Override public ConceptType type() { return ConceptType.Categorical; diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptType.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptType.java index f8ed755..e0ec6d1 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptType.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptType.java @@ -1,5 +1,7 @@ package edu.harvard.dbmi.avillach.dictionary.concept.model; +import org.springframework.util.StringUtils; + public enum ConceptType { /** * i.e. Eye color: brown, blue, hazel, etc. @@ -10,6 +12,13 @@ public enum ConceptType { * i.e. Age: 0 - 150 * Also known as numeric (to me) */ - Continuous, + Continuous; + + public static ConceptType toConcept(String in) { + return switch (StringUtils.capitalize(in)) { + case "Continuous" -> Continuous; + default -> Categorical; + }; + } } diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ContinuousConcept.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ContinuousConcept.java index db338b3..3b6c93c 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ContinuousConcept.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ContinuousConcept.java @@ -1,20 +1,22 @@ package edu.harvard.dbmi.avillach.dictionary.concept.model; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.annotation.Nullable; import java.util.Map; public record ContinuousConcept( - String conceptPath, String name, String display, String dataset, + String conceptPath, String name, String display, String dataset, String description, @Nullable Integer min, @Nullable Integer max, Map meta ) implements Concept { public ContinuousConcept(ContinuousConcept core, Map meta) { - this(core.conceptPath, core.name, core.display, core.dataset, core.min, core.max, meta); + this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.min, core.max, meta); } + @JsonProperty("type") @Override public ConceptType type() { return ConceptType.Continuous; diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetRepository.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetRepository.java index 5a9a738..2921dec 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetRepository.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetRepository.java @@ -46,7 +46,7 @@ public List getFacets(Filter filter) { facet LEFT JOIN facet_category ON facet_category.facet_category_id = facet.facet_category_id LEFT JOIN facet as parent_facet ON facet.parent_id = parent_facet.facet_id - LEFT JOIN ( + INNER JOIN ( SELECT count(*) as facet_count, inner_facet_q.facet_id AS inner_facet_id FROM diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/filter/FilterQueryGenerator.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/filter/FilterQueryGenerator.java index a159b0d..4021a66 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/filter/FilterQueryGenerator.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/filter/FilterQueryGenerator.java @@ -34,9 +34,23 @@ public QueryParamPair generateFilterQuery(Filter filter, Pageable pageable) { if (StringUtils.hasText(filter.search())) { clauses.add(createSearchFilter(filter.search(), params)); } - if (clauses.isEmpty()) { - clauses = List.of("\tSELECT concept_node.concept_node_id FROM concept_node\n"); - } + clauses.add(""" + ( + SELECT + concept_node.concept_node_id, 0 as rank + FROM + concept_node + LEFT JOIN concept_node_meta AS continuous_min ON concept_node.concept_node_id = continuous_min.concept_node_id AND continuous_min.KEY = 'min' + LEFT JOIN concept_node_meta AS continuous_max ON concept_node.concept_node_id = continuous_max.concept_node_id AND continuous_max.KEY = 'max' + LEFT JOIN concept_node_meta AS categorical_values ON concept_node.concept_node_id = categorical_values.concept_node_id AND categorical_values.KEY = 'values' + WHERE + continuous_min.value <> '' OR + continuous_max.value <> '' OR + categorical_values.value <> '' + ) + """ + ); + String query = "(\n" + String.join("\n\tINTERSECT\n", clauses) + "\n) ORDER BY concept_node_id\n"; if (pageable.isPaged()) { @@ -48,20 +62,25 @@ public QueryParamPair generateFilterQuery(Filter filter, Pageable pageable) { .addValue("offset", pageable.getOffset()); } + String superQuery = """ + WITH q AS (%s) SELECT concept_node_id FROM q GROUP BY concept_node_id ORDER BY sum(rank) DESC" + """.formatted(query); + - return new QueryParamPair(query, params); + return new QueryParamPair(superQuery, params); } private String createSearchFilter(String search, MapSqlParameterSource params) { - params.addValue("search", "%" + search + "%"); + params.addValue("search", search); return """ ( SELECT - concept_node.concept_node_id AS concept_node_id + concept_node.concept_node_id AS concept_node_id, + ts_rank(searchable_fields, (phraseto_tsquery(:search)::text || ':*')::tsquery) as rank FROM concept_node WHERE - concept_node.concept_path LIKE :search + concept_node.searchable_fields @@ (phraseto_tsquery(:search)::text || ':*')::tsquery ) """; } @@ -78,7 +97,7 @@ private List createFacetFilter(List facets, MapSqlParameterSource return """ ( SELECT - facet__concept_node.concept_node_id AS concept_node_id + facet__concept_node.concept_node_id AS concept_node_id , 0 as rank FROM facet LEFT JOIN facet__concept_node ON facet__concept_node.facet_id = facet.facet_id LEFT JOIN facet_category ON facet_category.facet_category_id = facet.facet_category_id diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/util/MapExtractor.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/util/MapExtractor.java index 910a74c..0f1b1fa 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/util/MapExtractor.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/util/MapExtractor.java @@ -19,7 +19,7 @@ public MapExtractor(String keyName, String valueName) { @Override public Map extractData(ResultSet rs) throws SQLException, DataAccessException { Map map = new HashMap<>(); - while (rs.next()) { + while (rs.next() && rs.getString(keyName) != null) { map.put(rs.getString(keyName), rs.getString(valueName)); } return map; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4982ace..19d18da 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,7 @@ spring.application.name=dictionary -spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST}:5432/${POSTGRES_DB} +spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST}:5432/${POSTGRES_DB}?currentSchema=dict spring.datasource.username=${POSTGRES_USER} spring.datasource.password=${POSTGRES_PASSWORD} -spring.datasource.driver-class-name=org.postgresql.Driver \ No newline at end of file +spring.datasource.driver-class-name=org.postgresql.Driver +server.port=80 + diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptControllerTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptControllerTest.java index b6baef6..878fe3f 100644 --- a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptControllerTest.java +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptControllerTest.java @@ -32,9 +32,9 @@ class ConceptControllerTest { @Test void shouldListConcepts() { List expected = List.of( - new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", List.of(), null, Map.of()), - new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", List.of("a", "b"), List.of(), Map.of()), - new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", 0, 100, Map.of()) + new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", "foo!", List.of(), null, Map.of()), + new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", "foo!", List.of("a", "b"), List.of(), Map.of()), + new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", "foo!", 0, 100, Map.of()) ); Filter filter = new Filter( List.of(new Facet("questionare", "Questionare", "?", 1, null, "category", null)), @@ -54,7 +54,7 @@ void shouldListConcepts() { @Test void shouldGetConceptDetails() { CategoricalConcept expected = - new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", List.of("a", "b"), List.of(), Map.of()); + new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", "foo!", List.of("a", "b"), List.of(), Map.of()); Mockito.when(conceptService.conceptDetail("my_dataset", "/foo//bar")) .thenReturn(Optional.of(expected)); @@ -77,11 +77,11 @@ void shouldNotGetConceptDetails() { @Test void shouldGetConceptTree() { Concept fooBar = - new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", List.of("a", "b"), List.of(), Map.of()); + new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", "foo!", List.of("a", "b"), List.of(), Map.of()); Concept fooBaz = - new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", 0, 100, Map.of()); + new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", "foo!", 0, 100, Map.of()); CategoricalConcept foo = - new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", List.of(), List.of(fooBar, fooBaz), Map.of()); + new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", "foo!", List.of(), List.of(fooBar, fooBaz), Map.of()); Mockito.when(conceptService.conceptTree("my_dataset", "/foo", 1)) .thenReturn(Optional.of(foo)); @@ -95,11 +95,11 @@ void shouldGetConceptTree() { @Test void shouldGetNotConceptTreeForLargeDepth() { Concept fooBar = - new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", List.of("a", "b"), List.of(), Map.of()); + new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", "foo!", List.of("a", "b"), List.of(), Map.of()); Concept fooBaz = - new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", 0, 100, Map.of()); + new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", "foo!", 0, 100, Map.of()); CategoricalConcept foo = - new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", List.of(), List.of(fooBar, fooBaz), Map.of()); + new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", "foo!", List.of(), List.of(fooBar, fooBaz), Map.of()); Mockito.when(conceptService.conceptTree("my_dataset", "/foo", 1)) .thenReturn(Optional.of(foo)); @@ -113,11 +113,11 @@ void shouldGetNotConceptTreeForLargeDepth() { @Test void shouldGetNotConceptTreeForNegativeDepth() { Concept fooBar = - new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", List.of("a", "b"), List.of(), Map.of()); + new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", "foo!", List.of("a", "b"), List.of(), Map.of()); Concept fooBaz = - new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", 0, 100, Map.of()); + new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", "foo!", 0, 100, Map.of()); CategoricalConcept foo = - new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", List.of(), List.of(fooBar, fooBaz), Map.of()); + new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", "foo!", List.of(), List.of(fooBar, fooBaz), Map.of()); Mockito.when(conceptService.conceptTree("my_dataset", "/foo", -1)) .thenReturn(Optional.of(foo)); @@ -130,11 +130,11 @@ void shouldGetNotConceptTreeForNegativeDepth() { @Test void shouldNotGetConceptTreeWhenConceptDNE() { Concept fooBar = - new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", List.of("a", "b"), List.of(), Map.of()); + new CategoricalConcept("/foo//bar", "bar", "Bar", "my_dataset", "foo!", List.of("a", "b"), List.of(), Map.of()); Concept fooBaz = - new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", 0, 100, Map.of()); + new ContinuousConcept("/foo//baz", "baz", "Baz", "my_dataset", "foo!", 0, 100, Map.of()); CategoricalConcept foo = - new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", List.of(), List.of(fooBar, fooBaz), Map.of()); + new CategoricalConcept("/foo", "foo", "Foo", "my_dataset", "foo!", List.of(), List.of(fooBar, fooBaz), Map.of()); Mockito.when(conceptService.conceptTree("my_dataset", "/foo", 1)) .thenReturn(Optional.of(foo)); diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepositoryTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepositoryTest.java index ba46693..7ea3b5a 100644 --- a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepositoryTest.java +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepositoryTest.java @@ -48,21 +48,21 @@ static void mySQLProperties(DynamicPropertyRegistry registry) { void shouldListAllConcepts() { List actual = subject.getConcepts(new Filter(List.of(), ""), Pageable.unpaged()); List expected = List.of( - new CategoricalConcept("\\\\\\\\A\\\\\\\\", "a", "A", "invalid.invalid", List.of("0", "1"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\", "1", "1", "invalid.invalid", List.of("X", "Z"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", List.of("X", "Y"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", List.of("foo", "bar"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", List.of("foo", "bar", "baz"), null, null), - new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", 0, 0, null), - new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", 0, 0, null), - new CategoricalConcept("\\\\\\\\B\\\\\\\\", "b", "B", "invalid.invalid", List.of("0", "2"), null, null), - new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", List.of("X", "Y", "Z"), null, null), - new CategoricalConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\", "2", "2", "invalid.invalid", List.of("Y", "Z"), null, null), - new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", List.of("bar", "baz"), null, null), - new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", List.of("bar", "baz", "qux"), null, null), - new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", List.of("foo", "bar", "baz", "qux"), null, null), - new ContinuousConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", 0, 0, null), - new ContinuousConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", 0, 0, null) + new CategoricalConcept("\\\\\\\\A\\\\\\\\", "a", "A", "invalid.invalid", null, List.of("0", "1"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\", "1", "1", "invalid.invalid", null, List.of("X", "Z"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", null, List.of("X", "Y"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, List.of("foo", "bar"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", null, List.of("foo", "bar", "baz"), null, null), + new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, 0, 0, null), + new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", null, 0, 0, null), + new CategoricalConcept("\\\\\\\\B\\\\\\\\", "b", "B", "invalid.invalid", null, List.of("0", "2"), null, null), + new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", null, List.of("X", "Y", "Z"), null, null), + new CategoricalConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\", "2", "2", "invalid.invalid", null, List.of("Y", "Z"), null, null), + new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, List.of("bar", "baz"), null, null), + new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", null, List.of("bar", "baz", "qux"), null, null), + new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", null, List.of("foo", "bar", "baz", "qux"), null, null), + new ContinuousConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", null, 0, 0, null), + new ContinuousConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", null, 0, 0, null) ); Assertions.assertEquals(expected, actual); @@ -72,8 +72,8 @@ void shouldListAllConcepts() { void shouldListFirstTwoConcepts() { List actual = subject.getConcepts(new Filter(List.of(), ""), Pageable.ofSize(2).first()); List expected = List.of( - new CategoricalConcept("\\\\\\\\A\\\\\\\\", "a", "A", "invalid.invalid", List.of("0", "1"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\", "1", "1", "invalid.invalid", List.of("X", "Z"), null, null) + new CategoricalConcept("\\\\\\\\A\\\\\\\\", "a", "A", "invalid.invalid", null, List.of("0", "1"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\", "1", "1", "invalid.invalid", null, List.of("X", "Z"), null, null) ); Assertions.assertEquals(expected, actual); @@ -83,8 +83,8 @@ void shouldListFirstTwoConcepts() { void shouldListNextTwoConcepts() { List actual = subject.getConcepts(new Filter(List.of(), ""), Pageable.ofSize(2).first().next()); List expected = List.of( - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", List.of("X", "Y"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", List.of("foo", "bar"), null, null) + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", null, List.of("X", "Y"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, List.of("foo", "bar"), null, null) ); Assertions.assertEquals(expected, actual); @@ -95,13 +95,13 @@ void shouldFilterConceptsByFacet() { List actual = subject.getConcepts(new Filter(List.of(new Facet("bch", "", "", 1, null, "site", null)), ""), Pageable.unpaged()); List expected = List.of( - new CategoricalConcept("\\\\\\\\A\\\\\\\\", "a", "A", "invalid.invalid", List.of("0", "1"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\", "1", "1", "invalid.invalid", List.of("X", "Z"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", List.of("X", "Y"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", List.of("foo", "bar"), null, null), - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", List.of("foo", "bar", "baz"), null, null), - new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", 0, 0, null), - new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", 0, 0, null) + new CategoricalConcept("\\\\\\\\A\\\\\\\\", "a", "A", "invalid.invalid", null, List.of("0", "1"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\", "1", "1", "invalid.invalid", null, List.of("X", "Z"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\", "0", "0", "invalid.invalid", null, List.of("X", "Y"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, List.of("foo", "bar"), null, null), + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\Y\\\\\\\\", "y", "Y", "invalid.invalid", null, List.of("foo", "bar", "baz"), null, null), + new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, 0, 0, null), + new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", null, 0, 0, null) ); Assertions.assertEquals(expected, actual); @@ -111,9 +111,9 @@ void shouldFilterConceptsByFacet() { void shouldFilterBySearch() { List actual = subject.getConcepts(new Filter(List.of(), "X"), Pageable.unpaged()); List expected = List.of( - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", List.of("foo", "bar"), null, null), - new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", 0, 0, null), - new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", List.of("bar", "baz"), null, null) + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, List.of("foo", "bar"), null, null), + new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, 0, 0, null), + new CategoricalConcept("\\\\\\\\B\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, List.of("bar", "baz"), null, null) ); Assertions.assertEquals(expected, actual); @@ -124,8 +124,8 @@ void shouldFilterByBothSearchAndFacet() { List actual = subject.getConcepts(new Filter(List.of(new Facet("bch", "", "", 1, null, "site", null)), "X"), Pageable.unpaged()); List expected = List.of( - new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", List.of("foo", "bar"), null, null), - new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", 0, 0, null) + new CategoricalConcept("\\\\\\\\A\\\\\\\\0\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, List.of("foo", "bar"), null, null), + new ContinuousConcept("\\\\\\\\A\\\\\\\\1\\\\\\\\X\\\\\\\\", "x", "X", "invalid.invalid", null, 0, 0, null) ); Assertions.assertEquals(expected, actual); @@ -147,7 +147,7 @@ void shouldGetCountWithFilter() { @Test void shouldGetDetailForConcept() { ContinuousConcept expected = - new ContinuousConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", 0, 0, null); + new ContinuousConcept("\\\\\\\\B\\\\\\\\2\\\\\\\\Z\\\\\\\\", "z", "Z", "invalid.invalid", null, 0, 0, null); Optional actual = subject.getConcept(expected.dataset(), expected.conceptPath()); Assertions.assertEquals(Optional.of(expected), actual); diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java index 22113cc..f47920f 100644 --- a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java @@ -29,7 +29,7 @@ class ConceptServiceTest { @Test void shouldListConcepts() { List expected = List.of( - new CategoricalConcept("A", "a", "A", "invalid.invalid", List.of(), null, null) + new CategoricalConcept("A", "a", "A", "invalid.invalid", null, List.of(), null, null) ); Filter filter = new Filter(List.of(), ""); Pageable page = Pageable.ofSize(10).first(); @@ -54,7 +54,7 @@ void shouldCountConcepts() { @Test void shouldShowDetailForContinuous() { - ContinuousConcept concept = new ContinuousConcept("path", "", "", "dataset", 0, 1, null); + ContinuousConcept concept = new ContinuousConcept("path", "", "", "dataset", null, 0, 1, null); Map meta = Map.of("MIN", "0", "MAX", "1", "stigmatizing", "true"); Mockito.when(repository.getConcept("dataset", "path")) .thenReturn(Optional.of(concept)); @@ -69,7 +69,7 @@ void shouldShowDetailForContinuous() { @Test void shouldShowDetailForCategorical() { - CategoricalConcept concept = new CategoricalConcept("path", "", "", "dataset", List.of("a"), List.of(), null); + CategoricalConcept concept = new CategoricalConcept("path", "", "", "dataset", null, List.of("a"), List.of(), null); Map meta = Map.of("VALUES", "a", "stigmatizing", "true"); Mockito.when(repository.getConcept("dataset", "path")) .thenReturn(Optional.of(concept)); diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTest.java index 45ac68e..c48b501 100644 --- a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTest.java +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTest.java @@ -14,7 +14,7 @@ class ConceptTest { @Test void shouldRoundTrip() throws JsonProcessingException { - Concept expected = new CategoricalConcept("/foo//bar", "bar", "Bar", "study_a", List.of("a", "b"), List.of(), Map.of()); + Concept expected = new CategoricalConcept("/foo//bar", "bar", "Bar", "study_a", null, List.of("a", "b"), List.of(), Map.of()); String json = objectMapper.writeValueAsString(expected); Concept actual = objectMapper.readValue(json, Concept.class); @@ -37,7 +37,7 @@ void shouldReadCategorical() throws JsonProcessingException { } """; - CategoricalConcept expected = new CategoricalConcept("/foo//bar", "bar", "Bar", "study_a", List.of("a", "b"), null, Map.of()); + CategoricalConcept expected = new CategoricalConcept("/foo//bar", "bar", "Bar", "study_a", null, List.of("a", "b"), null, Map.of()); Concept actual = new ObjectMapper().readValue(json, Concept.class); Assertions.assertEquals(expected, actual); @@ -60,10 +60,23 @@ void shouldReadContinuous() throws JsonProcessingException { } """; - ContinuousConcept expected = new ContinuousConcept("/foo//baz", "baz", "Baz", "study_a", 0, 1, Map.of()); + ContinuousConcept expected = new ContinuousConcept("/foo//baz", "baz", "Baz", "study_a", null, 0, 1, Map.of()); Concept actual = new ObjectMapper().readValue(json, Concept.class); Assertions.assertEquals(expected, actual); Assertions.assertEquals(ConceptType.Continuous, actual.type()); } + + @Test + void shouldIncludeTypeInList() throws JsonProcessingException { + List concepts = List.of( + new ContinuousConcept("/foo//baz", "baz", "Baz", "study_a", null, 0, 1, Map.of()), + new CategoricalConcept("/foo//bar", "bar", "Bar", "study_a", null, List.of("a", "b"), null, Map.of()) + ); + + String actual = new ObjectMapper().writeValueAsString(concepts); + String expected = ""; + + Assertions.assertEquals(expected, actual); + } } \ No newline at end of file diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTypeTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTypeTest.java new file mode 100644 index 0000000..660f1a3 --- /dev/null +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTypeTest.java @@ -0,0 +1,17 @@ +package edu.harvard.dbmi.avillach.dictionary.concept.model; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.util.StringUtils; + + +class ConceptTypeTest { + + @Test + void shouldGetValueOf() { + ConceptType actual = ConceptType.valueOf(StringUtils.capitalize("categorical")); + ConceptType expected = ConceptType.Categorical; + + Assertions.assertEquals(expected, actual); + } +} \ No newline at end of file