Commit 421af419 authored by Piotr Gawron's avatar Piotr Gawron
Browse files

Merge branch '841-autocomplete-for-ctdbase' into 'master'

Resolve "autocomplete for ctdbase"

Closes #841

See merge request !1058
parents ce217f4f 9d76c44d
Pipeline #20636 passed with stage
in 14 minutes and 26 seconds
minerva (15.0.0~beta.0) stable; urgency=medium
* Feature removal: old connection to CTD is removed and replaced with
new Data-API interface that closely check license compliance
* Feature removal: some custom fields (like transparency zoom level) are not
exported to CellDesigner file (#1071)
* Improvement: export to SBGN/SBML/GPML format provides popup with list of
warnings occurred in the translation process (#713)
* Small improvement: autocomplete for chemical searches is enabled (#841)
* Small improvement: import/export to SBML of unit of information obtained
from SBGN source is supported (#1088)
* Small improvement: rounded rectangle is used to visualize Simple chemicals
......@@ -17,7 +20,7 @@ minerva (15.0.0~beta.0) stable; urgency=medium
-- Piotr Gawron <piotr.gawron@uni.lu> Tue, 28 Jan 2020 13:00:00 +0200
minerva (15.0.0~alpha.2) stable; urgency=medium
* Feature removal: old connection to drugBank is removed and replaced with
* Feature removal: old connection to DrugBank is removed and replaced with
new Data-API interface that closely check license compliance
* Small improvement: when typing drug, list of autocomplete drugs should be
available limited to the drugs targetting something on the map (#641)
......
......@@ -292,6 +292,10 @@ public class Chemical implements Serializable, TargettingStructure {
this.synonyms.addAll(synonyms);
}
public void addTarget(Target target) {
this.getTargets().add(target);
}
/**
* Comparator of the objects by their name.
*
......@@ -311,4 +315,8 @@ public class Chemical implements Serializable, TargettingStructure {
}
public void removeTargets(Set<Target> targetsToRemove) {
this.getTargets().removeAll(targetsToRemove);
}
}
......@@ -28,7 +28,7 @@ public class Target implements Serializable {
/**
* Default class logger.
*/
private static transient Logger logger = LogManager.getLogger(Target.class);
private static transient Logger logger = LogManager.getLogger();
/**
* Database from which target was received.
......@@ -45,6 +45,8 @@ public class Target implements Serializable {
*/
private MiriamData organism;
private MiriamData associatedDisease;
/**
* List of genes located in a target.
*/
......@@ -223,7 +225,15 @@ public class Target implements Serializable {
@Override
public String toString() {
return "[" + this.getClass().getSimpleName() + "]: " + name + ", source: " + source + ", organism: " + organism
+ "; ";
+ "; disease: " + associatedDisease;
}
public MiriamData getAssociatedDisease() {
return associatedDisease;
}
public void setAssociatedDisease(MiriamData associatedDisease) {
this.associatedDisease = associatedDisease;
}
}
package lcsb.mapviewer.annotation.services.dapi;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lcsb.mapviewer.annotation.data.Chemical;
import lcsb.mapviewer.annotation.data.Target;
import lcsb.mapviewer.annotation.services.dapi.dto.*;
import lcsb.mapviewer.common.exception.InvalidArgumentException;
import lcsb.mapviewer.model.Project;
import lcsb.mapviewer.model.map.*;
import lcsb.mapviewer.model.map.model.ModelData;
import lcsb.mapviewer.model.map.model.ModelSubmodelConnection;
@Service
public class ChemicalParser {
private static final String DAPI_DATABASE_NAME = "CTD";
private Logger logger = LogManager.getLogger();
private DapiConnector dapiConnector;
private ChemicalEntityDtoConverter chemicalEntityDtoConverter;
ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
@Autowired
public ChemicalParser(DapiConnector dapiConnector, ChemicalEntityDtoConverter chemicalEntityDtoConverter) {
this.dapiConnector = dapiConnector;
this.chemicalEntityDtoConverter = chemicalEntityDtoConverter;
}
/**
* @param disease
* the MESH Id for the disease
* @return list of chemicals id, name pairs
* @throws ChemicalSearchException
* thrown when there is problem with accessing ctd database
*/
public List<Chemical> getChemicalsForDisease(MiriamData disease) throws ChemicalSearchException {
if (disease == null || disease.getResource() == null) {
throw new InvalidArgumentException("disease cannot be null");
}
if (!dapiConnector.isValidConnection()) {
return new ArrayList<>();
}
try {
String url = dapiConnector.getLatestReleaseUrl(DAPI_DATABASE_NAME);
if (url == null) {
return null;
}
String content = dapiConnector
.getAuthenticatedContent(
url + "/drugs/?size=1000&columns=sourceIdentifier&target_disease_identifier="
+ URLEncoder.encode(disease.getPrefixedIdentifier(), "UTF-8"));
ListChemicalEntityDto dto = objectMapper.readValue(content, ListChemicalEntityDto.class);
return fetchChemicals(dto, disease);
} catch (DapiConnectionException | IOException e) {
throw new ChemicalSearchException("Problem with accessing DAPI", e);
}
}
/**
* Returns list of chemicals for given set of target genes related to the
* disease.
*
* @param targets
* set of target genes
* @param disease
* identifier of the disease
* @return list of chemicals for given set of target genes related to the
* disease
* @throws ChemicalSearchException
* thrown when there is a problem with accessing ctd database
*/
public List<Chemical> getChemicalListByTarget(Collection<MiriamData> targets, MiriamData disease)
throws ChemicalSearchException {
Map<MiriamData, Chemical> chemicals = new HashMap<>();
for (MiriamData miriamData : targets) {
for (Chemical chemical : getChemicalListByTarget(miriamData, disease)) {
chemicals.put(chemical.getChemicalId(), chemical);
}
}
return new ArrayList<>(chemicals.values());
}
/**
* Returns list of chemicals that interact with a target.
*
* @param target
* target for which we are looking for chemicals
* @param disease
* we want to get chemicals in context of the disease
* @return list of chemicals that interact with a target
* @throws ChemicalSearchException
* thrown when there is a problem with accessing chemical database
*/
public List<Chemical> getChemicalListByTarget(MiriamData target, MiriamData disease) throws ChemicalSearchException {
if (target == null) {
return new ArrayList<>();
}
if (!dapiConnector.isValidConnection()) {
return new ArrayList<>();
}
List<MiriamData> targets = extractHgncTargets(target);
try {
String url = dapiConnector.getLatestReleaseUrl(DAPI_DATABASE_NAME);
if (url == null) {
return null;
}
Map<MiriamData, Chemical> result = new HashMap<>();
for (MiriamData t : targets) {
String content = dapiConnector
.getAuthenticatedContent(
url + "/drugs/?size=1000&columns=sourceIdentifier&target_identifier="
+ URLEncoder.encode(t.getPrefixedIdentifier(), "UTF-8") +
"&target_disease_identifier=" + URLEncoder.encode(disease.getPrefixedIdentifier(), "UTF-8"));
ListChemicalEntityDto dto = objectMapper.readValue(content, ListChemicalEntityDto.class);
List<Chemical> chemicals = fetchChemicals(dto, disease);
for (Chemical drug : chemicals) {
result.put(drug.getChemicalId(), drug);
}
}
return new ArrayList<>(result.values());
} catch (DapiConnectionException | IOException e) {
throw new ChemicalSearchException("Problem with accessing DAPI", e);
}
}
private List<MiriamData> extractHgncTargets(MiriamData target) {
List<MiriamData> targets = new ArrayList<>();
if (target.getDataType().equals(MiriamType.HGNC_SYMBOL)) {
targets.add(target);
} else {
throw new InvalidArgumentException("Only those identifiers are accepted: " + MiriamType.HGNC_SYMBOL);
}
return targets;
}
public List<String> getSuggestedQueryList(Project project, MiriamData diseaseMiriam) throws ChemicalSearchException {
if (diseaseMiriam == null) {
return new ArrayList<>();
}
Set<ModelData> maps = new HashSet<>();
maps.addAll(project.getModels());
for (ModelData model : project.getModels()) {
for (ModelSubmodelConnection submodel : model.getSubmodels()) {
maps.add(submodel.getSubmodel());
}
}
Set<MiriamData> hgncSymbols = new HashSet<>();
for (ModelData model : maps) {
for (BioEntity bioEntity : model.getElements()) {
for (MiriamData md : bioEntity.getMiriamData()) {
if (md.getDataType().equals(MiriamType.HGNC_SYMBOL)) {
hgncSymbols.add(md);
}
}
hgncSymbols.add(new MiriamData(MiriamType.HGNC_SYMBOL, bioEntity.getName()));
}
}
Set<String> names = new HashSet<>();
List<Chemical> chemicals = getChemicalListByTarget(hgncSymbols, diseaseMiriam);
for (Chemical chemical : chemicals) {
names.add(chemical.getChemicalName());
names.addAll(chemical.getSynonyms());
}
List<String> result = new ArrayList<>(names);
Collections.sort(result);
return result;
}
public List<Chemical> getChemicalsByName(MiriamData disease, String chemicalName) throws ChemicalSearchException {
try {
if (disease == null) {
throw new InvalidArgumentException("disease cannot be null");
}
if (!dapiConnector.isValidConnection()) {
return null;
}
String url = dapiConnector.getLatestReleaseUrl(DAPI_DATABASE_NAME);
if (url == null) {
return null;
}
String content = dapiConnector
.getAuthenticatedContent(
url + "/drugs/?columns=sourceIdentifier&name=" + URLEncoder.encode(chemicalName, "UTF-8")
+ "&target_disease_identifier="
+ URLEncoder.encode(disease.getPrefixedIdentifier(), "UTF-8"));
ListChemicalEntityDto result = objectMapper.readValue(content, ListChemicalEntityDto.class);
List<Chemical> chemicals = fetchChemicals(result, disease);
if (chemicals.size() == 0) {
content = dapiConnector
.getAuthenticatedContent(
url + "/drugs/?columns=sourceIdentifier&synonym=" + URLEncoder.encode(chemicalName, "UTF-8"));
result = objectMapper.readValue(content, ListChemicalEntityDto.class);
chemicals = fetchChemicals(result, disease);
}
return chemicals;
} catch (DapiConnectionException | IOException e) {
throw new ChemicalSearchException("Problem with fetching drugbank data", e);
}
}
private List<Chemical> fetchChemicals(ListChemicalEntityDto list, MiriamData disease) throws DapiConnectionException {
List<Chemical> result = new ArrayList<>();
for (ChemicalEntityDto entity : list.getContent()) {
Chemical chemical = getByIdentifier(MiriamType.getMiriamDataFromPrefixIdentifier(entity.getSourceIdentifier()), disease);
if (chemical == null) {
logger.warn("Invalid chemical identifier: " + entity.getSourceIdentifier());
} else {
result.add(chemical);
}
}
return result;
}
private Chemical getByIdentifier(MiriamData identifier, MiriamData disease) throws DapiConnectionException {
String url = dapiConnector.getLatestReleaseUrl(DAPI_DATABASE_NAME);
if (url == null) {
return null;
}
String content = dapiConnector.getAuthenticatedContent(url + "/drugs/" + identifier.getResource());
try {
ChemicalEntityDto result = objectMapper.readValue(content, ChemicalEntityDto.class);
Chemical chemical = chemicalEntityDtoConverter.dtoToChemical(result);
Set<Target> targetsToRemove = new HashSet<>();
for (Target t:chemical.getTargets()) {
if (disease!=null && !Objects.equals(t.getAssociatedDisease(), disease)) {
targetsToRemove.add(t);
}
}
chemical.removeTargets(targetsToRemove);
return chemical;
} catch (IOException e) {
throw new DapiConnectionException("Problem with accessing dapi", e);
}
}
}
package lcsb.mapviewer.annotation.services;
package lcsb.mapviewer.annotation.services.dapi;
/**
* Exception thrown when there was a problem when searching for a chemical.
......
......@@ -2,6 +2,7 @@ package lcsb.mapviewer.annotation.services.dapi.dto;
import org.springframework.stereotype.Service;
import lcsb.mapviewer.annotation.data.Chemical;
import lcsb.mapviewer.annotation.data.Drug;
import lcsb.mapviewer.model.map.MiriamType;
......@@ -30,4 +31,15 @@ public class ChemicalEntityDtoConverter {
return result;
}
public Chemical dtoToChemical(ChemicalEntityDto dto) {
Chemical result = new Chemical();
result.setChemicalId(MiriamType.getMiriamDataFromPrefixIdentifier(dto.getSourceIdentifier()));
result.setChemicalName(dto.getName());
result.addSynonyms(dto.getSynonyms());
for (ChemicalTargetDto target : dto.getTargets()) {
result.addTarget(chemicalTargetDtoConverter.dtoToTarget(target));
}
return result;
}
}
......@@ -7,6 +7,7 @@ public class ChemicalTargetDto {
private String name;
private String sourceIdentifier;
private String associatedDisease;
private String organism;
private List<String> identifiers = new ArrayList<>();
private List<String> references = new ArrayList<>();
......@@ -50,4 +51,12 @@ public class ChemicalTargetDto {
public void setReferences(List<String> references) {
this.references = references;
}
public String getAssociatedDisease() {
return associatedDisease;
}
public void setAssociatedDisease(String associatedDisease) {
this.associatedDisease = associatedDisease;
}
}
......@@ -26,6 +26,7 @@ public class ChemicalTargetDtoConverter {
Target result = new Target();
result.setName(dto.getName());
result.setSource(MiriamType.getMiriamDataFromPrefixIdentifier(dto.getSourceIdentifier()));
result.setAssociatedDisease(MiriamType.getMiriamDataFromPrefixIdentifier(dto.getAssociatedDisease()));
result.setOrganism(MiriamType.getMiriamDataFromPrefixIdentifier(dto.getOrganism()));
for (String identifier : dto.getIdentifiers()) {
MiriamData md = MiriamType.getMiriamDataFromPrefixIdentifier(identifier);
......
......@@ -13,8 +13,6 @@ import lcsb.mapviewer.annotation.services.genome.AllGenomeTests;
AllDapiTests.class,
AllGenomeTests.class,
ChEMBLParserTest.class,
ChemicalParserTest.class,
ChemicalSearchExceptionTest.class,
DrugAnnotationTest.class,
ExternalServiceStatusTest.class,
ExternalServiceStatusTypeTest.class,
......
package lcsb.mapviewer.annotation.services;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.*;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import lcsb.mapviewer.annotation.AnnotationTestFunctions;
import lcsb.mapviewer.annotation.cache.*;
import lcsb.mapviewer.annotation.data.*;
import lcsb.mapviewer.annotation.services.annotators.AnnotatorException;
import lcsb.mapviewer.annotation.services.annotators.HgncAnnotator;
import lcsb.mapviewer.common.exception.InvalidArgumentException;
import lcsb.mapviewer.model.Project;
import lcsb.mapviewer.model.map.MiriamData;
import lcsb.mapviewer.model.map.MiriamType;
import lcsb.mapviewer.model.map.model.Model;
@Ignore("ctd decided to block us due to too many requests")
public class ChemicalParserTest extends AnnotationTestFunctions {
final MiriamData parkinsonDiseaseId = new MiriamData(MiriamType.MESH_2012, "D010300");
final MiriamData dystoniaDisease = new MiriamData(MiriamType.MESH_2012, "C538007");
Logger logger = LogManager.getLogger(ChemicalParserTest.class);
MiriamData glutathioneDisulfideChemicalId = new MiriamData(MiriamType.TOXICOGENOMIC_CHEMICAL, "D019803");
MiriamData stilbeneOxideChemicalId = new MiriamData(MiriamType.TOXICOGENOMIC_CHEMICAL, "C025906");
// lazabemide with one Gene MAOB and therapeutic and 3 publications
MiriamData lazabemideChemicalId = new MiriamData(MiriamType.TOXICOGENOMIC_CHEMICAL, "C059303");
@Autowired
private ChemicalParser chemicalParser;
@Autowired
private GeneralCacheInterface cache;
@Autowired
private PermanentDatabaseLevelCacheInterface permanentDatabaseLevelCache;
@Test
public void testCreateChemicalListFromDB() throws Exception {
// skip first call to cache, so we will have to at least parse the data
// Parkinson disease
Map<MiriamData, String> result = chemicalParser.getChemicalsForDisease(parkinsonDiseaseId);
assertNotNull(result);
assertTrue(!result.isEmpty());
}
@Test(expected = ChemicalSearchException.class)
public void testGetChemicalListWhenProblemWithConnection() throws Exception {
GeneralCacheInterface cache = chemicalParser.getCache();
WebPageDownloader downloader = chemicalParser.getWebPageDownloader();
try {
// skip first call to cache, so we will have to at least parse the data
chemicalParser.setCache(null);
WebPageDownloader mockDownloader = Mockito.mock(WebPageDownloader.class);
when(mockDownloader.getFromNetwork(anyString(), anyString(), nullable(String.class)))
.thenThrow(new IOException());
chemicalParser.setWebPageDownloader(mockDownloader);
// Parkinson disease
chemicalParser.getChemicalsForDisease(parkinsonDiseaseId);
} finally {
chemicalParser.setCache(cache);
chemicalParser.setWebPageDownloader(downloader);
}
}
@Test(expected = InvalidArgumentException.class)
public void testGetChemicalListForInvalidDiseaseId() throws Exception {
chemicalParser.getChemicalsForDisease(null);
}
@Test
public void testCreateChemicalListFromDBWithInvalidId() throws Exception {
// Parkinson disease
MiriamData diseaseID = new MiriamData(MiriamType.MESH_2012, "D01030012");
Map<MiriamData, String> result = chemicalParser.getChemicalsForDisease(diseaseID);
assertNotNull(result);
assertTrue(result.isEmpty());
}
@Test
public void testFindByIDFromChemicalList() throws Exception {
List<MiriamData> idsList = new ArrayList<>();
idsList.add(stilbeneOxideChemicalId);
idsList.add(lazabemideChemicalId);
List<Chemical> chemcials = chemicalParser.getChemicals(parkinsonDiseaseId, idsList);
assertEquals(idsList.size(), chemcials.size());
for (Chemical chem : chemcials) {
assertNotNull(chem);
for (Target t : chem.getInferenceNetwork()) {
for (MiriamData md : t.getReferences()) {
assertEquals(MiriamType.PUBMED, md.getDataType());
assertTrue(NumberUtils.isDigits(md.getResource()));
}
}
}
}
@Test
public void testGetGlutathioneDisulfideData() throws Exception {
List<MiriamData> idsList = new ArrayList<>();
idsList.add(glutathioneDisulfideChemicalId);
List<Chemical> chemicals = chemicalParser.getChemicals(parkinsonDiseaseId, idsList);
assertEquals(1, chemicals.size());
Chemical glutathioneDisulfide = chemicals.get(0);
assertTrue(glutathioneDisulfide.getSynonyms().size() > 0);
}
@Test
public void testGetChemicalBySynonym() throws Exception {
String glutathioneDisulfideSynonym = "GSSG";
List<Chemical> chemicals = chemicalParser.getChemicalsBySynonym(parkinsonDiseaseId, glutathioneDisulfideSynonym);
assertEquals(1, chemicals.size());
Chemical mptp = chemicals.get(0);
assertTrue(mptp.getSynonyms().contains(glutathioneDisulfideSynonym));
}
@Test
public void testFindByInvalidIdFromChemicalList() throws Exception {
// Parkinson disease
MiriamData chemicalId2 = new MiriamData(MiriamType.TOXICOGENOMIC_CHEMICAL, "D0198012433");
List<MiriamData> idsList = new ArrayList<MiriamData>();
idsList.add(chemicalId2);
List<Chemical> chemcials = chemicalParser.getChemicals(parkinsonDiseaseId, idsList);
assertTrue(chemcials.isEmpty());
}