//Dstl (c) Crown Copyright 2017 package uk.gov.dstl.baleen.consumers.utils; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.apache.uima.cas.Feature; import org.apache.uima.cas.FeatureStructure; import org.apache.uima.jcas.cas.FSArray; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.MapLikeType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.common.primitives.Ints; import uk.gov.dstl.baleen.core.history.DocumentHistory; import uk.gov.dstl.baleen.core.history.HistoryEvent; import uk.gov.dstl.baleen.core.history.HistoryEvents; import uk.gov.dstl.baleen.types.Base; import uk.gov.dstl.baleen.types.semantic.Entity; import uk.gov.dstl.baleen.types.semantic.Event; import uk.gov.dstl.baleen.types.semantic.Relation; import uk.gov.dstl.baleen.uima.UimaMonitor; import uk.gov.dstl.baleen.uima.utils.FeatureUtils; /** * Converts from an Entity or Relation into a Map representation, adding history if required. */ public class EntityRelationConverter { private final boolean outputHistory; private final DocumentHistory documentHistory; private final Set<String> stopFeatures; private final UimaMonitor monitor; private final IEntityConverterFields fields; private static final String FIELD_VALUE = "value"; private final ObjectMapper mapper = new ObjectMapper(); private final MapLikeType mapLikeType = TypeFactory .defaultInstance() .constructMapLikeType(Map.class, String.class, Object.class); /** * New instance. * * @param monitor * the monitor to log to * @param outputHistory * true if should output history * @param documentHistory * the history of the document * @param stopFeatures * features which should be excluded from the serialisation * @param fields * fields to map properties to */ public EntityRelationConverter(UimaMonitor monitor, boolean outputHistory, DocumentHistory documentHistory, Set<String> stopFeatures, IEntityConverterFields fields) { this.monitor = monitor; this.outputHistory = outputHistory; this.documentHistory = documentHistory; this.stopFeatures = stopFeatures; this.fields = fields; } private UimaMonitor getMonitor() { return monitor; } /** * Convert from an entity to a map. * * @param entity * the entity to convert * @return a map containing the entity's fields (and history is required) */ public Map<String, Object> convertEntity(Entity entity) { Map<String, Object> map = Maps.newHashMap(); convertFeatures(map, entity); if (outputHistory && documentHistory != null) { Collection<HistoryEvent> events = documentHistory.getHistory(entity.getInternalId()); convertHistory(map, events, entity.getInternalId()); } putIfExists(map, fields.getExternalId(), entity.getExternalId()); return map; } /** * Convert from a relation to a map. * * @param relation * the relation to convert * @return a map containing the relation's fields (and history is required) */ public Map<String, Object> convertRelation(Relation relation) { Map<String, Object> map = Maps.newHashMap(); convertFeatures(map, relation); if (outputHistory && documentHistory != null) { Collection<HistoryEvent> events = documentHistory.getHistory(relation.getInternalId()); convertHistory(map, events, relation.getInternalId()); } putIfExists(map, fields.getExternalId(), relation.getExternalId()); return map; } /** * Convert from an event to a map. * * @param event * the relation to convert * @return a map containing the relation's fields (and history is required) */ public Map<String, Object> convertEvent(Event event) { Map<String, Object> map = Maps.newHashMap(); convertFeatures(map, event); if (outputHistory && documentHistory != null) { Collection<HistoryEvent> events = documentHistory.getHistory(event.getInternalId()); convertHistory(map, events, event.getInternalId()); } putIfExists(map, fields.getExternalId(), event.getExternalId()); return map; } private void convertFeatures(Map<String, Object> map, Base base) { for (Feature f : base.getType().getFeatures()) { if (stopFeatures.contains(f.getName())) { continue; } try { convertFeature(map, base, f); } catch (Exception e) { getMonitor().warn( "Couldn't output {} to map. Type '{}' isn't supported.", f.getName(), f.getRange().getShortName(), e); } } map.put("type", base.getType().getShortName()); if (map.get(FIELD_VALUE) == null || Strings.isNullOrEmpty(map.get(FIELD_VALUE).toString())) { map.put(FIELD_VALUE, base.getCoveredText()); } } private void convertFeature(Map<String, Object> map, Base base, Feature f) { if (f.getRange().isPrimitive()) { if ("geoJson".equals(f.getShortName())) { getMonitor().trace("Feature is GeoJSON - parsing to a database object"); putGeoJson(map, base.getFeatureValueAsString(f)); } else { getMonitor().trace("Converting primitive feature to an object"); map.put(ConsumerUtils.toCamelCase(f.getShortName()), FeatureUtils.featureToObject(f, base)); } } else if (f.getRange().isArray() && f.getRange().getComponentType() != null && f.getRange().getComponentType().isPrimitive()) { getMonitor().trace("Converting primitive feature to an array"); map.put(ConsumerUtils.toCamelCase(f.getShortName()), FeatureUtils.featureToList(f, base)); } else { getMonitor().trace("Feature is not a primitive type - will try to treat the feature as an entity"); if (f.getRange().isArray()) { getMonitor().trace("Feature is an array - attempting converstion to an array of entities"); FSArray fArr = (FSArray) base.getFeatureValue(f); if (fArr != null) { map.put(ConsumerUtils.toCamelCase(f.getShortName()), getEntityIds(fArr)); } } else { getMonitor().trace("Feature is singular - attempting conversion to a single entity"); FeatureStructure ent = base.getFeatureValue(f); if (ent == null) { // Ignore null entities } else if (ent instanceof Entity) { map.put(ConsumerUtils.toCamelCase(f.getShortName()), ((Entity) ent).getExternalId()); } else { getMonitor().trace("Unable to persist feature {}", f.getShortName()); } } } } private void putGeoJson(Map<String, Object> map, String geojson) { try { if (!Strings.isNullOrEmpty(geojson)) { putIfExists(map, fields.getGeoJSON(), mapper.readValue(geojson, mapLikeType)); } } catch (IOException e) { getMonitor().warn("Unable to persist geoJson", e); } } private List<String> getEntityIds(FSArray entityArray) { List<String> entities = new ArrayList<>(); for (int x = 0; x < entityArray.size(); x++) { FeatureStructure featureStructure = entityArray.get(x); if (featureStructure instanceof Entity) { Entity ent = (Entity) featureStructure; entities.add(ent.getExternalId()); } } return entities; } private void convertHistory(Map<String, Object> map, Collection<HistoryEvent> events, long entityInternalId) { List<Object> list = new LinkedList<Object>(); saveEvents(list, events, entityInternalId); putIfExists(map, fields.getHistory(), list); } private void saveEvents(List<Object> list, Collection<HistoryEvent> events, long entityInternalId) { for (HistoryEvent event : events) { saveEvent(list, event, entityInternalId); } } private void saveEvent(List<Object> list, HistoryEvent event, long entityInternalId) { Map<String, Object> e = new LinkedHashMap<String, Object>(); if (event.getRecordable().getInternalId() != entityInternalId) { // Only save the internal id as a reference to entities which aren't this one. putIfExists(e, fields.getHistoryRecordable(), event.getRecordable().getInternalId()); } putIfExists(e, fields.getHistoryAction(), event.getAction()); putIfExists(e, fields.getHistoryType(), event.getEventType()); putIfExists(e, fields.getHistoryParameters(), event.getParameters()); putIfExists(e, fields.getHistoryReferrer(), event.getReferrer()); putIfExists(e, fields.getHistoryTimestamp(), event.getTimestamp()); list.add(e); if (HistoryEvents.MERGED_TYPE.equalsIgnoreCase(event.getEventType()) && event.getParameters() != null && event.getParameters(HistoryEvents.PARAM_MERGED_ID).isPresent()) { Optional<String> mergedId = event.getParameters(HistoryEvents.PARAM_MERGED_ID); Integer id = Ints.tryParse(mergedId.get()); if (id != null) { Collection<HistoryEvent> mergedEvents = documentHistory.getHistory(id); if (mergedEvents != null) { saveEvents(list, mergedEvents, entityInternalId); } else { getMonitor().warn("Null history for {}", id); } } else { getMonitor().warn("No merge id for merge history of {}", event.getRecordable()); } } } private void putIfExists(Map<String, Object> map, String key, Object value) { if (!Strings.isNullOrEmpty(key)) { map.put(key, value); } } }