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 20, 2024
1 parent a26a253 commit 807aba8
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package edu.harvard.dbmi.avillach.dictionary.concept;

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;
import java.util.function.Predicate;
import java.util.stream.Stream;

@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 = Stream.of(concept.conceptPath()
.split("\\\\")).filter(Predicate.not(String::isBlank)).toList(); // 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.conceptDetailWithoutAncestors(concept.dataset(), studyPath).orElse(null);
Concept table = conceptService.conceptDetailWithoutAncestors(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
Expand Up @@ -12,17 +12,18 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
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 All @@ -44,19 +45,28 @@ public long countConcepts(Filter filter) {
}

public Optional<Concept> conceptDetail(String dataset, String conceptPath) {
return conceptRepository.getConcept(dataset, conceptPath)
return getConcept(dataset, conceptPath, true);
}

private Optional<Concept> getConcept(String dataset, String conceptPath, boolean addAncestors) {
Optional<Concept> concept = conceptRepository.getConcept(dataset, conceptPath)
.map(core -> {
var meta = conceptRepository.getConceptMeta(dataset, conceptPath);
return switch (core) {
case ContinuousConcept cont -> new ContinuousConcept(cont, meta);
case CategoricalConcept cat -> new CategoricalConcept(cat, meta);
case ConceptShell ignored -> throw new RuntimeException("Concept shell escaped to API");
};
}
);
var meta = conceptRepository.getConceptMeta(dataset, conceptPath);
return switch (core) {
case ContinuousConcept cont -> new ContinuousConcept(cont, meta);
case CategoricalConcept cat -> new CategoricalConcept(cat, meta);
case ConceptShell ignored -> throw new RuntimeException("Concept shell escaped to API");
};
}
);
return addAncestors ? concept.map(conceptDecoratorService::populateParentConcepts) : concept;
}

public Optional<Concept> conceptTree(String dataset, String conceptPath, int depth) {
return conceptRepository.getConceptTree(dataset, conceptPath, depth);
}

public Optional<Concept> conceptDetailWithoutAncestors(String dataset, String conceptPath) {
return getConcept(dataset, conceptPath, false);
}
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public List<DashboardColumn> getColumns() {
}

private int calculateOrder(DashboardColumn column) {
if (columnOrder.contains(column.label())) {
return columnOrder.indexOf(column.label());
if (columnOrder.contains(column.dataElement())) {
return columnOrder.indexOf(column.dataElement());
} else {
return Integer.MAX_VALUE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class DashboardRowResultSetExtractor implements ResultSetExtractor<List<M
@Autowired
public DashboardRowResultSetExtractor(List<DashboardColumn> columns) {
template = columns.stream()
.collect(Collectors.toMap(DashboardColumn::label, (ignored) -> ""));
.collect(Collectors.toMap(DashboardColumn::dataElement, (ignored) -> ""));
}

@Override
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,79 @@
package edu.harvard.dbmi.avillach.dictionary.concept;

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);
}

@Test
void shouldNotPopulateWeirdConcept() {
CategoricalConcept concept = new CategoricalConcept("\\1\\2\\3\\4\\5\\6\\", "dataset");
Concept actual = subject.populateParentConcepts(concept);

Assertions.assertEquals(concept, actual);
}
}
Loading

0 comments on commit 807aba8

Please sign in to comment.