Skip to content

Commit ee546be

Browse files
author
Luke Sikina
committed
[ALS-7336] Show specific ancestors in details
- Lots of weird BDC stuff. We decorate the concept with them
1 parent b414666 commit ee546be

File tree

12 files changed

+242
-6
lines changed

12 files changed

+242
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package edu.harvard.dbmi.avillach.dictionary;
2+
3+
import edu.harvard.dbmi.avillach.dictionary.concept.ConceptService;
4+
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.context.annotation.Lazy;
10+
import org.springframework.stereotype.Service;
11+
12+
import java.util.List;
13+
14+
@Service
15+
public class ConceptDecoratorService {
16+
17+
private static final Logger LOG = LoggerFactory.getLogger(ConceptDecoratorService.class);
18+
private final boolean enabled;
19+
private final ConceptService conceptService;
20+
21+
private static final int COMPLIANT = 4, NON_COMPLIANT_TABLED = 3, NON_COMPLIANT_UNTABLED = 2;
22+
23+
@Autowired
24+
public ConceptDecoratorService(
25+
@Value("${dashboard.enable.extra_details}") boolean enabled,
26+
@Lazy ConceptService conceptService // circular dep
27+
) {
28+
this.enabled = enabled;
29+
this.conceptService = conceptService;
30+
}
31+
32+
33+
public Concept populateParentConcepts(Concept concept) {
34+
if (!enabled) {
35+
return concept;
36+
}
37+
38+
// In some environments, certain parent concepts have critical details that we need to add to the detailed response
39+
List<String> conceptNodes = List.of(concept.conceptPath()
40+
.split("\\\\")); // you have to double escape the slash. Once for strings, and once for regex
41+
42+
return switch (conceptNodes.size()) {
43+
case COMPLIANT, NON_COMPLIANT_TABLED -> populateTabledConcept(concept, conceptNodes);
44+
case NON_COMPLIANT_UNTABLED -> populateNonCompliantTabledConcept(concept, conceptNodes);
45+
default -> {
46+
LOG.warn("Ignoring decoration request for weird concept path {}", concept.conceptPath());
47+
yield concept;
48+
}
49+
};
50+
}
51+
52+
private Concept populateTabledConcept(Concept concept, List<String> conceptNodes) {
53+
String studyPath = String.join("\\", conceptNodes.subList(0, 1)) + "\\";
54+
String tablePath = String.join("\\", conceptNodes.subList(0, 2)) + "\\";
55+
Concept study = conceptService.conceptDetail(concept.dataset(), studyPath).orElse(null);
56+
Concept table = conceptService.conceptDetail(concept.dataset(), tablePath).orElse(null);
57+
return concept.withStudy(study).withTable(table);
58+
}
59+
60+
private Concept populateNonCompliantTabledConcept(Concept concept, List<String> conceptNodes) {
61+
String studyPath = String.join("\\", conceptNodes.subList(0, 1));
62+
Concept study = conceptService.conceptDetail(concept.dataset(), studyPath).orElse(null);
63+
return concept.withStudy(study);
64+
}
65+
}

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptService.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package edu.harvard.dbmi.avillach.dictionary.concept;
22

3+
import edu.harvard.dbmi.avillach.dictionary.ConceptDecoratorService;
34
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
45
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
56
import edu.harvard.dbmi.avillach.dictionary.concept.model.ConceptShell;
@@ -20,9 +21,12 @@ public class ConceptService {
2021

2122
private final ConceptRepository conceptRepository;
2223

24+
private final ConceptDecoratorService conceptDecoratorService;
25+
2326
@Autowired
24-
public ConceptService(ConceptRepository conceptRepository) {
27+
public ConceptService(ConceptRepository conceptRepository, ConceptDecoratorService conceptDecoratorService) {
2528
this.conceptRepository = conceptRepository;
29+
this.conceptDecoratorService = conceptDecoratorService;
2630
}
2731

2832
public List<Concept> listConcepts(Filter filter, Pageable page) {
@@ -53,7 +57,7 @@ public Optional<Concept> conceptDetail(String dataset, String conceptPath) {
5357
case ConceptShell ignored -> throw new RuntimeException("Concept shell escaped to API");
5458
};
5559
}
56-
);
60+
).map(conceptDecoratorService::populateParentConcepts);
5761
}
5862

