/**
* Copyright (c) 2015 Lemur Consulting Ltd.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package uk.co.flax.biosolr.ontology.core.ols;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.co.flax.biosolr.ontology.core.AbstractOntologyHelper;
import uk.co.flax.biosolr.ontology.core.OntologyHelperConfiguration;
import uk.co.flax.biosolr.ontology.core.OntologyHelperException;
import uk.co.flax.biosolr.ontology.core.ols.graph.Edge;
import uk.co.flax.biosolr.ontology.core.ols.graph.Graph;
import uk.co.flax.biosolr.ontology.core.ols.graph.Node;
import uk.co.flax.biosolr.ontology.core.ols.terms.*;
import javax.ws.rs.core.UriBuilder;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
/**
* OLS-specific implementation of OntologyHelper.
*
* <p>Created by Matt Pearce on 21/10/15.</p>
*
* @author Matt Pearce
*/
public class OLSOntologyHelper extends AbstractOntologyHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(OLSOntologyHelper.class);
@SuppressWarnings("unused")
public static final int THREADPOOL_SIZE = 8;
public static final int PAGE_SIZE = 100;
static final String ENCODING = "UTF-8";
static final String ONTOLOGIES_URL_SUFFIX = "/ontologies";
static final String TERMS_URL_SUFFIX = "/terms";
static final String SIZE_PARAM = "size";
static final String PAGE_PARAM = "page";
private final OLSOntologyConfiguration configuration;
private final String baseUrl;
protected final OLSHttpClient olsClient;
// Map caching the ontology terms after lookup
private final Map<String, OntologyTerm> terms = new HashMap<>();
// Related IRI cache, keyed by IRI then relation type
private final Map<String, Map<TermLinkType, Collection<String>>> relatedIris = new HashMap<>();
// Graph cache, keyed by IRI
private final Map<String, Graph> graphs = new HashMap<>();
private final Map<String, String> graphLabels = new HashMap<>();
private long lastCallTime;
public OLSOntologyHelper(OLSOntologyConfiguration config, OLSHttpClient olsClient) {
this.configuration = config;
this.baseUrl = buildBaseUrl(config.getOlsBaseUrl(), config.getOntology());
this.olsClient = olsClient;
}
private String buildBaseUrl(final String baseUrl, final String ontology) {
String url = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
if (StringUtils.isNotBlank(ontology)) {
url = url + ONTOLOGIES_URL_SUFFIX + "/" + ontology;
}
return url;
}
@Override
public void updateLastCallTime() {
lastCallTime = System.currentTimeMillis();
}
@Override
public long getLastCallTime() {
return lastCallTime;
}
@Override
public void dispose() {
LOGGER.info("Disposing of OLS ontology helper for {}", configuration.getOntology());
olsClient.shutdown();
// Clear caches
terms.clear();
relatedIris.clear();
graphs.clear();
graphLabels.clear();
}
@Override
protected OntologyHelperConfiguration getConfiguration() {
return new OntologyHelperConfiguration();
}
/**
* Check whether a term is in the terms cache and, if not, attempt to add it.
*
* @param iri the IRI to look up.
* @throws OntologyHelperException if problems occur looking up the IRI.
*/
protected void checkTerm(String iri) throws OntologyHelperException {
checkTerms(Collections.singletonList(iri));
}
/**
* Check whether a collection of terms are in the terms cache, and if not,
* attempt to add them. Terms which cannot be found in OLS are added as a
* <code>null</code> entry, to avoid looking them up again.
*
* @param iris the collection of IRIs to be queried.
* @throws OntologyHelperException if the lookup is interrupted.
*/
private void checkTerms(final Collection<String> iris) throws OntologyHelperException {
final List<String> lookups = iris.stream()
.filter(iri -> !terms.containsKey(iri))
.collect(Collectors.toList());
if (!lookups.isEmpty()) {
List<OntologyTerm> foundTerms = lookupTerms(lookups);
// Add the found terms to the terms map
foundTerms.forEach(t -> terms.put(t.getIri(), t));
// For all not found terms, add null entries to terms map
lookups.forEach(iri -> terms.putIfAbsent(iri, null));
}
}
/**
* Look up a collection of terms in OLS.
*
* @param iris the terms to be queried.
* @return a list of those terms which were found.
* @throws OntologyHelperException if the lookup is interrupted.
*/
protected List<OntologyTerm> lookupTerms(final List<String> iris) throws OntologyHelperException {
Collection<String> callUrls = createCallUrls(iris);
return olsClient.callOLS(callUrls, OntologyTerm.class);
}
private Collection<String> createCallUrls(List<String> iris) {
// Build a list of URLs we need to call
List<String> urls = new ArrayList<>(iris.size());
for (final String iri : iris) {
try {
final String dblEncodedIri = URLEncoder.encode(URLEncoder.encode(iri, ENCODING), ENCODING);
urls.add(baseUrl + TERMS_URL_SUFFIX + "/" + dblEncodedIri);
} catch (UnsupportedEncodingException e) {
// Not expecting to get here
LOGGER.error(e.getMessage());
}
}
return urls;
}
@Override
public boolean isIriInOntology(String iri) throws OntologyHelperException {
checkTerm(iri);
return terms.containsKey(iri) && terms.get(iri) != null;
}
@Override
public Collection<String> findLabels(String iri) throws OntologyHelperException {
return findLabelsForIRIs(Collections.singletonList(iri));
}
@Override
public Collection<String> findLabelsForIRIs(Collection<String> iris) throws OntologyHelperException {
Collection<String> labels;
if (iris == null) {
labels = Collections.emptyList();
} else {
// Check if we have labels in the graph cache
labels = iris.stream()
.filter(graphLabels::containsKey)
.map(graphLabels::get)
.collect(Collectors.toList());
if (labels.size() != iris.size()) {
// Not everything in graph cache - do further lookups
Collection<String> lookups = iris.stream()
.filter(i -> !graphLabels.containsKey(i))
.collect(Collectors.toList());
checkTerms(lookups);
labels.addAll(lookups.stream()
.filter(i -> Objects.nonNull(terms.get(i)))
.map(i -> terms.get(i).getLabel())
.collect(Collectors.toList()));
}
}
return labels;
}
@Override
public Collection<String> findSynonyms(String iri) throws OntologyHelperException {
checkTerm(iri);
Collection<String> synonyms;
if (terms.get(iri) != null) {
synonyms = terms.get(iri).getSynonyms();
} else {
synonyms = Collections.emptyList();
}
return synonyms;
}
@Override
public Collection<String> findDefinitions(String iri) throws OntologyHelperException {
checkTerm(iri);
Collection<String> definitions;
if (terms.get(iri) != null) {
definitions = terms.get(iri).getDescription();
} else {
definitions = Collections.emptyList();
}
return definitions;
}
@Override
public Collection<String> getChildIris(String iri) throws OntologyHelperException {
checkTerm(iri);
return findRelatedTerms(terms.get(iri), TermLinkType.CHILDREN);
}
@Override
public Collection<String> getDescendantIris(String iri) throws OntologyHelperException {
checkTerm(iri);
return findRelatedTerms(terms.get(iri), TermLinkType.DESCENDANTS);
}
@Override
public Collection<String> getParentIris(String iri) throws OntologyHelperException {
checkTerm(iri);
return findRelatedTerms(terms.get(iri), TermLinkType.PARENTS);
}
@Override
public Collection<String> getAncestorIris(String iri) throws OntologyHelperException {
checkTerm(iri);
return findRelatedTerms(terms.get(iri), TermLinkType.ANCESTORS);
}
private Collection<String> findRelatedTerms(OntologyTerm term, TermLinkType linkType) throws OntologyHelperException {
Collection<String> iris;
if (term == null) {
iris = Collections.emptyList();
} else if (isRelationInCache(term.getIri(), linkType)) {
iris = retrieveRelatedIrisFromCache(term.getIri(), linkType);
} else {
String linkUrl = getLinkUrl(term, linkType);
if (linkUrl == null) {
iris = Collections.emptyList();
} else {
iris = queryWebServiceForTerms(linkUrl);
}
cacheRelatedIris(term.getIri(), linkType, iris);
}
return iris;
}
/**
* Extract the URL for a particular type of link from an OntologyTerm.
*
* @param term the term.
* @param linkType the type of link required.
* @return the URL, or <code>null</code> if the term is null, or doesn't
* have a link of the required type.
*/
static String getLinkUrl(OntologyTerm term, TermLinkType linkType) {
String ret = null;
if (term != null) {
Link link = term.getLinks().get(linkType.toString());
if (link != null && StringUtils.isNotBlank(link.getHref())) {
ret = link.getHref();
}
}
return ret;
}
protected boolean isRelationInCache(String iri, TermLinkType relation) {
return relatedIris.containsKey(iri) && relatedIris.get(iri).containsKey(relation);
}
protected Collection<String> retrieveRelatedIrisFromCache(String iri, TermLinkType relation) {
Collection<String> ret = null;
if (relatedIris.containsKey(iri) && relatedIris.get(iri).containsKey(relation)) {
ret = relatedIris.get(iri).get(relation);
}
return ret;
}
protected void cacheRelatedIris(String iri, TermLinkType relation, Collection<String> iris) {
if (!relatedIris.containsKey(iri)) {
relatedIris.put(iri, new HashMap<>());
}
if (relatedIris.get(iri).containsKey(relation)) {
relatedIris.get(iri).get(relation).addAll(iris);
} else {
relatedIris.get(iri).put(relation, iris);
}
}
/**
* Find the IRIs of all terms referenced by a related URL.
*
* @param baseUrl the base URL to look up, from a Link or similar
* query-type URL.
* @return a set of IRIs referencing the terms found for the
* given URL.
* @throws OntologyHelperException if problems occur accessing the
* web service.
*/
protected Set<String> queryWebServiceForTerms(String baseUrl) throws OntologyHelperException {
Set<String> retList;
// Build URL for first page
List<String> urls = buildPageUrls(baseUrl, 0, 1);
// Sort returned calls by page number
SortedSet<RelatedTermsResult> results = new TreeSet<>(
(RelatedTermsResult r1, RelatedTermsResult r2) -> r1.getPage().compareTo(r2.getPage()));
results.addAll(olsClient.callOLS(urls, RelatedTermsResult.class));
if (results.size() == 0) {
retList = Collections.emptySet();
} else {
Page page = results.first().getPage();
if (page.getTotalPages() > 1) {
// Get remaining pages
urls = buildPageUrls(baseUrl, page.getNumber() + 1, page.getTotalPages());
results.addAll(olsClient.callOLS(urls, RelatedTermsResult.class));
}
retList = new HashSet<>(page.getTotalSize());
for (RelatedTermsResult result : results) {
result.getTerms().forEach(t -> {
terms.put(t.getIri(), t);
retList.add(t.getIri());
});
}
}
return retList;
}
/**
* Build a list of URLs for a range of pages.
*
* @param baseUrl the base URL; the page size and page number will be appended to
* this as query parameters.
* @param firstPage the first page in the range, inclusive.
* @param lastPage the last page in the range, exclusive.
* @return the list of generated URLs.
*/
protected List<String> buildPageUrls(String baseUrl, int firstPage, int lastPage) {
UriBuilder builder = UriBuilder.fromUri(baseUrl)
.queryParam(SIZE_PARAM, configuration.getPageSize())
.queryParam(PAGE_PARAM, "{pageNum}");
List<String> pageUrls = new ArrayList<>(lastPage - firstPage);
for (int i = firstPage; i < lastPage; i++) {
pageUrls.add(builder.build(i).toString());
}
return pageUrls;
}
@Override
public Map<String, Collection<String>> getRelations(String iri) throws OntologyHelperException {
Map<String, Collection<String>> relations = new HashMap<>();
checkTerm(iri);
Graph graph = lookupGraph(iri);
if (graph != null) {
for (Edge e : graph.getEdgesBySource(iri, false)) {
if (!relations.containsKey(e.getLabel())) {
relations.put(e.getLabel(), new ArrayList<>());
}
relations.get(e.getLabel()).add(e.getTarget());
}
}
return relations;
}
private Graph lookupGraph(String iri) throws OntologyHelperException {
if (!graphs.containsKey(iri)) {
String graphUrl = getLinkUrl(terms.get(iri), TermLinkType.GRAPH);
if (graphUrl != null) {
List<Graph> graphResults = olsClient.callOLS(Collections.singletonList(graphUrl), Graph.class);
if (graphResults.size() > 0) {
graphs.put(iri, graphResults.get(0));
cacheGraphLabels(graphResults.get(0));
} else {
graphs.put(iri, null);
}
}
}
return graphs.get(iri);
}
private void cacheGraphLabels(Graph graph) {
if (graph.getNodes() != null) {
Map<String, String> nodeLabels = graph.getNodes().stream()
.filter(n -> !graphLabels.containsKey(n.getIri()))
.collect(Collectors.toMap(Node::getIri, Node::getLabel));
graphLabels.putAll(nodeLabels);
}
}
String getBaseUrl() {
return baseUrl;
}
}