diff --git a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java index e034fb41c2..9de8432a22 100644 --- a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java +++ b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java @@ -3,6 +3,8 @@ import static org.ohdsi.webapi.service.cscompare.ConceptSetCompareService.CONCEPT_SET_COMPARISON_ROW_MAPPER; import static org.ohdsi.webapi.util.SecurityUtils.whitelist; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -71,6 +73,7 @@ import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; +import org.ohdsi.webapi.vocabulary.MappedRelatedConcept; /** * Provides REST services for working with @@ -96,7 +99,10 @@ public class VocabularyService extends AbstractDaoService { @Autowired private ConceptSetCompareService conceptSetCompareService; - + + @Autowired + private ObjectMapper objectMapper; + @Value("${datasource.driverClassName}") private String driver; @@ -783,7 +789,75 @@ public Collection getRelatedConcepts(@PathParam("sourceKey") Str return concepts.values(); } - /** + @POST + @Path("{sourceKey}/related-standard") + @Produces(MediaType.APPLICATION_JSON) + public Collection getRelatedStandardMappedConcepts(@PathParam("sourceKey") String sourceKey, List allConceptIds) { + Source source = getSourceRepository().findBySourceKey(sourceKey); + String relatedConceptsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts.sql"; + String relatedMappedFromIdsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts_getMappedFromIds.sql"; + String tableQualifier = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary); + + String[] searchStrings = {"CDM_schema"}; + String[] replacementStrings = {tableQualifier}; + + String[] varNames = {"conceptIdList"}; + + final Map resultCombinedMappedConcepts = new HashMap<>(); + final Map relatedStandardConcepts = new HashMap<>(); + for(final List conceptIdsBatch: Lists.partition(allConceptIds, PreparedSqlRender.getParameterLimit(source))) { + Object[] varValues = {conceptIdsBatch.toArray()}; + PreparedStatementRenderer relatedConceptsRenderer = new PreparedStatementRenderer(source, relatedConceptsSQLPath, searchStrings, replacementStrings, varNames, varValues); + getSourceJdbcTemplate(source).query(relatedConceptsRenderer.getSql(), relatedConceptsRenderer.getSetter(), (RowMapper) (resultSet, arg1) -> { + addRelationships(relatedStandardConcepts, resultSet); + return null; + }); + + final Map> relatedNonStandardConceptIdsByStandardId = new HashMap<>(); + + PreparedStatementRenderer mappedFromConceptsRenderer = new PreparedStatementRenderer(source, relatedMappedFromIdsSQLPath, searchStrings, replacementStrings, varNames, varValues); + getSourceJdbcTemplate(source).query(mappedFromConceptsRenderer.getSql(), mappedFromConceptsRenderer.getSetter(), (RowMapper) (resultSet, arg1) -> { + populateRelatedConceptIds(relatedNonStandardConceptIdsByStandardId, resultSet); + return null; + }); + + enrichResultCombinedMappedConcepts(resultCombinedMappedConcepts, relatedStandardConcepts, relatedNonStandardConceptIdsByStandardId); + } + return resultCombinedMappedConcepts.values(); + } + + private void populateRelatedConceptIds(final Map> mappedConceptsIds, final ResultSet resultSet) throws SQLException { + final Long concept_id = resultSet.getLong("CONCEPT_ID"); + if (!mappedConceptsIds.containsKey(concept_id)) { + Set mappedIds = new HashSet<>(); + mappedIds.add(resultSet.getLong("MAPPED_FROM_ID")); + mappedConceptsIds.put(concept_id,mappedIds); + } else { + mappedConceptsIds.get(concept_id).add(resultSet.getLong("MAPPED_FROM_ID")); + } + } + + void enrichResultCombinedMappedConcepts(Map resultCombinedMappedConcepts, + Map relatedStandardConcepts, + Map> relatedNonStandardConceptIdsByStandardId) { + relatedNonStandardConceptIdsByStandardId.forEach((standardConceptId, mappedFromIds)->{ + if(resultCombinedMappedConcepts.containsKey(standardConceptId)){ + resultCombinedMappedConcepts.get(standardConceptId).mappedFromIds.addAll(mappedFromIds); + } else { + MappedRelatedConcept mappedRelatedConcept; + try { + mappedRelatedConcept = objectMapper.readValue(objectMapper.writeValueAsString(relatedStandardConcepts.get(standardConceptId)), MappedRelatedConcept.class); + mappedRelatedConcept.mappedFromIds=mappedFromIds; + resultCombinedMappedConcepts.put(standardConceptId,mappedRelatedConcept); + } catch (JsonProcessingException e) { + log.error("Could not convert RelatedConcept to MappedRelatedConcept", e); + throw new WebApplicationException(e); + } + } + }); + } + + /** * Get ancestor and descendant concepts for the selected concept identifier * from a source. * diff --git a/src/main/java/org/ohdsi/webapi/vocabulary/MappedRelatedConcept.java b/src/main/java/org/ohdsi/webapi/vocabulary/MappedRelatedConcept.java new file mode 100644 index 0000000000..1d7cb2e0cc --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/vocabulary/MappedRelatedConcept.java @@ -0,0 +1,12 @@ +package org.ohdsi.webapi.vocabulary; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Set; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MappedRelatedConcept extends RelatedConcept { + @JsonProperty("mapped_from") + public Set mappedFromIds; +} diff --git a/src/main/resources/db/migration/postgresql/V2.15.0.20241009000001__add_related_standard_concepts_permission_if_not_exists.sql b/src/main/resources/db/migration/postgresql/V2.15.0.20241009000001__add_related_standard_concepts_permission_if_not_exists.sql new file mode 100644 index 0000000000..c106df2543 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V2.15.0.20241009000001__add_related_standard_concepts_permission_if_not_exists.sql @@ -0,0 +1,18 @@ +INSERT INTO ${ohdsiSchema}.sec_permission (id, value, description) +SELECT nextval('${ohdsiSchema}.sec_permission_id_seq'), 'vocabulary:*:related-standard:post', 'Access related mapped standard concepts resource' +WHERE NOT EXISTS ( + SELECT NULL FROM ${ohdsiSchema}.sec_permission + WHERE value = 'vocabulary:*:related-standard:post' +); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'vocabulary:*:related-standard:post' + ) AND sr.name IN ('Atlas users') + AND NOT EXISTS ( + SELECT NULL FROM ${ohdsiSchema}.sec_role_permission + WHERE permission_id = sp.id and role_id = sr.id); + + diff --git a/src/main/resources/resources/vocabulary/sql/getRelatedStandardMappedConcepts.sql b/src/main/resources/resources/vocabulary/sql/getRelatedStandardMappedConcepts.sql new file mode 100644 index 0000000000..fae14064de --- /dev/null +++ b/src/main/resources/resources/vocabulary/sql/getRelatedStandardMappedConcepts.sql @@ -0,0 +1,44 @@ +SELECT + c.CONCEPT_ID, + c.CONCEPT_NAME, + COALESCE(c.STANDARD_CONCEPT, 'N') as STANDARD_CONCEPT, + COALESCE(c.INVALID_REASON, 'V') as INVALID_REASON, + c.CONCEPT_CODE, + c.CONCEPT_CLASS_ID, + c.DOMAIN_ID, + c.VOCABULARY_ID, + c.VALID_START_DATE, + c.VALID_END_DATE, + c.RELATIONSHIP_NAME, + c.RELATIONSHIP_DISTANCE +FROM ( + SELECT + c.CONCEPT_ID, CONCEPT_NAME, COALESCE(c.STANDARD_CONCEPT, 'N') as STANDARD_CONCEPT, COALESCE(c.INVALID_REASON, 'V') as INVALID_REASON, + c.CONCEPT_CODE, c.CONCEPT_CLASS_ID, c.DOMAIN_ID, c.VOCABULARY_ID, c.VALID_START_DATE, c.VALID_END_DATE, + r.RELATIONSHIP_NAME, 1 as RELATIONSHIP_DISTANCE + FROM + @CDM_schema.concept_relationship cr + JOIN + @CDM_schema.concept c ON cr.CONCEPT_ID_2 = c.CONCEPT_ID + JOIN + @CDM_schema.relationship r ON cr.RELATIONSHIP_ID = r.RELATIONSHIP_ID + WHERE + cr.CONCEPT_ID_1 IN (@conceptIdList) + AND COALESCE(c.STANDARD_CONCEPT, 'N') IN ('S', 'C') + AND cr.INVALID_REASON IS NULL +) c +GROUP BY + c.CONCEPT_ID, + c.CONCEPT_NAME, + c.STANDARD_CONCEPT, + c.INVALID_REASON, + c.CONCEPT_CODE, + c.CONCEPT_CLASS_ID, + c.DOMAIN_ID, + c.VOCABULARY_ID, + c.VALID_START_DATE, + c.VALID_END_DATE, + c.RELATIONSHIP_NAME, + c.RELATIONSHIP_DISTANCE +ORDER BY + c.RELATIONSHIP_DISTANCE ASC; \ No newline at end of file diff --git a/src/main/resources/resources/vocabulary/sql/getRelatedStandardMappedConcepts_getMappedFromIds.sql b/src/main/resources/resources/vocabulary/sql/getRelatedStandardMappedConcepts_getMappedFromIds.sql new file mode 100644 index 0000000000..a1db504bed --- /dev/null +++ b/src/main/resources/resources/vocabulary/sql/getRelatedStandardMappedConcepts_getMappedFromIds.sql @@ -0,0 +1,20 @@ +SELECT + c.CONCEPT_ID, + c.MAPPED_FROM_ID +FROM ( + SELECT DISTINCT + c.CONCEPT_ID, + CAST(cr.CONCEPT_ID_1 AS VARCHAR) as MAPPED_FROM_ID + FROM + @CDM_schema.concept_relationship cr + JOIN + @CDM_schema.concept c ON cr.CONCEPT_ID_2 = c.CONCEPT_ID + JOIN + @CDM_schema.relationship r ON cr.RELATIONSHIP_ID = r.RELATIONSHIP_ID + WHERE + cr.CONCEPT_ID_1 IN (@conceptIdList) + AND COALESCE(c.STANDARD_CONCEPT, 'N') IN ('S', 'C') + AND cr.INVALID_REASON IS NULL +) c +ORDER BY + c.CONCEPT_ID; \ No newline at end of file