package mil.nga.giat.geowave.adapter.vector.query.cql;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.geotools.filter.text.cql2.CQL;
import org.geotools.filter.text.cql2.CQLException;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.Filter;
import com.vividsolutions.jts.geom.Geometry;
import mil.nga.giat.geowave.adapter.vector.GeotoolsFeatureDataAdapter;
import mil.nga.giat.geowave.adapter.vector.plugin.ExtractAttributesFilter;
import mil.nga.giat.geowave.adapter.vector.plugin.ExtractGeometryFilterVisitor;
import mil.nga.giat.geowave.adapter.vector.plugin.ExtractGeometryFilterVisitorResult;
import mil.nga.giat.geowave.adapter.vector.plugin.ExtractTimeFilterVisitor;
import mil.nga.giat.geowave.adapter.vector.util.QueryIndexHelper;
import mil.nga.giat.geowave.adapter.vector.utils.TimeDescriptors;
import mil.nga.giat.geowave.core.geotime.GeometryUtils;
import mil.nga.giat.geowave.core.geotime.GeometryUtils.GeoConstraintsWrapper;
import mil.nga.giat.geowave.core.geotime.index.dimension.LatitudeDefinition;
import mil.nga.giat.geowave.core.geotime.index.dimension.TimeDefinition;
import mil.nga.giat.geowave.core.geotime.store.dimension.LatitudeField;
import mil.nga.giat.geowave.core.geotime.store.dimension.LongitudeField;
import mil.nga.giat.geowave.core.geotime.store.dimension.TimeField;
import mil.nga.giat.geowave.core.geotime.store.filter.SpatialQueryFilter.CompareOperation;
import mil.nga.giat.geowave.core.geotime.store.query.SpatialQuery;
import mil.nga.giat.geowave.core.geotime.store.query.SpatialTemporalQuery;
import mil.nga.giat.geowave.core.geotime.store.query.TemporalConstraints;
import mil.nga.giat.geowave.core.geotime.store.query.TemporalConstraintsSet;
import mil.nga.giat.geowave.core.geotime.store.query.TemporalQuery;
import mil.nga.giat.geowave.core.index.ByteArrayRange;
import mil.nga.giat.geowave.core.index.NumericIndexStrategy;
import mil.nga.giat.geowave.core.index.PersistenceUtils;
import mil.nga.giat.geowave.core.index.dimension.NumericDimensionDefinition;
import mil.nga.giat.geowave.core.index.sfc.data.MultiDimensionalNumericData;
import mil.nga.giat.geowave.core.store.dimension.NumericDimensionField;
import mil.nga.giat.geowave.core.store.filter.DistributableQueryFilter;
import mil.nga.giat.geowave.core.store.filter.QueryFilter;
import mil.nga.giat.geowave.core.store.index.CommonIndexModel;
import mil.nga.giat.geowave.core.store.index.Index;
import mil.nga.giat.geowave.core.store.index.PrimaryIndex;
import mil.nga.giat.geowave.core.store.index.SecondaryIndex;
import mil.nga.giat.geowave.core.store.query.BasicQuery;
import mil.nga.giat.geowave.core.store.query.BasicQuery.Constraints;
import mil.nga.giat.geowave.core.store.query.DistributableQuery;
import mil.nga.giat.geowave.core.store.query.Query;
public class CQLQuery implements
DistributableQuery
{
private final static Logger LOGGER = LoggerFactory.getLogger(CQLQuery.class);
private Query baseQuery;
private CQLQueryFilter filter;
private Filter cqlFilter;
protected CQLQuery() {}
public static Query createOptimalQuery(
final String cql,
final GeotoolsFeatureDataAdapter adapter,
final PrimaryIndex index )
throws CQLException {
return createOptimalQuery(
cql,
adapter,
index,
null);
}
public static Query createOptimalQuery(
final String cql,
final GeotoolsFeatureDataAdapter adapter,
final PrimaryIndex index,
final BasicQuery baseQuery )
throws CQLException {
return createOptimalQuery(
cql,
adapter,
CompareOperation.INTERSECTS,
index,
baseQuery);
}
public static Query createOptimalQuery(
final String cql,
final GeotoolsFeatureDataAdapter adapter,
final CompareOperation geoCompareOp,
final PrimaryIndex index,
final BasicQuery baseQuery )
throws CQLException {
final Filter cqlFilter = CQL.toFilter(cql);
return createOptimalQuery(
cqlFilter,
adapter,
geoCompareOp,
index,
baseQuery);
}
public static Query createOptimalQuery(
final Filter cqlFilter,
final GeotoolsFeatureDataAdapter adapter,
final PrimaryIndex index,
final BasicQuery baseQuery ) {
return createOptimalQuery(
cqlFilter,
adapter,
CompareOperation.INTERSECTS,
index,
baseQuery);
}
public static Query createOptimalQuery(
final Filter cqlFilter,
final GeotoolsFeatureDataAdapter adapter,
final CompareOperation geoCompareOp,
final PrimaryIndex index,
BasicQuery baseQuery ) {
final ExtractAttributesFilter attributesVisitor = new ExtractAttributesFilter();
final Object obj = cqlFilter.accept(
attributesVisitor,
null);
final Collection<String> attrs;
if ((obj != null) && (obj instanceof Collection)) {
attrs = (Collection<String>) obj;
}
else {
attrs = new ArrayList<String>();
}
// assume the index can't handle spatial or temporal constraints if its
// null
final boolean isSpatial = index == null ? false : hasAtLeastSpatial(index);
final boolean isTemporal = index == null ? false : hasTime(index) && adapter.hasTemporalConstraints();
if (isSpatial) {
final String geomName = adapter.getFeatureType().getGeometryDescriptor().getLocalName();
attrs.remove(geomName);
}
if (isTemporal) {
final TimeDescriptors timeDescriptors = adapter.getTimeDescriptors();
if (timeDescriptors != null) {
final AttributeDescriptor timeDesc = timeDescriptors.getTime();
if (timeDesc != null) {
attrs.remove(timeDesc.getLocalName());
}
final AttributeDescriptor startDesc = timeDescriptors.getStartRange();
if (startDesc != null) {
attrs.remove(startDesc.getLocalName());
}
final AttributeDescriptor endDesc = timeDescriptors.getEndRange();
if (endDesc != null) {
attrs.remove(endDesc.getLocalName());
}
}
}
if (baseQuery == null) {
// there is only space and time
final ExtractGeometryFilterVisitorResult geometryAndCompareOp = ExtractGeometryFilterVisitor
.getConstraints(
cqlFilter,
adapter.getFeatureType().getCoordinateReferenceSystem());
final TemporalConstraintsSet timeConstraintSet = new ExtractTimeFilterVisitor(
adapter.getTimeDescriptors()).getConstraints(cqlFilter);
if (geometryAndCompareOp != null) {
Geometry geometry = geometryAndCompareOp.getGeometry();
final GeoConstraintsWrapper geoConstraints = GeometryUtils
.basicGeoConstraintsWrapperFromGeometry(geometry);
Constraints constraints = geoConstraints.getConstraints();
final CompareOperation extractedCompareOp = geometryAndCompareOp.getCompareOp();
if ((timeConstraintSet != null) && !timeConstraintSet.isEmpty()) {
// determine which time constraints are associated with an
// indexable
// field
final TemporalConstraints temporalConstraints = QueryIndexHelper
.getTemporalConstraintsForDescriptors(
adapter.getTimeDescriptors(),
timeConstraintSet);
// convert to constraints
final Constraints timeConstraints = SpatialTemporalQuery.createConstraints(
temporalConstraints,
false);
constraints = geoConstraints.getConstraints().merge(
timeConstraints);
}
// TODO: this actually doesn't boost performance much, if at
// all, and one key is missing - the query geometry has to be
// topologically equivalent to its envelope and the ingested
// geometry has to be topologically equivalent to its envelope
// this could be kept as a statistic on ingest, but considering
// it doesn't boost performance it may not be worthwhile
// pursuing
// if (geoConstraints.isConstraintsMatchGeometry() &&
// CompareOperation.INTERSECTS.equals(geoCompareOp)) {
// baseQuery = new BasicQuery(
// constraints);
// }
// else {
baseQuery = new SpatialQuery(
constraints,
geometry,
extractedCompareOp);
// ExtractGeometryFilterVisitor sets predicate to NULL when CQL
// expression
// involves multiple dissimilar geometric relationships (i.e.
// "CROSSES(...) AND TOUCHES(...)")
// In which case, baseQuery is not sufficient to represent CQL
// expression.
// By setting Exact flag to false we are forcing CQLQuery to
// represent CQL expression but use
// linear constraint from baseQuery
if (extractedCompareOp == null) {
baseQuery.setExact(false);
}
// }
}
else if ((timeConstraintSet != null) && !timeConstraintSet.isEmpty()) {
// determine which time constraints are associated with an
// indexable
// field
final TemporalConstraints temporalConstraints = QueryIndexHelper.getTemporalConstraintsForDescriptors(
adapter.getTimeDescriptors(),
timeConstraintSet);
baseQuery = new TemporalQuery(
temporalConstraints);
}
}
// if baseQuery completely represents CQLQuery expression then use that
if (attrs.isEmpty() && (baseQuery != null) && baseQuery.isExact()) {
return baseQuery;
}
else {
// baseQuery is passed to CQLQuery just to extract out linear
// constraints only
return new CQLQuery(
baseQuery,
cqlFilter,
adapter);
}
}
public CQLQuery(
final Query baseQuery,
final Filter filter,
final GeotoolsFeatureDataAdapter adapter ) {
this.baseQuery = baseQuery;
cqlFilter = filter;
this.filter = new CQLQueryFilter(
filter,
adapter);
}
@Override
public List<QueryFilter> createFilters(
final CommonIndexModel indexModel ) {
List<QueryFilter> queryFilters;
// note, this assumes the CQL filter covers the baseQuery which *should*
// be a safe assumption, otherwise we need to add the
// baseQuery.createFilters to the list of query filters
queryFilters = new ArrayList<QueryFilter>();
if (filter != null) {
queryFilters = new ArrayList<QueryFilter>(
queryFilters);
queryFilters.add(filter);
}
return queryFilters;
}
@Override
public boolean isSupported(
final Index<?, ?> index ) {
if (baseQuery != null) {
return baseQuery.isSupported(index);
}
return true;
}
@Override
public List<MultiDimensionalNumericData> getIndexConstraints(
final NumericIndexStrategy indexStrategy ) {
if (baseQuery != null) {
return baseQuery.getIndexConstraints(indexStrategy);
}
return Collections.emptyList();
}
@Override
public byte[] toBinary() {
byte[] baseQueryBytes;
if (baseQuery != null) {
if (!(baseQuery instanceof DistributableQuery)) {
throw new IllegalArgumentException(
"Cannot distribute CQL query with base query of type '" + baseQuery.getClass() + "'");
}
else {
baseQueryBytes = PersistenceUtils.toBinary((DistributableQuery) baseQuery);
}
}
else {
// base query can be null, no reason to log a warning
baseQueryBytes = new byte[] {};
}
final byte[] filterBytes;
if (filter != null) {
filterBytes = filter.toBinary();
}
else {
LOGGER.warn("Filter is null");
filterBytes = new byte[] {};
}
final ByteBuffer buf = ByteBuffer.allocate(filterBytes.length + baseQueryBytes.length + 4);
buf.putInt(filterBytes.length);
buf.put(filterBytes);
buf.put(baseQueryBytes);
return buf.array();
}
@Override
public void fromBinary(
final byte[] bytes ) {
final ByteBuffer buf = ByteBuffer.wrap(bytes);
final int filterBytesLength = buf.getInt();
final int baseQueryBytesLength = bytes.length - filterBytesLength - 4;
if (filterBytesLength > 0) {
final byte[] filterBytes = new byte[filterBytesLength];
filter = new CQLQueryFilter();
filter.fromBinary(filterBytes);
}
else {
LOGGER.warn("CQL filter is empty bytes");
filter = null;
}
if (baseQueryBytesLength > 0) {
final byte[] baseQueryBytes = new byte[baseQueryBytesLength];
try {
baseQuery = PersistenceUtils.fromBinary(
baseQueryBytes,
DistributableQuery.class);
}
catch (final Exception e) {
throw new IllegalArgumentException(
e);
}
}
else {
// base query can be null, no reason to log a warning
baseQuery = null;
}
}
@Override
public List<ByteArrayRange> getSecondaryIndexConstraints(
final SecondaryIndex<?> index ) {
final PropertyFilterVisitor visitor = new PropertyFilterVisitor();
final PropertyConstraintSet constraints = (PropertyConstraintSet) cqlFilter.accept(
visitor,
null);
return constraints.getRangesFor(index);
}
@Override
public List<DistributableQueryFilter> getSecondaryQueryFilter(
final SecondaryIndex<?> index ) {
final PropertyFilterVisitor visitor = new PropertyFilterVisitor();
final PropertyConstraintSet constraints = (PropertyConstraintSet) cqlFilter.accept(
visitor,
null);
return constraints.getFiltersFor(index);
}
protected static boolean hasAtLeastSpatial(
final PrimaryIndex index ) {
if ((index == null) || (index.getIndexModel() == null) || (index.getIndexModel().getDimensions() == null)) {
return false;
}
boolean hasLatitude = false;
boolean hasLongitude = false;
for (final NumericDimensionField dimension : index.getIndexModel().getDimensions()) {
if (dimension instanceof LatitudeField) {
hasLatitude = true;
}
if (dimension instanceof LongitudeField) {
hasLongitude = true;
}
}
return hasLatitude && hasLongitude;
}
protected static boolean hasTime(
final PrimaryIndex index ) {
if ((index == null) || (index.getIndexModel() == null) || (index.getIndexModel().getDimensions() == null)) {
return false;
}
for (final NumericDimensionField dimension : index.getIndexModel().getDimensions()) {
if (dimension instanceof TimeField) {
return true;
}
}
return false;
}
}