/**
* This file is hereby placed into the Public Domain. This means anyone is
* free to do whatever they wish with this file.
*/
package mil.nga.giat.data.elasticsearch;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vividsolutions.jts.geom.Geometry;
import mil.nga.giat.shaded.es.common.joda.Joda;
import mil.nga.giat.shaded.joda.time.format.DateTimeFormatter;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.DATE_FORMAT;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.FULL_NAME;
import org.geotools.data.FeatureReader;
import org.geotools.data.store.ContentState;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* FeatureReader access to the Elasticsearch index.
*/
public class ElasticFeatureReader implements FeatureReader<SimpleFeatureType, SimpleFeature> {
private final static Logger LOGGER = Logging.getLogger(ElasticFeatureReader.class);
private final ContentState state;
private final SimpleFeatureType featureType;
private final float maxScore;
private final ObjectMapper mapper;
private SimpleFeatureBuilder builder;
private Iterator<ElasticHit> searchHitIterator;
private Iterator<Map<String,Object>> aggregationIterator;
private ElasticParserUtil parserUtil;
public ElasticFeatureReader(ContentState contentState, ElasticResponse response) {
this(contentState, response.getHits(), response.getAggregations(), response.getMaxScore());
}
public ElasticFeatureReader(ContentState contentState, List<ElasticHit> hits, Map<String,ElasticAggregation> aggregations, float maxScore) {
this.state = contentState;
this.featureType = state.getFeatureType();
this.searchHitIterator = hits.iterator();
this.builder = new SimpleFeatureBuilder(featureType);
this.parserUtil = new ElasticParserUtil();
this.maxScore = maxScore;
this.aggregationIterator = Collections.emptyIterator();
if (aggregations != null && !aggregations.isEmpty()) {
String aggregationName = aggregations.keySet().stream().findFirst().orElse(null);
if (aggregations.size() > 1) {
LOGGER.info("Result has multiple aggregations. Using " + aggregationName);
}
if (aggregations.get(aggregationName).getBuckets() != null) {
this.aggregationIterator = aggregations.get(aggregationName).getBuckets().iterator();
}
}
this.mapper = new ObjectMapper();
}
@Override
public SimpleFeatureType getFeatureType() {
return this.featureType;
}
@Override
public SimpleFeature next() {
String id = searchHitIterator.hasNext() ? nextHit() : nextAggregation();
return builder.buildFeature(id);
}
private String nextHit() {
final ElasticHit hit = searchHitIterator.next();
final SimpleFeatureType type = getFeatureType();
final Map<String, Object> source = hit.getSource();
final Float score;
final Float relativeScore;
if (hit.getScore() != null && !Float.isNaN(hit.getScore()) && maxScore>0) {
score = hit.getScore();
relativeScore = score / maxScore;
} else {
score = null;
relativeScore = null;
}
for (final AttributeDescriptor descriptor : type.getAttributeDescriptors()) {
final String name = descriptor.getType().getName().getLocalPart();
final String sourceName = (String) descriptor.getUserData().get(FULL_NAME);
List<Object> values = hit.field(sourceName);
if (values == null && source != null) {
// read field from source
values = parserUtil.readField(source, sourceName);
}
if (values == null && name.equals("_id")) {
builder.set(name, hit.getId());
} else if (values == null && name.equals("_index")) {
builder.set(name, hit.getIndex());
} else if (values == null && name.equals("_type")) {
builder.set(name, hit.getType());
} else if (values == null && name.equals("_score")) {
builder.set(name, score);
} else if (values == null && name.equals("_relative_score")) {
builder.set(name, relativeScore);
} else if (values == null) {
// skip missing attribute
} else if (Geometry.class.isAssignableFrom(descriptor.getType().getBinding())) {
if (sourceName.endsWith(".coordinates") && values instanceof List) {
builder.set(name, parserUtil.createGeometry(values));
} else {
builder.set(name, parserUtil.createGeometry(values.get(0)));
}
} else if (Date.class.isAssignableFrom(descriptor.getType().getBinding())) {
Object dataVal = values.get(0);
if (dataVal instanceof Double) {
builder.set(name, new Date(Math.round((Double) dataVal)));
} else if (dataVal instanceof Integer) {
builder.set(name, new Date((Integer) dataVal));
} else if (dataVal instanceof Long) {
builder.set(name, new Date((long) dataVal));
} else {
final String format = (String) descriptor.getUserData().get(DATE_FORMAT);
final DateTimeFormatter dateFormatter = Joda.forPattern(format).parser();
Date date = dateFormatter.parseDateTime((String) dataVal).toDate();
builder.set(name, date);
}
} else if (values.size() == 1) {
builder.set(name, values.get(0));
} else if (String.class.isAssignableFrom(descriptor.getType().getBinding())) {
final StringBuilder valueBuilder = new StringBuilder();
for (final Object value : values) {
valueBuilder.append(valueBuilder.length()>0 ? ";" : "");
valueBuilder.append(value);
}
builder.set(name, valueBuilder.toString());
} else if (!name.equals("_aggregation")) {
builder.set(name, values);
}
}
return state.getEntry().getTypeName() + "." + hit.getId();
}
private String nextAggregation() {
final Map<String, Object> aggregation = aggregationIterator.next();
try {
final byte[] data = mapper.writeValueAsBytes(aggregation);
builder.set("_aggregation", data);
} catch (IOException e) {
LOGGER.warning("Unable to set aggregation. Try reloading layer.");
}
return null;
}
@Override
public boolean hasNext() {
return searchHitIterator.hasNext() || aggregationIterator.hasNext();
}
@Override
public void close() {
builder = null;
searchHitIterator = null;
}
}