Skip to content

Commit

Permalink
[ALS-6711] Tree view of concepts
Browse files Browse the repository at this point in the history
- Add to service, repo layer for new query
- Make new mapper
- Add children to all concepts
- Tests!
  • Loading branch information
Luke Sikina authored and Luke-Sikina committed Sep 5, 2024
1 parent 3f25818 commit a43a77d
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ public class ConceptRepository {

private final ConceptFilterQueryGenerator filterGen;
private final ConceptMetaExtractor conceptMetaExtractor;
private final ConceptResultSetExtractor conceptResultSetExtractor;


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


Expand Down Expand Up @@ -133,4 +135,87 @@ public Map<Concept, Map<String, String>> getConceptMetaForConcepts(List<Concept>
return template.query(sql, params, conceptMetaExtractor);

}

public Optional<Concept> getConceptTree(String dataset, String conceptPath, int depth) {
String sql = """
WITH core_query AS (
WITH RECURSIVE nodes AS (
SELECT
concept_node_id, parent_id, 0 AS depth
FROM
concept_node
LEFT JOIN dataset ON concept_node.dataset_id = dataset.dataset_id
WHERE
concept_node.CONCEPT_PATH = :path
AND dataset.REF = :dataset
UNION
SELECT
child_nodes.concept_node_id, child_nodes.parent_id, parent_node.depth+ 1
FROM
concept_node child_nodes
INNER JOIN nodes parent_node ON child_nodes.parent_id = parent_node.concept_node_id
LEFT JOIN dataset ON child_nodes.dataset_id = dataset.dataset_id
)
SELECT
depth, child_nodes.concept_node_id
FROM
nodes parent_node
INNER JOIN concept_node child_nodes ON child_nodes.parent_id = parent_node.concept_node_id
WHERE
depth < :depth
UNION
SELECT
0 as depth, concept_node.concept_node_id
FROM
concept_node
LEFT JOIN dataset ON concept_node.dataset_id = dataset.dataset_id
WHERE
concept_node.CONCEPT_PATH = :path
AND dataset.REF = :dataset
UNION
SELECT
-1 as depth, concept_node.concept_node_id
FROM
concept_node
WHERE
concept_node.concept_node_id = (
SELECT
parent_id
FROM
concept_node
LEFT JOIN dataset ON concept_node.dataset_id = dataset.dataset_id
WHERE
concept_node.CONCEPT_PATH = :path
AND dataset.REF = :dataset
)
ORDER BY depth ASC
)
SELECT
concept_node.*,
ds.REF AS dataset,
continuous_min.VALUE AS min, continuous_max.VALUE AS max,
categorical_values.VALUE AS values,
meta_description.VALUE AS description,
core_query.depth AS depth
FROM
concept_node
INNER JOIN core_query ON concept_node.concept_node_id = core_query.concept_node_id
LEFT JOIN dataset AS ds ON concept_node.dataset_id = ds.dataset_id
LEFT JOIN concept_node_meta AS meta_description ON concept_node.concept_node_id = meta_description.concept_node_id AND meta_description.KEY = 'description'
LEFT JOIN concept_node_meta AS continuous_min ON concept_node.concept_node_id = continuous_min.concept_node_id AND continuous_min.KEY = 'min'
LEFT JOIN concept_node_meta AS continuous_max ON concept_node.concept_node_id = continuous_max.concept_node_id AND continuous_max.KEY = 'max'
LEFT JOIN concept_node_meta AS categorical_values ON concept_node.concept_node_id = categorical_values.concept_node_id AND categorical_values.KEY = 'values'
""";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("path", conceptPath)
.addValue("dataset", dataset)
.addValue("depth", depth);

if (depth < 0) {
return Optional.empty();
}

return Optional.ofNullable(template.query(sql, params, conceptResultSetExtractor));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package edu.harvard.dbmi.avillach.dictionary.concept;

import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
import edu.harvard.dbmi.avillach.dictionary.concept.model.ConceptType;
import org.springframework.beans.factory.annotation.Autowired;
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.*;

@Component
public class ConceptResultSetExtractor implements ResultSetExtractor<Concept> {
@Autowired
private ConceptResultSetUtil conceptResultSetUtil;

private record ConceptWithId(Concept c, int id) {
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
ConceptWithId conceptWithId = (ConceptWithId) object;
return id == conceptWithId.id;
}

@Override
public int hashCode() {
return Objects.hashCode(id);
}
};

@Override
public Concept extractData(ResultSet rs) throws SQLException, DataAccessException {
Map<Integer, List<ConceptWithId>> conceptsByParentId = new HashMap<>();
ConceptWithId root = null;
while (rs.next()) {
Concept c = switch (ConceptType.toConcept(rs.getString("concept_type"))) {
case Categorical -> conceptResultSetUtil.mapCategorical(rs);
case Continuous -> conceptResultSetUtil.mapContinuous(rs);
};
ConceptWithId conceptWithId = new ConceptWithId(c, rs.getInt("concept_node_id"));
if (root == null) { root = conceptWithId; }

int parentId = rs.getInt("parent_id");
// weirdness: null value for int is 0, so to check for missing parent value, you need the wasNull check
if (!rs.wasNull()) {
List<ConceptWithId> concepts = conceptsByParentId.getOrDefault(parentId, new ArrayList<>());
concepts.add(conceptWithId);
conceptsByParentId.put(parentId, concepts);
}
}


return root == null ? null : seedChildren(root, conceptsByParentId);
}

private Concept seedChildren(ConceptWithId root, Map<Integer, List<ConceptWithId>> conceptsByParentId) {
List<Concept> children = conceptsByParentId.getOrDefault(root.id, List.of()).stream()
.map(conceptWithId -> seedChildren(conceptWithId, conceptsByParentId))
.toList();
return root.c.withChildren(children);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package edu.harvard.dbmi.avillach.dictionary.concept;

import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
import edu.harvard.dbmi.avillach.dictionary.concept.model.ContinuousConcept;
import org.json.JSONArray;
import org.json.JSONException;
import org.springframework.stereotype.Component;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

@Component
public class ConceptResultSetUtil {

public CategoricalConcept mapCategorical(ResultSet rs) throws SQLException {
return new CategoricalConcept(
rs.getString("concept_path"), rs.getString("name"),
rs.getString("display"), rs.getString("dataset"), rs.getString("description"),
rs.getString("values") == null ? List.of() : List.of(rs.getString("values").split(",")),
null,
null
);
}

public ContinuousConcept mapContinuous(ResultSet rs) throws SQLException {
return new ContinuousConcept(
rs.getString("concept_path"), rs.getString("name"),
rs.getString("display"), rs.getString("dataset"), rs.getString("description"),
parseMin(rs.getString("values")), parseMax(rs.getString("values")),
null
);
}

public Integer parseMin(String valuesArr) {
try {
JSONArray arr = new JSONArray(valuesArr);
return arr.length() == 2 ? arr.getInt(0) : 0;
} catch (JSONException ex) {
return 0;
}
}

public Integer parseMax(String valuesArr) {
try {
JSONArray arr = new JSONArray(valuesArr);
return arr.length() == 2 ? arr.getInt(1) : 0;
} catch (JSONException ex) {
return 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import edu.harvard.dbmi.avillach.dictionary.concept.model.*;
import org.json.JSONArray;
import org.json.JSONException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
Expand All @@ -15,48 +16,14 @@
@Component
public class ConceptRowMapper implements RowMapper<Concept> {

@Autowired
ConceptResultSetUtil conceptResultSetUtil;

@Override
public Concept mapRow(ResultSet rs, int rowNum) throws SQLException {
return switch (ConceptType.toConcept(rs.getString("concept_type"))) {
case Categorical -> mapCategorical(rs);
case Continuous -> mapContinuous(rs);
case Categorical -> conceptResultSetUtil.mapCategorical(rs);
case Continuous -> conceptResultSetUtil.mapContinuous(rs);
};
}

private CategoricalConcept mapCategorical(ResultSet rs) throws SQLException {
return new CategoricalConcept(
rs.getString("concept_path"), rs.getString("name"),
rs.getString("display"), rs.getString("dataset"), rs.getString("description"),
rs.getString("values") == null ? List.of() : List.of(rs.getString("values").split(",")),
null,
null
);
}

private ContinuousConcept mapContinuous(ResultSet rs) throws SQLException {
return new ContinuousConcept(
rs.getString("concept_path"), rs.getString("name"),
rs.getString("display"), rs.getString("dataset"), rs.getString("description"),
parseMin(rs.getString("values")), parseMax(rs.getString("values")),
null
);
}

private Integer parseMin(String valuesArr) {
try {
JSONArray arr = new JSONArray(valuesArr);
return arr.length() == 2 ? arr.getInt(0) : 0;
} catch (JSONException ex) {
return 0;
}
}

private Integer parseMax(String valuesArr) {
try {
JSONArray arr = new JSONArray(valuesArr);
return arr.length() == 2 ? arr.getInt(1) : 0;
} catch (JSONException ex) {
return 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ public Optional<Concept> conceptDetail(String dataset, String conceptPath) {
}

public Optional<Concept> conceptTree(String dataset, String conceptPath, int depth) {
return Optional.empty();
return conceptRepository.getConceptTree(dataset, conceptPath, depth);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,26 @@ public CategoricalConcept(CategoricalConcept core, Map<String, String> meta) {
this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.values, core.children, meta);
}

public CategoricalConcept(CategoricalConcept core, List<Concept> children) {
this(core.conceptPath, core.name, core.display, core.dataset, core.description, core.values, children, core.meta);
}

public CategoricalConcept(String conceptPath, String dataset) {
this(conceptPath, "", "", dataset, "", List.of(), List.of(), null);
}


@JsonProperty("type")
@Override
public ConceptType type() {
return ConceptType.Categorical;
}

@Override
public CategoricalConcept withChildren(List<Concept> children) {
return new CategoricalConcept(this, children);
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
Expand All @@ -40,4 +53,5 @@ public boolean equals(Object object) {
public int hashCode() {
return Objects.hash(conceptPath, dataset);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.annotation.Nullable;

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

Expand Down Expand Up @@ -49,6 +51,11 @@ public sealed interface Concept

Map<String, String> meta();

@Nullable
List<Concept> children();

Concept withChildren(List<Concept> children);

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
@@ -1,5 +1,8 @@
package edu.harvard.dbmi.avillach.dictionary.concept.model;

import jakarta.annotation.Nullable;

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

Expand All @@ -24,6 +27,16 @@ public Map<String, String> meta() {
return Map.of();
}

@Override
public List<Concept> children() {
return List.of();
}

@Override
public ConceptShell withChildren(List<Concept> children) {
return this;
}

@Override
public boolean equals(Object object) {
return conceptEquals(object);
Expand Down
Loading

0 comments on commit a43a77d

Please sign in to comment.