Skip to content

Commit

Permalink
[ALS-7336] Show specific ancestors in details
Browse files Browse the repository at this point in the history
- Lots of weird BDC stuff. We decorate the concept with them
  • Loading branch information
Luke Sikina committed Sep 19, 2024
1 parent 8e48362 commit 17890f6
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package edu.harvard.dbmi.avillach.dictionary;

import edu.harvard.dbmi.avillach.dictionary.concept.ConceptService;
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ConceptDecoratorService {

private static final Logger LOG = LoggerFactory.getLogger(ConceptDecoratorService.class);
private final boolean enabled;
private final ConceptService conceptService;

private static final int COMPLIANT = 4, NON_COMPLIANT_TABLED = 3, NON_COMPLIANT_UNTABLED = 2;

@Autowired
public ConceptDecoratorService(
@Value("${dashboard.enable.extra_details}") boolean enabled,
@Lazy ConceptService conceptService // circular dep
) {
this.enabled = enabled;
this.conceptService = conceptService;
}


public Concept populateParentConcepts(Concept concept) {
if (!enabled) {
return concept;
}

// In some environments, certain parent concepts have critical details that we need to add to the detailed response
List<String> conceptNodes = List.of(concept.conceptPath()
.split("\\\\")); // you have to double escape the slash. Once for strings, and once for regex

return switch (conceptNodes.size()) {
case COMPLIANT, NON_COMPLIANT_TABLED -> populateTabledConcept(concept, conceptNodes);
case NON_COMPLIANT_UNTABLED -> populateNonCompliantTabledConcept(concept, conceptNodes);
default -> {
LOG.warn("Ignoring decoration request for weird concept path {}", concept.conceptPath());
yield concept;
}
};
}

private Concept populateTabledConcept(Concept concept, List<String> conceptNodes) {
String studyPath = String.join("\\", conceptNodes.subList(0, 1)) + "\\";
String tablePath = String.join("\\", conceptNodes.subList(0, 2)) + "\\";
Concept study = conceptService.conceptDetail(concept.dataset(), studyPath).orElse(null);
Concept table = conceptService.conceptDetail(concept.dataset(), tablePath).orElse(null);
return concept.withStudy(study).withTable(table);
}

private Concept populateNonCompliantTabledConcept(Concept concept, List<String> conceptNodes) {
String studyPath = String.join("\\", conceptNodes.subList(0, 1));
Concept study = conceptService.conceptDetail(concept.dataset(), studyPath).orElse(null);
return concept.withStudy(study);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.harvard.dbmi.avillach.dictionary.concept;

import edu.harvard.dbmi.avillach.dictionary.ConceptDecoratorService;
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
import edu.harvard.dbmi.avillach.dictionary.concept.model.ConceptShell;
Expand All @@ -20,9 +21,12 @@ public class ConceptService {

private final ConceptRepository conceptRepository;

private final ConceptDecoratorService conceptDecoratorService;

@Autowired
public ConceptService(ConceptRepository conceptRepository) {
public ConceptService(ConceptRepository conceptRepository, ConceptDecoratorService conceptDecoratorService) {
this.conceptRepository = conceptRepository;
this.conceptDecoratorService = conceptDecoratorService;
}

public List<Concept> listConcepts(Filter filter, Pageable page) {
Expand Down Expand Up @@ -53,7 +57,7 @@ public Optional<Concept> conceptDetail(String dataset, String conceptPath) {
case ConceptShell ignored -> throw new RuntimeException("Concept shell escaped to API");
};
}
);
).map(conceptDecoratorService::populateParentConcepts);
}

public Optional<Concept> conceptTree(String dataset, String conceptPath, int depth) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,23 @@ public record CategoricalConcept(
List<Concept> children,

@Nullable
Map<String, String> meta
Map<String, String> meta,

@Nullable
Concept table,

@Nullable
Concept study

) implements Concept {

public CategoricalConcept(
String conceptPath, String name, String display, String dataset, String description, List<String> values,
@Nullable List<Concept> children, @Nullable Map<String, String> meta
) {
this(conceptPath, name, display, dataset, description, values, children, meta, null, null);
}

public CategoricalConcept(CategoricalConcept core, Map<String, String> meta) {
this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.values, core.children, meta);
}
Expand All @@ -44,6 +57,20 @@ public CategoricalConcept withChildren(List<Concept> children) {
return new CategoricalConcept(this, children);
}

@Override
public Concept withTable(Concept table) {
return new CategoricalConcept(
conceptPath, name, display, dataset, description, values, children, meta, table, study
);
}

@Override
public Concept withStudy(Concept study) {
return new CategoricalConcept(
conceptPath, name, display, dataset, description, values, children, meta, table, study
);
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,21 @@ public sealed interface Concept
*/
ConceptType type();

Concept table();

Concept study();

Map<String, String> meta();

@Nullable
List<Concept> children();

Concept withChildren(List<Concept> children);

Concept withTable(Concept table);

Concept withStudy(Concept study);

default boolean conceptEquals(Object object) {
if (this == object) return true;
if (!(object instanceof Concept)) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ public ConceptType type() {
return ConceptType.Continuous;
}

@Override
public Concept table() {
return null;
}

@Override
public Concept study() {
return null;
}

@Override
public Map<String, String> meta() {
return Map.of();
Expand All @@ -37,6 +47,16 @@ public ConceptShell withChildren(List<Concept> children) {
return this;
}

@Override
public Concept withTable(Concept table) {
return this;
}

@Override
public Concept withStudy(Concept study) {
return this;
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,22 @@ public record ContinuousConcept(
@Nullable Integer min, @Nullable Integer max,
Map<String, String> meta,
@Nullable
List<Concept> children
List<Concept> children,

@Nullable
Concept table,

@Nullable
Concept study
) implements Concept {

public ContinuousConcept(
String conceptPath, String name, String display, String dataset, String description,
@Nullable Integer min, @Nullable Integer max, Map<String, String> meta, @Nullable List<Concept> children
) {
this(conceptPath, name, display, dataset, description, min, max, meta, children, null, null);
}

public ContinuousConcept(ContinuousConcept core, Map<String, String> meta) {
this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.min, core.max, meta, core.children);
}
Expand Down Expand Up @@ -47,6 +60,20 @@ public ContinuousConcept withChildren(List<Concept> children) {
return new ContinuousConcept(this, children);
}

@Override
public Concept withTable(Concept table) {
return new ContinuousConcept(
conceptPath, name, display, dataset, description, min, max, meta, children, table, study
);
}

@Override
public Concept withStudy(Concept study) {
return new ContinuousConcept(
conceptPath, name, display, dataset, description, min, max, meta, children, table, study
);
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application-bdc.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ spring.datasource.driver-class-name=com.amazonaws.secretsmanager.sql.AWSSecretsM
spring.datasource.url=jdbc-secretsmanager:postgresql://${DATASOURCE_URL}/picsure?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&autoReconnectForPools=true&currentSchema=dict
spring.datasource.username=${DATASOURCE_USERNAME}
server.port=80

dashboard.enable.extra_details=true
3 changes: 2 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ server.port=80

dashboard.columns={abbreviation:'Abbreviation',name:'Name',clinvars:'Clinical Variables'}
dashboard.column-order=abbreviation,name,clinvars
dashboard.nonmeta-columns=abbreviation,name
dashboard.nonmeta-columns=abbreviation,name
dashboard.enable.extra_details=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package edu.harvard.dbmi.avillach.dictionary;

import edu.harvard.dbmi.avillach.dictionary.concept.ConceptService;
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import java.util.Optional;


@SpringBootTest
class ConceptDecoratorServiceTest {

@MockBean
ConceptService conceptService;

@Autowired
ConceptDecoratorService subject;

@Test
void shouldPopulateCompliantStudy() {
CategoricalConcept concept = new CategoricalConcept("\\study\\table\\idk\\concept\\", "dataset");
CategoricalConcept table = new CategoricalConcept("\\study\\table\\", "dataset");
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");

Mockito.when(conceptService.conceptDetail("dataset", table.dataset()))
.thenReturn(Optional.of(table));
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
.thenReturn(Optional.of(study));

Concept actual = subject.populateParentConcepts(concept);
Concept expected = concept.withStudy(study).withTable(table);

Assertions.assertEquals(expected, actual);
}

@Test
void shouldPopulateNonCompliantTabledStudy() {
CategoricalConcept concept = new CategoricalConcept("\\study\\table\\concept\\", "dataset");
CategoricalConcept table = new CategoricalConcept("\\study\\table\\", "dataset");
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");

Mockito.when(conceptService.conceptDetail("dataset", table.dataset()))
.thenReturn(Optional.of(table));
Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
.thenReturn(Optional.of(study));

Concept actual = subject.populateParentConcepts(concept);
Concept expected = concept.withStudy(study).withTable(table);

Assertions.assertEquals(expected, actual);
}

@Test
void shouldPopulateNonCompliantUnTabledStudy() {
CategoricalConcept concept = new CategoricalConcept("\\study\\concept\\", "dataset");
CategoricalConcept study = new CategoricalConcept("\\study\\", "dataset");

Mockito.when(conceptService.conceptDetail("dataset", study.dataset()))
.thenReturn(Optional.of(study));

Concept actual = subject.populateParentConcepts(concept);
Concept expected = concept.withStudy(study);

Assertions.assertEquals(expected, actual);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.harvard.dbmi.avillach.dictionary.concept;

import edu.harvard.dbmi.avillach.dictionary.ConceptDecoratorService;
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
import edu.harvard.dbmi.avillach.dictionary.concept.model.ConceptShell;
Expand All @@ -24,6 +25,9 @@ class ConceptServiceTest {
@MockBean
ConceptRepository repository;

@MockBean
ConceptDecoratorService decoratorService;

@Autowired
ConceptService subject;

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ void shouldIncludeTypeInList() throws JsonProcessingException {
);

String actual = new ObjectMapper().writeValueAsString(concepts);
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\"}]";
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\"}]";

Assertions.assertEquals(expected, actual);
}
Expand Down
2 changes: 2 additions & 0 deletions src/test/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ spring.datasource.driver-class-name=org.postgresql.Driver
dashboard.columns={abbreviation:'Abbreviation',melast:'This one goes last',name:'Name',clinvars:'Clinical Variables',participants:'Participants'}
dashboard.column-order=abbreviation,name,clinvars
dashboard.nonmeta-columns=abbreviation,name

dashboard.enable.extra_details=true

0 comments on commit 17890f6

Please sign in to comment.