Skip to content

Commit 807aba8

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 a26a253 commit 807aba8

16 files changed

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

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

+22-12
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@
1212
import java.util.List;
1313
import java.util.Map;
1414
import java.util.Optional;
15-
import java.util.function.Function;
16-
import java.util.stream.Collectors;
1715

1816
@Service
1917
public class ConceptService {
2018

2119
private final ConceptRepository conceptRepository;
2220

21+
private final ConceptDecoratorService conceptDecoratorService;
22+
2323
@Autowired
24-
public ConceptService(ConceptRepository conceptRepository) {
24+
public ConceptService(ConceptRepository conceptRepository, ConceptDecoratorService conceptDecoratorService) {
2525
this.conceptRepository = conceptRepository;
26+
this.conceptDecoratorService = conceptDecoratorService;
2627
}
2728

2829
public List<Concept> listConcepts(Filter filter, Pageable page) {
@@ -44,19 +45,28 @@ public long countConcepts(Filter filter) {
4445
}
4546

4647
public Optional<Concept> conceptDetail(String dataset, String conceptPath) {
47-
return conceptRepository.getConcept(dataset, conceptPath)
48+
return getConcept(dataset, conceptPath, true);
49+
}
50+
51+
private Optional<Concept> getConcept(String dataset, String conceptPath, boolean addAncestors) {
52+
Optional<Concept> concept = conceptRepository.getConcept(dataset, conceptPath)
4853
.map(core -> {
49-
var meta = conceptRepository.getConceptMeta(dataset, conceptPath);
50-
return switch (core) {
51-
case ContinuousConcept cont -> new ContinuousConcept(cont, meta);
52-
case CategoricalConcept cat -> new CategoricalConcept(cat, meta);
53-
case ConceptShell ignored -> throw new RuntimeException("Concept shell escaped to API");
54-
};
55-
}
56-
);
54+
var meta = conceptRepository.getConceptMeta(dataset, conceptPath);
55+
return switch (core) {
56+
case ContinuousConcept cont -> new ContinuousConcept(cont, meta);
57+
case CategoricalConcept cat -> new CategoricalConcept(cat, meta);
58+
case ConceptShell ignored -> throw new RuntimeException("Concept shell escaped to API");
59+
};
60+
}
61+
);
62+
return addAncestors ? concept.map(conceptDecoratorService::populateParentConcepts) : concept;
5763
}
5864

5965
public Optional<Concept> conceptTree(String dataset, String conceptPath, int depth) {
6066
return conceptRepository.getConceptTree(dataset, conceptPath, depth);
6167
}
68+
69+
public Optional<Concept> conceptDetailWithoutAncestors(String dataset, String conceptPath) {
70+
return getConcept(dataset, conceptPath, false);
71+
}
6272
}

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/java/edu/harvard/dbmi/avillach/dictionary/dashboard/DashboardConfig.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public List<DashboardColumn> getColumns() {
3131
}
3232

3333
private int calculateOrder(DashboardColumn column) {
34-
if (columnOrder.contains(column.label())) {
35-
return columnOrder.indexOf(column.label());
34+
if (columnOrder.contains(column.dataElement())) {
35+
return columnOrder.indexOf(column.dataElement());
3636
} else {
3737
return Integer.MAX_VALUE;
3838
}

src/main/java/edu/harvard/dbmi/avillach/dictionary/dashboard/DashboardRowResultSetExtractor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class DashboardRowResultSetExtractor implements ResultSetExtractor<List<M
2323
@Autowired
2424
public DashboardRowResultSetExtractor(List<DashboardColumn> columns) {
2525
template = columns.stream()
26-
.collect(Collectors.toMap(DashboardColumn::label, (ignored) -> ""));
26+
.collect(Collectors.toMap(DashboardColumn::dataElement, (ignored) -> ""));
2727
}
2828

2929
@Override

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,79 @@
1+
package edu.harvard.dbmi.avillach.dictionary.concept;
2+
3+
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
4+
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.Test;
7+
import org.mockito.Mockito;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.context.SpringBootTest;
10+
import org.springframework.boot.test.mock.mockito.MockBean;
11+
12+
import java.util.Optional;
13+
14+
15+
@SpringBootTest
16+
class ConceptDecoratorServiceTest {
17+
18+
@MockBean
19+
ConceptService conceptService;
20+
21+
@Autowired
22+
ConceptDecoratorService subject;
23+
24+
@Test
25+
void shouldPopulateCompliantStudy() {
26+
CategoricalConcept concept = new CategoricalConcept("\\study\\table\\idk\\concept\\", "dataset");
27+
CategoricalConcept table = new CategoricalConcept("\\study\\table\\", "dataset");
28+
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");
29+
30+
Mockito.when(conceptService.conceptDetail("dataset", table.dataset()))
31+
.thenReturn(Optional.of(table));
32+
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
33+
.thenReturn(Optional.of(study));
34+
35+
Concept actual = subject.populateParentConcepts(concept);
36+
Concept expected = concept.withStudy(study).withTable(table);
37+
38+
Assertions.assertEquals(expected, actual);
39+
}
40+
41+
@Test
42+
void shouldPopulateNonCompliantTabledStudy() {
43+
CategoricalConcept concept = new CategoricalConcept("\\study\\table\\concept\\", "dataset");
44+
CategoricalConcept table = new CategoricalConcept("\\study\\table\\", "dataset");
45+
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");
46+
47+
Mockito.when(conceptService.conceptDetail("dataset", table.dataset()))
48+
.thenReturn(Optional.of(table));
49+
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
50+
.thenReturn(Optional.of(study));
51+
52+
Concept actual = subject.populateParentConcepts(concept);
53+
Concept expected = concept.withStudy(study).withTable(table);
54+
55+
Assertions.assertEquals(expected, actual);
56+
}
57+
58+
@Test
59+
void shouldPopulateNonCompliantUnTabledStudy() {
60+
CategoricalConcept concept = new CategoricalConcept("\\study\\concept\\", "dataset");
61+
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");
62+
63+
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
64+
.thenReturn(Optional.of(study));
65+
66+
Concept actual = subject.populateParentConcepts(concept);
67+
Concept expected = concept.withStudy(study);
68+
69+
Assertions.assertEquals(expected, actual);
70+
}
71+
72+
@Test
73+
void shouldNotPopulateWeirdConcept() {
74+
CategoricalConcept concept = new CategoricalConcept("\\1\\2\\3\\4\\5\\6\\", "dataset");
75+
Concept actual = subject.populateParentConcepts(concept);
76+
77+
Assertions.assertEquals(concept, actual);
78+
}
79+
}

0 commit comments

Comments
 (0)