/**
* 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.elasticsearch.mapper.ontology;
import com.google.common.collect.Iterators;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.document.Field;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.collect.CopyOnWriteHashMap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.mapper.*;
import org.elasticsearch.index.mapper.core.StringFieldMapper;
import uk.co.flax.biosolr.elasticsearch.OntologyHelperBuilder;
import uk.co.flax.biosolr.ontology.core.OntologyData;
import uk.co.flax.biosolr.ontology.core.OntologyDataBuilder;
import uk.co.flax.biosolr.ontology.core.OntologyHelper;
import uk.co.flax.biosolr.ontology.core.OntologyHelperException;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import static org.elasticsearch.index.mapper.core.TypeParsers.parseField;
/**
* Mapper class to expand ontology details from an ontology
* annotation field value.
*
* @author mlp
*/
public class OntologyMapper extends FieldMapper {
public static final String CONTENT_TYPE = "ontology";
public static final String ONTOLOGY_PROPERTIES = "properties";
public static final String DYNAMIC_URI_FIELD_SUFFIX = "_rel_uris";
public static final String DYNAMIC_LABEL_FIELD_SUFFIX = "_rel_labels";
private static final ESLogger logger = ESLoggerFactory.getLogger(OntologyMapper.class.getName());
public static class Defaults {
public static final ContentPath.Type PATH_TYPE = ContentPath.Type.FULL;
public static final MappedFieldType LABEL_FIELD_TYPE = new StringFieldMapper.StringFieldType();
public static final MappedFieldType URI_FIELD_TYPE = new StringFieldMapper.StringFieldType();
public static final MappedFieldType FIELD_TYPE = new StringFieldMapper.StringFieldType();
static {
LABEL_FIELD_TYPE.setStored(true);
LABEL_FIELD_TYPE.setTokenized(true);
LABEL_FIELD_TYPE.freeze();
URI_FIELD_TYPE.setStored(true);
URI_FIELD_TYPE.setTokenized(false);
URI_FIELD_TYPE.freeze();
FIELD_TYPE.setStored(true);
FIELD_TYPE.freeze();
}
}
public static class Builder extends FieldMapper.Builder<Builder, OntologyMapper> {
private final ContentPath.Type pathType = Defaults.PATH_TYPE;
private OntologySettings ontologySettings;
private Map<String, StringFieldMapper.Builder> propertyBuilders;
public Builder(String name) {
super(name, Defaults.FIELD_TYPE);
builder = this;
}
public Builder ontologySettings(OntologySettings settings) {
this.ontologySettings = settings;
return this;
}
public Builder propertyBuilders(Map<String, StringFieldMapper.Builder> props) {
this.propertyBuilders = props;
return this;
}
@Override
public OntologyMapper build(BuilderContext context) {
ContentPath.Type origPathType = context.path().pathType();
context.path().pathType(pathType);
Map<String, StringFieldMapper> fieldMappers = new HashMap<>();
context.path().add(name);
if (propertyBuilders != null) {
for (String property : propertyBuilders.keySet()) {
StringFieldMapper sfm = propertyBuilders.get(property).build(context);
fieldMappers.put(sfm.fieldType().names().indexName(), sfm);
}
}
// Initialise field mappers for the pre-defined fields
for (FieldMappings mapping : ontologySettings.getFieldMappings()) {
if (!fieldMappers.containsKey(context.path().fullPathAsText(mapping.getFieldName()))) {
StringFieldMapper mapper = MapperBuilders.stringField(mapping.getFieldName())
.store(true)
.index(true)
.tokenized(!mapping.isUriField())
.includeInAll(true)
.build(context);
fieldMappers.put(mapper.fieldType().names().indexName(), mapper);
}
}
context.path().remove(); // remove name
context.path().pathType(origPathType);
setupFieldType(context);
return new OntologyMapper(name, fieldType, defaultFieldType, context.indexSettings(),
multiFieldsBuilder.build(this, context),
ontologySettings, fieldMappers);
}
}
public static class TypeParser implements Mapper.TypeParser {
public TypeParser() {
}
/**
* Parse the mapping definition for the ontology type.
*
* @param name the field name
* @param node the JSON node holding the mapping definitions.
* @param parserContext the parser context object.
* @return a Builder for an OntologyMapper.
*/
@SuppressWarnings("unchecked")
@Override
public Builder parse(String name, Map<String, Object> node, ParserContext parserContext)
throws MapperParsingException {
OntologySettings ontologySettings = null;
Builder builder = new Builder(name);
parseField(builder, name, node, parserContext);
for (Iterator<Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext(); ) {
Entry<String, Object> entry = iterator.next();
if (entry.getKey().equals(OntologySettings.ONTOLOGY_SETTINGS_KEY)) {
ontologySettings = new OntologySettingsBuilder()
.settingsNode((Map<String, Object>) entry.getValue())
.build();
iterator.remove();
} else if (entry.getKey().equals(ONTOLOGY_PROPERTIES)) {
Map<String, StringFieldMapper.Builder> builders = parseProperties((Map<String, Object>) entry.getValue(), parserContext);
builder = builder.propertyBuilders(builders);
iterator.remove();
}
}
if (ontologySettings == null) {
throw new MapperParsingException("No ontology settings supplied");
} else if (StringUtils.isBlank(ontologySettings.getOntologyUri())
&& StringUtils.isBlank(ontologySettings.getOlsBaseUrl())) {
throw new MapperParsingException("No ontology URI or OLS details supplied");
} else {
builder = builder.ontologySettings(ontologySettings);
}
return builder;
}
private Map<String, StringFieldMapper.Builder> parseProperties(Map<String, Object> propertiesNode, ParserContext parserContext) {
Map<String, StringFieldMapper.Builder> propertyMap = new HashMap<>();
for (Iterator<Entry<String, Object>> iterator = propertiesNode.entrySet().iterator(); iterator.hasNext(); ) {
Entry<String, Object> entry = iterator.next();
String name = entry.getKey();
@SuppressWarnings("unchecked")
Mapper.Builder builder = new StringFieldMapper.TypeParser().parse(entry.getKey(), (Map<String, Object>) entry.getValue(), parserContext);
propertyMap.put(name, (StringFieldMapper.Builder) builder);
}
return propertyMap;
}
}
private final Object mutex = new Object();
private final OntologySettings ontologySettings;
private volatile CopyOnWriteHashMap<String, StringFieldMapper> mappers;
public OntologyMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType,
Settings indexSettings, MultiFields multiFields, OntologySettings oSettings,
Map<String, StringFieldMapper> fieldMappers) {
super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, null);
this.ontologySettings = oSettings;
// Dynamic mappers are added to mappers map as they are used/created
this.mappers = CopyOnWriteHashMap.copyOf(fieldMappers);
}
@Override
public String contentType() {
return CONTENT_TYPE;
}
@Override
protected void parseCreateField(ParseContext context, List<Field> fields) throws IOException {
throw new UnsupportedOperationException(
"Parsing is implemented in parse(), this method should NEVER be called");
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(name());
builder.field("type", CONTENT_TYPE);
builder.startObject(OntologySettings.ONTOLOGY_SETTINGS_KEY);
if (StringUtils.isNotBlank(ontologySettings.getOntologyUri())) {
builder.field(OntologySettings.ONTOLOGY_URI_PARAM, ontologySettings.getOntologyUri());
builder.field(OntologySettings.LABEL_URI_PARAM, ontologySettings.getLabelPropertyUris());
builder.field(OntologySettings.DEFINITION_URI_PARAM, ontologySettings.getDefinitionPropertyUris());
builder.field(OntologySettings.SYNONYM_URI_PARAM, ontologySettings.getSynonymPropertyUris());
}
if (StringUtils.isNotBlank(ontologySettings.getOlsBaseUrl())) {
builder.field(OntologySettings.OLS_BASE_URL_PARAM, ontologySettings.getOlsBaseUrl());
builder.field(OntologySettings.OLS_ONTOLOGY_PARAM, ontologySettings.getOlsOntology());
}
builder.field(OntologySettings.INCLUDE_INDIRECT_PARAM, ontologySettings.isIncludeIndirect());
builder.field(OntologySettings.INCLUDE_RELATIONS_PARAM, ontologySettings.isIncludeRelations());
builder.field(OntologySettings.INCLUDE_PARENT_PATHS_PARAM, ontologySettings.isIncludeParentPaths());
builder.field(OntologySettings.INCLUDE_PARENT_PATH_LABELS_PARAM, ontologySettings.isIncludeParentPathLabels());
builder.endObject();
if (!mappers.isEmpty()) {
Mapper[] sortedMappers = mappers.values().toArray(new Mapper[mappers.size()]);
Arrays.sort(sortedMappers, new Comparator<Mapper>() {
@Override
public int compare(Mapper o1, Mapper o2) {
return o1.name().compareTo(o2.name());
}
});
builder.startObject(ONTOLOGY_PROPERTIES);
for (Mapper mapper : sortedMappers) {
mapper.toXContent(builder, params);
}
builder.endObject(); // ontology_properties
}
builder.endObject(); // name
return builder;
}
@Override
public Mapper parse(ParseContext context) throws IOException {
String iri;
XContentParser parser = context.parser();
XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.VALUE_STRING) {
iri = parser.text();
} else {
throw new MapperParsingException(name() + " does not contain String value");
}
ContentPath.Type origPathType = context.path().pathType();
context.path().pathType(ContentPath.Type.FULL);
context.path().add(simpleName());
boolean modified = false;
try {
OntologyHelper helper = OntologyHelperBuilder.getOntologyHelper(ontologySettings);
OntologyData data = findOntologyData(helper, iri);
if (data == null) {
logger.debug("Cannot find OWL class for IRI {}", iri);
} else {
// Add the IRI
addFieldData(context, getPredefinedMapper(FieldMappings.URI, context), Collections.singletonList(iri));
// Look up the label(s)
addFieldData(context, getPredefinedMapper(FieldMappings.LABEL, context), data.getLabels());
// Look up the synonyms
addFieldData(context, getPredefinedMapper(FieldMappings.SYNONYMS, context), data.getLabels());
// Add the child details
addRelatedNodesWithLabels(context, data.getChildIris(), getPredefinedMapper(FieldMappings.CHILD_URI, context), data.getChildLabels(),
getPredefinedMapper(FieldMappings.CHILD_LABEL, context));
// Add the parent details
addRelatedNodesWithLabels(context, data.getParentIris(), getPredefinedMapper(FieldMappings.PARENT_URI, context), data.getParentLabels(),
getPredefinedMapper(FieldMappings.PARENT_LABEL, context));
if (ontologySettings.isIncludeIndirect()) {
// Add the descendant details
addRelatedNodesWithLabels(context, data.getDescendantIris(), getPredefinedMapper(FieldMappings.DESCENDANT_URI, context), data.getDescendantLabels(),
getPredefinedMapper(FieldMappings.DESCENDANT_LABEL, context));
// Add the ancestor details
addRelatedNodesWithLabels(context, data.getAncestorIris(), getPredefinedMapper(FieldMappings.ANCESTOR_URI, context), data.getAncestorLabels(),
getPredefinedMapper(FieldMappings.ANCESTOR_LABEL, context));
}
if (ontologySettings.isIncludeRelations()) {
// Add the related nodes
Map<String, Collection<String>> relations = data.getRelationIris();
for (String relation : relations.keySet()) {
// Sanitise the relation name
String sanRelation = relation.replaceAll("\\W+", "_");
String uriMapperName = sanRelation + DYNAMIC_URI_FIELD_SUFFIX;
String labelMapperName = sanRelation + DYNAMIC_LABEL_FIELD_SUFFIX;
// Get the mapper for the relation
StringFieldMapper uriMapper = mappers.get(context.path().fullPathAsText(uriMapperName));
StringFieldMapper labelMapper = mappers.get(context.path().fullPathAsText(labelMapperName));
if (uriMapper == null) {
// No mappers created yet - build new ones for URI and label
BuilderContext builderContext = new BuilderContext(context.indexSettings(), context.path());
uriMapper = MapperBuilders.stringField(uriMapperName)
.store(true)
.index(true)
.tokenized(false)
.includeInAll(true)
.build(builderContext);
labelMapper = MapperBuilders.stringField(labelMapperName)
.store(true)
.index(true)
.tokenized(true)
.includeInAll(true)
.build(builderContext);
synchronized (mutex) {
mappers = mappers.copyAndPut(uriMapper.fieldType().names().indexName(), uriMapper);
mappers = mappers.copyAndPut(labelMapper.fieldType().names().indexName(), labelMapper);
}
modified = true;
}
addRelatedNodesWithLabels(context, relations.get(relation), uriMapper,
helper.findLabelsForIRIs(relations.get(relation)), labelMapper);
}
if (ontologySettings.isIncludeParentPaths()) {
// Add the parent paths
addFieldData(context, getPredefinedMapper(FieldMappings.PARENT_PATHS, context), data.getParentPaths());
}
}
}
helper.updateLastCallTime();
} catch (OntologyHelperException e) {
throw new ElasticsearchException("Could not initialise ontology helper", e);
} finally {
context.path().remove();
context.path().pathType(origPathType);
}
return modified ? this : null;
}
private StringFieldMapper getPredefinedMapper(FieldMappings mapping, ParseContext context) {
String mappingName = context.path().fullPathAsText(mapping.getFieldName());
return mappers.get(mappingName);
}
private OntologyData findOntologyData(OntologyHelper helper, String iri) {
OntologyData data = null;
try {
data = new OntologyDataBuilder(helper, iri)
.includeSynonyms(true)
.includeDefinitions(true)
.includeIndirect(ontologySettings.isIncludeIndirect())
.includeRelations(ontologySettings.isIncludeRelations())
.includeParentPaths(ontologySettings.isIncludeParentPaths())
.includeParentPathLabels(ontologySettings.isIncludeParentPathLabels())
.build();
} catch (OntologyHelperException e) {
logger.error("Problem building ontology data for {}: {}", iri, e.getMessage());
}
return data;
}
private void addFieldData(ParseContext context, StringFieldMapper mapper, Collection<String> data) throws IOException {
if (data != null && !data.isEmpty()) {
for (String value : data) {
ParseContext evc = context.createExternalValueContext(value);
mapper.parse(evc);
}
}
}
private void addRelatedNodesWithLabels(ParseContext context, Collection<String> iris, StringFieldMapper iriMapper,
Collection<String> labels, StringFieldMapper labelMapper) throws IOException {
if (!iris.isEmpty()) {
addFieldData(context, iriMapper, iris);
addFieldData(context, labelMapper, labels);
}
}
@Override
public void merge(Mapper mergeWith, MergeResult mergeResult) throws MergeMappingException {
super.merge(mergeWith, mergeResult);
if (!this.getClass().equals(mergeWith.getClass())) {
return;
}
OntologyMapper oMergeWith = (OntologyMapper) mergeWith;
OntologySettings mergeSettings = oMergeWith.ontologySettings;
if (mergeSettings.getOntologyUri() != null && !mergeSettings.getOntologyUri().equals(ontologySettings.getOntologyUri())) {
mergeResult.addConflict("mapper [" + fieldType().names().fullName() + "] has different ontology URI");
} else if (mergeSettings.getOlsBaseUrl() != null && !mergeSettings.getOlsBaseUrl().equals(ontologySettings.getOlsBaseUrl())) {
mergeResult.addConflict("mapper [" + fieldType().names().fullName() + "] has different OLS base URL");
}
// Not sure if the below is necessary or not...
if (!mergeResult.simulate() && !mergeResult.hasConflicts()) {
// Merge the mappers
List<FieldMapper> newFieldMappers = null;
Map<String, StringFieldMapper> newMapperMap = null;
for (Entry<String, StringFieldMapper> entry : oMergeWith.mappers.entrySet()) {
StringFieldMapper mergeIntoMapper = mappers.get(entry.getKey());
if (mergeIntoMapper == null) {
if (newFieldMappers == null) {
newFieldMappers = new ArrayList<>(oMergeWith.mappers.size());
newMapperMap = new HashMap<>();
}
newFieldMappers.add(entry.getValue());
newMapperMap.put(entry.getKey(), entry.getValue());
} else {
mergeIntoMapper.merge(entry.getValue(), mergeResult);
}
}
if (newFieldMappers != null) {
mergeResult.addFieldMappers(newFieldMappers);
mappers = mappers.copyAndPutAll(newMapperMap);
}
}
}
@Override
public Iterator<Mapper> iterator() {
List<Mapper> extras = new ArrayList<>(mappers.values());
return Iterators.concat(super.iterator(), extras.iterator());
}
@Override
public boolean isGenerated() {
return true;
}
}