Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ALS-6800] Detailed, paginated concepts fetch #22

Merged
merged 1 commit into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
public class ConceptController {

Expand Down Expand Up @@ -43,6 +45,21 @@ public ResponseEntity<Page<Concept>> listConcepts(
return ResponseEntity.ok(pageResp);
}

@GetMapping(path = "/concepts/dump")
public ResponseEntity<Page<Concept>> dumpConcepts(
@RequestParam(name = "page_number", defaultValue = "0", required = false) int page,
@RequestParam(name = "page_size", defaultValue = "10", required = false) int size
) {
PageRequest pagination = PageRequest.of(page, size);
PageImpl<Concept> pageResp = new PageImpl<>(
conceptService.listDetailedConcepts(new Filter(List.of(), ""), pagination),
pagination,
conceptService.countConcepts(new Filter(List.of(), ""))
);

return ResponseEntity.ok(pageResp);
}

@PostMapping(path = "/concepts/detail/{dataset}")
public ResponseEntity<Concept> conceptDetail(
@PathVariable(name = "dataset") String dataset,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 edu.harvard.dbmi.avillach.dictionary.concept.model.ConceptShell;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Component;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

@Component
public class ConceptMetaExtractor implements ResultSetExtractor<Map<Concept, Map<String, String>>> {

@Override
public Map<Concept, Map<String, String>> extractData(ResultSet rs) throws SQLException, DataAccessException {
Map<Concept, Map<String, String>> sets = new HashMap<>();
while (rs.next()) {
Concept key = new ConceptShell(rs.getString("concept_path"), rs.getString("dataset_name"));
Map<String, String> meta = sets.getOrDefault(key, new HashMap<>());
meta.put(rs.getString("KEY"), rs.getString("VALUE"));
sets.put(key, meta);
}
return sets;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ public class ConceptRepository {
private final ConceptRowMapper mapper;

private final ConceptFilterQueryGenerator filterGen;
private final ConceptMetaExtractor conceptMetaExtractor;


@Autowired
public ConceptRepository(
NamedParameterJdbcTemplate template, ConceptRowMapper mapper, ConceptFilterQueryGenerator filterGen
NamedParameterJdbcTemplate template, ConceptRowMapper mapper, ConceptFilterQueryGenerator filterGen,
ConceptMetaExtractor conceptMetaExtractor
) {
this.template = template;
this.mapper = mapper;
this.filterGen = filterGen;
this.conceptMetaExtractor = conceptMetaExtractor;
}


Expand Down Expand Up @@ -107,4 +111,26 @@ public Map<String, String> getConceptMeta(String dataset, String conceptPath) {
.addValue("dataset", dataset);
return template.query(sql, params, new MapExtractor("KEY", "VALUE"));
}

public Map<Concept, Map<String, String>> getConceptMetaForConcepts(List<Concept> concepts) {
String sql = """
SELECT
concept_node_meta.KEY, concept_node_meta.VALUE,
concept_node.CONCEPT_PATH AS concept_path, dataset.REF AS dataset_name
FROM
concept_node
LEFT JOIN concept_node_meta ON concept_node.concept_node_id = concept_node_meta.concept_node_id
LEFT JOIN dataset ON concept_node.dataset_id = dataset.dataset_id
WHERE
(concept_node.CONCEPT_PATH, dataset.REF) IN (:pairs)
ORDER BY concept_node.CONCEPT_PATH, dataset.REF
""";
List<String[]> pairs = concepts.stream()
.map(c -> new String[]{c.conceptPath(), c.dataset()})
.toList();
MapSqlParameterSource params = new MapSqlParameterSource().addValue("pairs", pairs);

return template.query(sql, params, conceptMetaExtractor);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

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;
import edu.harvard.dbmi.avillach.dictionary.concept.model.ContinuousConcept;
import edu.harvard.dbmi.avillach.dictionary.filter.Filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

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 {
Expand All @@ -25,6 +29,16 @@ public List<Concept> listConcepts(Filter filter, Pageable page) {
return conceptRepository.getConcepts(filter, page);
}

public List<Concept> listDetailedConcepts(Filter filter, Pageable page) {
List<Concept> concepts = conceptRepository.getConcepts(filter, page);
Map<Concept, Map<String, String>> metas = conceptRepository.getConceptMetaForConcepts(concepts);
return concepts.stream().map(concept -> (Concept) switch (concept) {
case ContinuousConcept cont -> new ContinuousConcept(cont, metas.getOrDefault(cont, Map.of()));
case CategoricalConcept cat -> new CategoricalConcept(cat, metas.getOrDefault(cat, Map.of()));
case ConceptShell ignored -> throw new RuntimeException("Concept shell escaped to API");
}).toList();
}

public long countConcepts(Filter filter) {
return conceptRepository.countConcepts(filter);
}
Expand All @@ -36,6 +50,7 @@ public Optional<Concept> conceptDetail(String dataset, String 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");
};
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import java.util.List;
import java.util.Map;
import java.util.Objects;

public record CategoricalConcept(
String conceptPath, String name, String display, String dataset, String description,
Expand All @@ -29,4 +30,14 @@ public CategoricalConcept(CategoricalConcept core, Map<String, String> meta) {
public ConceptType type() {
return ConceptType.Categorical;
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
}

@Override
public int hashCode() {
return Objects.hash(conceptPath, dataset);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo;

import java.util.Map;
import java.util.Objects;


@JsonIgnoreProperties(ignoreUnknown = true)
Expand All @@ -18,7 +19,7 @@
@JsonSubTypes.Type(value = CategoricalConcept.class, name = "Categorical"),
})
public sealed interface Concept
permits CategoricalConcept, ContinuousConcept {
permits CategoricalConcept, ConceptShell, ContinuousConcept {

/**
* @return The complete concept path for this concept (// delimited)
Expand Down Expand Up @@ -48,5 +49,10 @@ public sealed interface Concept

Map<String, String> meta();


default boolean conceptEquals(Object object) {
if (this == object) return true;
if (!(object instanceof Concept)) return false;
Concept that = (Concept) object;
return Objects.equals(dataset(), that.dataset()) && Objects.equals(conceptPath(), that.conceptPath());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package edu.harvard.dbmi.avillach.dictionary.concept.model;

import java.util.Map;
import java.util.Objects;

public record ConceptShell(String conceptPath, String dataset) implements Concept {
@Override
public String name() {
return "Shell. Not for external use.";
}

@Override
public String display() {
return "Shell. Not for external use.";
}

@Override
public ConceptType type() {
return ConceptType.Continuous;
}

@Override
public Map<String, String> meta() {
return Map.of();
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
}

@Override
public int hashCode() {
return Objects.hash(conceptPath, dataset);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import jakarta.annotation.Nullable;

import java.util.Map;
import java.util.Objects;

public record ContinuousConcept(
String conceptPath, String name, String display, String dataset, String description,
Expand All @@ -21,4 +22,14 @@ public ContinuousConcept(ContinuousConcept core, Map<String, String> meta) {
public ConceptType type() {
return ConceptType.Continuous;
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
}

@Override
public int hashCode() {
return Objects.hash(conceptPath, dataset);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -143,4 +144,25 @@ void shouldNotGetConceptTreeWhenConceptDNE() {

Assertions.assertEquals(HttpStatus.NOT_FOUND, actual.getStatusCode());
}

@Test
void shouldDumpConcepts() {
Concept fooBar = new CategoricalConcept(
"/foo//bar", "bar", "Bar", "my_dataset", "foo!", List.of("a", "b"), List.of(),
Map.of("key", "value")
);
Concept fooBaz = new ContinuousConcept(
"/foo//baz", "baz", "Baz", "my_dataset", "foo!", 0, 100,
Map.of("key", "value")
);
List<Concept> concepts = List.of(fooBar, fooBaz);
PageRequest page = PageRequest.of(0, 10);
Mockito.when(conceptService.listDetailedConcepts(new Filter(List.of(), ""), page))
.thenReturn(concepts);

ResponseEntity<Page<Concept>> actual = subject.dumpConcepts(0, 10);

Assertions.assertEquals(concepts, actual.getBody().getContent());
Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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;
import edu.harvard.dbmi.avillach.dictionary.concept.model.ContinuousConcept;
import edu.harvard.dbmi.avillach.dictionary.facet.Facet;
import edu.harvard.dbmi.avillach.dictionary.filter.Filter;
Expand Down Expand Up @@ -145,7 +146,36 @@ void shouldNotGetConceptThatDNE() {
Optional<Concept> actual = subject.getConcept("invalid.invalid", "fake");
Assertions.assertEquals(Optional.empty(), actual);

actual = subject.getConcept("fake", "\\\\\\\\B\\\\\\\\2\\\\\\\\Z\\\\\\\\");
actual = subject.getConcept("fake", "\\\\B\\\\2\\\\Z\\\\");
Assertions.assertEquals(Optional.empty(), actual);
}

@Test
void shouldGetMetaForMultipleConcepts() {
List<Concept> concepts = List.of(
new ContinuousConcept("\\phs000007\\pht000022\\phv00004260\\FM219\\", "", "", "phs000007", "", null, null, Map.of()),
new ContinuousConcept("\\phs000007\\pht000033\\phv00008849\\D080\\", "", "", "phs000007", "", null, null, Map.of())
);

Map<Concept, Map<String, String>> actual = subject.getConceptMetaForConcepts(concepts);
Map<Concept, Map<String, String>> expected = Map.of(
new ConceptShell("\\phs000007\\pht000022\\phv00004260\\FM219\\", "phs000007"), Map.of(
"unique_identifier", "no",
"stigmatizing", "no",
"bdc_open_access", "yes",
"values", "[0, 1]",
"description", "# 12 OZ CUPS OF CAFFEINATED COLA / DAY",
"free_text", "no"
),
new ConceptShell("\\phs000007\\pht000033\\phv00008849\\D080\\", "phs000007"), Map.of(
"unique_identifier", "no",
"stigmatizing", "no",
"bdc_open_access", "yes",
"values", "[0, 5]",
"description", "# 12 OZ CUPS OF CAFFEINATED COLA/DAY",
"free_text", "no"
)
);
Assertions.assertEquals(expected, actual);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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;
import edu.harvard.dbmi.avillach.dictionary.concept.model.ContinuousConcept;
import edu.harvard.dbmi.avillach.dictionary.filter.Filter;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -81,4 +82,33 @@ void shouldShowDetailForCategorical() {

Assertions.assertEquals(expected, actual);
}

@Test
void shouldShowDetailForMultiple() {
ConceptShell shellA = new ConceptShell("pathA", "dataset");
CategoricalConcept conceptA = new CategoricalConcept("pathA", "", "", "dataset", null, List.of("a"), List.of(), null);
Map<String, String> metaA = Map.of("VALUES", "a", "stigmatizing", "true");

ConceptShell shellB = new ConceptShell("pathB", "dataset");
ContinuousConcept conceptB = new ContinuousConcept("pathB", "", "", "dataset", null, 0, 1, null);
Map<String, String> metaB = Map.of("MIN", "0", "MAX", "1", "stigmatizing", "true");

Map<Concept, Map<String, String>> metas = Map.of(shellA, metaA, shellB, metaB);
List<Concept> concepts = List.of(conceptA, conceptB);
Filter emptyFilter = new Filter(List.of(), "");


Mockito.when(repository.getConceptMetaForConcepts(concepts))
.thenReturn(metas);
Mockito.when(repository.getConcepts(emptyFilter, Pageable.unpaged()))
.thenReturn(concepts);

List<Concept> actual = subject.listDetailedConcepts(emptyFilter, Pageable.unpaged());
List<Concept> expected = List.of(
new CategoricalConcept(conceptA, metaA),
new ContinuousConcept(conceptB, metaB)
);

Assertions.assertEquals(expected, actual);
}
}