5963
public Optional<Concept> conceptTree(String dataset, String conceptPath, int depth) {

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/CategoricalConcept.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,23 @@ public record CategoricalConcept(
1616
List<Concept> children,
1717

1818
@Nullable
19-
Map<String, String> meta
19+
Map<String, String> meta,
20+
21+
@Nullable
22+
Concept table,
23+
24+
@Nullable
25+
Concept study
2026

2127
) implements Concept {
2228

29+
public CategoricalConcept(
30+
String conceptPath, String name, String display, String dataset, String description, List<String> values,
31+
@Nullable List<Concept> children, @Nullable Map<String, String> meta
32+
) {
33+
this(conceptPath, name, display, dataset, description, values, children, meta, null, null);
34+
}
35+
2336
public CategoricalConcept(CategoricalConcept core, Map<String, String> meta) {
2437
this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.values, core.children, meta);
2538
}
@@ -44,6 +57,20 @@ public CategoricalConcept withChildren(List<Concept> children) {
4457
return new CategoricalConcept(this, children);
4558
}
4659

60+
@Override
61+
public Concept withTable(Concept table) {
62+
return new CategoricalConcept(
63+
conceptPath, name, display, dataset, description, values, children, meta, table, study
64+
);
65+
}
66+
67+
@Override
68+
public Concept withStudy(Concept study) {
69+
return new CategoricalConcept(
70+
conceptPath, name, display, dataset, description, values, children, meta, table, study
71+
);
72+
}
73+
4774
@Override
4875
public boolean equals(Object object) {
4976
return conceptEquals(object);

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/Concept.java

+8
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,21 @@ public sealed interface Concept
4949
*/
5050
ConceptType type();
5151

52+
Concept table();
53+
54+
Concept study();
55+
5256
Map<String, String> meta();
5357

5458
@Nullable
5559
List<Concept> children();
5660

5761
Concept withChildren(List<Concept> children);
5862

63+
Concept withTable(Concept table);
64+
65+
Concept withStudy(Concept study);
66+
5967
default boolean conceptEquals(Object object) {
6068
if (this == object) return true;
6169
if (!(object instanceof Concept)) return false;

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptShell.java

+20
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ public ConceptType type() {
2222
return ConceptType.Continuous;
2323
}
2424

25+
@Override
26+
public Concept table() {
27+
return null;
28+
}
29+
30+
@Override
31+
public Concept study() {
32+
return null;
33+
}
34+
2535
@Override
2636
public Map<String, String> meta() {
2737
return Map.of();
@@ -37,6 +47,16 @@ public ConceptShell withChildren(List<Concept> children) {
3747
return this;
3848
}
3949

50+
@Override
51+
public Concept withTable(Concept table) {
52+
return this;
53+
}
54+
55+
@Override
56+
public Concept withStudy(Concept study) {
57+
return this;
58+
}
59+
4060
@Override
4161
public boolean equals(Object object) {
4262
return conceptEquals(object);

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ContinuousConcept.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,22 @@ public record ContinuousConcept(
1414
@Nullable Integer min, @Nullable Integer max,
1515
Map<String, String> meta,
1616
@Nullable
17-
List<Concept> children
17+
List<Concept> children,
18+
19+
@Nullable
20+
Concept table,
21+
22+
@Nullable
23+
Concept study
1824
) implements Concept {
1925

26+
public ContinuousConcept(
27+
String conceptPath, String name, String display, String dataset, String description,
28+
@Nullable Integer min, @Nullable Integer max, Map<String, String> meta, @Nullable List<Concept> children
29+
) {
30+
this(conceptPath, name, display, dataset, description, min, max, meta, children, null, null);
31+
}
32+
2033
public ContinuousConcept(ContinuousConcept core, Map<String, String> meta) {
2134
this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.min, core.max, meta, core.children);
2235
}
@@ -47,6 +60,20 @@ public ContinuousConcept withChildren(List<Concept> children) {
4760
return new ContinuousConcept(this, children);
4861
}
4962

63+
@Override
64+
public Concept withTable(Concept table) {
65+
return new ContinuousConcept(
66+
conceptPath, name, display, dataset, description, min, max, meta, children, table, study
67+
);
68+
}
69+
70+
@Override
71+
public Concept withStudy(Concept study) {
72+
return new ContinuousConcept(
73+
conceptPath, name, display, dataset, description, min, max, meta, children, table, study
74+
);
75+
}
76+
5077
@Override
5178
public boolean equals(Object object) {
5279
return conceptEquals(object);

src/main/resources/application-bdc.properties

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ spring.datasource.driver-class-name=com.amazonaws.secretsmanager.sql.AWSSecretsM
44
spring.datasource.url=jdbc-secretsmanager:postgresql://${DATASOURCE_URL}/picsure?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&autoReconnectForPools=true&currentSchema=dict
55
spring.datasource.username=${DATASOURCE_USERNAME}
66
server.port=80
7+
8+
dashboard.enable.extra_details=true

src/main/resources/application.properties

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ server.port=80
77

88
dashboard.columns={abbreviation:'Abbreviation',name:'Name',clinvars:'Clinical Variables'}
99
dashboard.column-order=abbreviation,name,clinvars
10-
dashboard.nonmeta-columns=abbreviation,name
10+
dashboard.nonmeta-columns=abbreviation,name
11+
dashboard.enable.extra_details=true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package edu.harvard.dbmi.avillach.dictionary;
2+
3+
import edu.harvard.dbmi.avillach.dictionary.concept.ConceptService;
4+
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
5+
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
6+
import org.junit.jupiter.api.Assertions;
7+
import org.junit.jupiter.api.Test;
8+
import org.mockito.Mockito;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.boot.test.mock.mockito.MockBean;
12+
13+
import java.util.Optional;
14+
15+
16+
@SpringBootTest
17+
class ConceptDecoratorServiceTest {
18+
19+
@MockBean
20+
ConceptService conceptService;
21+
22+
@Autowired
23+
ConceptDecoratorService subject;
24+
25+
@Test
26+
void shouldPopulateCompliantStudy() {
27+
CategoricalConcept concept = new CategoricalConcept("\\study\\table\\idk\\concept\\", "dataset");
28+
CategoricalConcept table = new CategoricalConcept("\\study\\table\\", "dataset");
29+
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");
30+
31+
Mockito.when(conceptService.conceptDetail("dataset", table.dataset()))
32+
.thenReturn(Optional.of(table));
33+
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
34+
.thenReturn(Optional.of(study));
35+
36+
Concept actual = subject.populateParentConcepts(concept);
37+
Concept expected = concept.withStudy(study).withTable(table);
38+
39+
Assertions.assertEquals(expected, actual);
40+
}
41+
42+
@Test
43+
void shouldPopulateNonCompliantTabledStudy() {
44+
CategoricalConcept concept = new CategoricalConcept("\\study\\table\\concept\\", "dataset");
45+
CategoricalConcept table = new CategoricalConcept("\\study\\table\\", "dataset");
46+
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");
47+
48+
Mockito.when(conceptService.conceptDetail("dataset", table.dataset()))
49+
.thenReturn(Optional.of(table));
50+
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
51+
.thenReturn(Optional.of(study));
52+
53+
Concept actual = subject.populateParentConcepts(concept);
54+
Concept expected = concept.withStudy(study).withTable(table);
55+
56+
Assertions.assertEquals(expected, actual);
57+
}
58+
59+
@Test
60+
void shouldPopulateNonCompliantUnTabledStudy() {
61+
CategoricalConcept concept = new CategoricalConcept("\\study\\concept\\", "dataset");
62+
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");
63+
64+
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
65+
.thenReturn(Optional.of(study));
66+
67+
Concept actual = subject.populateParentConcepts(concept);
68+
Concept expected = concept.withStudy(study);
69+
70+
Assertions.assertEquals(expected, actual);
71+
}
72+
}

src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package edu.harvard.dbmi.avillach.dictionary.concept;
22

3+
import edu.harvard.dbmi.avillach.dictionary.ConceptDecoratorService;
34
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
45
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
56
import edu.harvard.dbmi.avillach.dictionary.concept.model.ConceptShell;
@@ -24,6 +25,9 @@ class ConceptServiceTest {
2425
@MockBean
2526
ConceptRepository repository;
2627

28+
@MockBean
29+
ConceptDecoratorService decoratorService;
30+
2731
@Autowired
2832
ConceptService subject;
2933

@@ -59,6 +63,8 @@ void shouldShowDetailForContinuous() {
5963
Map<String, String> meta = Map.of("MIN", "0", "MAX", "1", "stigmatizing", "true");
6064
Mockito.when(repository.getConcept("dataset", "path"))
6165
.thenReturn(Optional.of(concept));
66+
Mockito.when(decoratorService.populateParentConcepts(Mockito.any()))
67+
.thenAnswer(i -> i.getArguments()[0]);
6268
Mockito.when(repository.getConceptMeta("dataset", "path"))
6369
.thenReturn(meta);
6470

@@ -74,6 +80,8 @@ void shouldShowDetailForCategorical() {
7480
Map<String, String> meta = Map.of("VALUES", "a", "stigmatizing", "true");
7581
Mockito.when(repository.getConcept("dataset", "path"))
7682
.thenReturn(Optional.of(concept));
83+
Mockito.when(decoratorService.populateParentConcepts(Mockito.any()))
84+
.thenAnswer(i -> i.getArguments()[0]);
7785
Mockito.when(repository.getConceptMeta("dataset", "path"))
7886
.thenReturn(meta);
7987

src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void shouldIncludeTypeInList() throws JsonProcessingException {
7575
);
7676

7777
String actual = new ObjectMapper().writeValueAsString(concepts);
78-
String expected = "[{\"conceptPath\":\"/foo//baz\",\"name\":\"baz\",\"display\":\"Baz\",\"dataset\":\"study_a\",\"description\":null,\"min\":0,\"max\":1,\"meta\":{},\"children\":null,\"type\":\"Continuous\"},{\"conceptPath\":\"/foo//bar\",\"name\":\"bar\",\"display\":\"Bar\",\"dataset\":\"study_a\",\"description\":null,\"values\":[\"a\",\"b\"],\"children\":null,\"meta\":{},\"type\":\"Categorical\"}]";
78+
String expected = "[{\"conceptPath\":\"/foo//baz\",\"name\":\"baz\",\"display\":\"Baz\",\"dataset\":\"study_a\",\"description\":null,\"min\":0,\"max\":1,\"meta\":{},\"children\":null,\"table\":null,\"study\":null,\"type\":\"Continuous\"},{\"conceptPath\":\"/foo//bar\",\"name\":\"bar\",\"display\":\"Bar\",\"dataset\":\"study_a\",\"description\":null,\"values\":[\"a\",\"b\"],\"children\":null,\"meta\":{},\"table\":null,\"study\":null,\"type\":\"Categorical\"}]";
7979

8080
Assertions.assertEquals(expected, actual);
8181
}

src/test/resources/application.properties

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ spring.datasource.driver-class-name=org.postgresql.Driver
77
dashboard.columns={abbreviation:'Abbreviation',melast:'This one goes last',name:'Name',clinvars:'Clinical Variables',participants:'Participants'}
88
dashboard.column-order=abbreviation,name,clinvars
99
dashboard.nonmeta-columns=abbreviation,name
10+
11+
dashboard.enable.extra_details=true

0 commit comments

Comments
 (0)