package mil.nga.giat.geowave.core.store.query; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import mil.nga.giat.geowave.core.index.ByteArrayId; import mil.nga.giat.geowave.core.index.ByteArrayRange; import mil.nga.giat.geowave.core.index.NumericIndexStrategy; import mil.nga.giat.geowave.core.index.QueryConstraints; import mil.nga.giat.geowave.core.index.StringUtils; import mil.nga.giat.geowave.core.index.dimension.NumericDimensionDefinition; import mil.nga.giat.geowave.core.index.sfc.data.BasicNumericDataset; import mil.nga.giat.geowave.core.index.sfc.data.MultiDimensionalNumericData; import mil.nga.giat.geowave.core.index.sfc.data.NumericData; import mil.nga.giat.geowave.core.index.sfc.data.NumericRange; import mil.nga.giat.geowave.core.store.dimension.NumericDimensionField; import mil.nga.giat.geowave.core.store.filter.BasicQueryFilter; import mil.nga.giat.geowave.core.store.filter.BasicQueryFilter.BasicQueryCompareOperation; import mil.nga.giat.geowave.core.store.filter.DistributableFilterList; 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.CommonIndexValue; import mil.nga.giat.geowave.core.store.index.FilterableConstraints; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.math.DoubleMath; /** * The Basic Query class represent a hyper-cube(s) query across all dimensions * that match the Constraints passed into the constructor * * NOTE: query to an index that requires a constraint and the constraint is * missing within the query equates to an unconstrained index scan. The query * filter is still applied. * */ public class BasicQuery implements DistributableQuery { private final static double DOUBLE_TOLERANCE = 1E-12d; private final static Logger LOGGER = LoggerFactory.getLogger(BasicQuery.class); protected boolean exact = true; /** * * A set of constraints, one range per dimension * */ public static class ConstraintSet { protected Map<Class<? extends NumericDimensionDefinition>, ConstraintData> constraintsPerTypeOfDimensionDefinition; public ConstraintSet() { constraintsPerTypeOfDimensionDefinition = new HashMap<Class<? extends NumericDimensionDefinition>, ConstraintData>(); } public ConstraintSet( final Map<Class<? extends NumericDimensionDefinition>, ConstraintData> constraintsPerTypeOfDimensionDefinition ) { this.constraintsPerTypeOfDimensionDefinition = constraintsPerTypeOfDimensionDefinition; } public ConstraintSet( final Class<? extends NumericDimensionDefinition> dimDefinition, final ConstraintData constraintData ) { this(); addConstraint( dimDefinition, constraintData); } public void addConstraint( final Class<? extends NumericDimensionDefinition> dimDefinition, final ConstraintData constraintData ) { final ConstraintData myCd = constraintsPerTypeOfDimensionDefinition.get(dimDefinition); if (myCd != null) { constraintsPerTypeOfDimensionDefinition.put( dimDefinition, myCd.merge(constraintData)); } else { constraintsPerTypeOfDimensionDefinition.put( dimDefinition, constraintData); } } public ConstraintSet merge( final ConstraintSet constraintSet ) { final Map<Class<? extends NumericDimensionDefinition>, ConstraintData> newSet = new HashMap<Class<? extends NumericDimensionDefinition>, ConstraintData>(); for (final Map.Entry<Class<? extends NumericDimensionDefinition>, ConstraintData> entry : constraintSet.constraintsPerTypeOfDimensionDefinition .entrySet()) { final ConstraintData data = constraintsPerTypeOfDimensionDefinition.get(entry.getKey()); if (data == null) { newSet.put( entry.getKey(), entry.getValue()); } else { newSet.put( entry.getKey(), data.merge(entry.getValue())); } } for (final Map.Entry<Class<? extends NumericDimensionDefinition>, ConstraintData> entry : constraintsPerTypeOfDimensionDefinition .entrySet()) { final ConstraintData data = constraintSet.constraintsPerTypeOfDimensionDefinition.get(entry.getKey()); if (data == null) { newSet.put( entry.getKey(), entry.getValue()); } } return new ConstraintSet( newSet); } public boolean isEmpty() { return constraintsPerTypeOfDimensionDefinition.isEmpty(); } public boolean matches( final ConstraintSet constraints ) { if (constraints.isEmpty() != isEmpty()) { return false; } for (final Map.Entry<Class<? extends NumericDimensionDefinition>, ConstraintData> entry : constraintsPerTypeOfDimensionDefinition .entrySet()) { final ConstraintData data = constraints.constraintsPerTypeOfDimensionDefinition.get(entry.getKey()); if ((data == null) || !data.matches(entry.getValue())) { return false; } } return true; } /** * * @param constraints * @return true if all dimensions intersect */ public boolean intersects( final ConstraintSet constraints ) { if (constraints.isEmpty() != isEmpty()) { return true; } boolean intersects = true; for (final Map.Entry<Class<? extends NumericDimensionDefinition>, ConstraintData> entry : constraintsPerTypeOfDimensionDefinition .entrySet()) { final ConstraintData data = constraints.constraintsPerTypeOfDimensionDefinition.get(entry.getKey()); intersects &= ((data != null) && data.intersects(entry.getValue())); } return intersects; } /* * Makes the decision to provide a empty data set if an one dimension is * left unconstrained. */ public MultiDimensionalNumericData getIndexConstraints( final NumericIndexStrategy indexStrategy ) { if (constraintsPerTypeOfDimensionDefinition.isEmpty()) { return new BasicNumericDataset(); } final NumericDimensionDefinition[] dimensionDefinitions = indexStrategy.getOrderedDimensionDefinitions(); final NumericData[] dataPerDimension = new NumericData[dimensionDefinitions.length]; // all or nothing...for now for (int d = 0; d < dimensionDefinitions.length; d++) { final ConstraintData dimConstraint = constraintsPerTypeOfDimensionDefinition .get(dimensionDefinitions[d].getClass()); if (dimConstraint == null) { return new BasicNumericDataset(); } dataPerDimension[d] = dimConstraint.range; } return new BasicNumericDataset( dataPerDimension); } public boolean isSupported( final PrimaryIndex index ) { final NumericDimensionField<? extends CommonIndexValue>[] fields = index.getIndexModel().getDimensions(); final Set<Class<? extends NumericDimensionDefinition>> fieldTypeSet = new HashSet<Class<? extends NumericDimensionDefinition>>(); // first create a set of the field's base definition types that are // within the index model for (final NumericDimensionField<? extends CommonIndexValue> field : fields) { fieldTypeSet.add(field.getBaseDefinition().getClass()); } // then ensure each of the definition types that is required by // these // constraints are in the index model for (final Map.Entry<Class<? extends NumericDimensionDefinition>, ConstraintData> entry : constraintsPerTypeOfDimensionDefinition .entrySet()) { // ** defaults are not mandatory ** if (!fieldTypeSet.contains(entry.getKey()) && !entry.getValue().isDefault) { return false; } } return true; } protected DistributableQueryFilter createFilter( final CommonIndexModel indexModel, final BasicQuery basicQuery ) { final NumericDimensionField<?>[] dimensionFields = indexModel.getDimensions(); NumericDimensionField<?>[] orderedConstrainedDimensionFields = dimensionFields; NumericDimensionField<?>[] unconstrainedDimensionFields; NumericData[] orderedConstraintsPerDimension = new NumericData[dimensionFields.length]; // trim dimension fields to be only what is contained in the // constraints final Set<Integer> fieldsToTrim = new HashSet<Integer>(); for (int d = 0; d < dimensionFields.length; d++) { final ConstraintData nd = constraintsPerTypeOfDimensionDefinition.get(dimensionFields[d] .getBaseDefinition() .getClass()); if (nd == null) { fieldsToTrim.add(d); } else { orderedConstraintsPerDimension[d] = constraintsPerTypeOfDimensionDefinition.get(dimensionFields[d] .getBaseDefinition() .getClass()).range; } } if (!fieldsToTrim.isEmpty()) { final NumericDimensionField<?>[] newDimensionFields = new NumericDimensionField[dimensionFields.length - fieldsToTrim.size()]; unconstrainedDimensionFields = new NumericDimensionField[fieldsToTrim.size()]; final NumericData[] newOrderedConstraintsPerDimension = new NumericData[newDimensionFields.length]; int newDimensionCtr = 0; int constrainedCtr = 0; for (int i = 0; i < dimensionFields.length; i++) { if (!fieldsToTrim.contains(i)) { newDimensionFields[newDimensionCtr] = dimensionFields[i]; newOrderedConstraintsPerDimension[newDimensionCtr++] = orderedConstraintsPerDimension[i]; } else { unconstrainedDimensionFields[constrainedCtr++] = dimensionFields[i]; } } orderedConstrainedDimensionFields = newDimensionFields; orderedConstraintsPerDimension = newOrderedConstraintsPerDimension; } else { unconstrainedDimensionFields = new NumericDimensionField[] {}; } return basicQuery.createQueryFilter( new BasicNumericDataset( orderedConstraintsPerDimension), orderedConstrainedDimensionFields, unconstrainedDimensionFields); } public byte[] toBinary() { final List<byte[]> bytes = new ArrayList<byte[]>( constraintsPerTypeOfDimensionDefinition.size()); int totalBytes = 4; for (final Entry<Class<? extends NumericDimensionDefinition>, ConstraintData> c : constraintsPerTypeOfDimensionDefinition .entrySet()) { final byte[] className = StringUtils.stringToBinary(c.getKey().getName()); final double min = c.getValue().range.getMin(); final double max = c.getValue().range.getMax(); final int entryLength = className.length + 22; final short isDefault = (short) (c.getValue().isDefault ? 1 : 0); final ByteBuffer entryBuf = ByteBuffer.allocate(entryLength); entryBuf.putInt(className.length); entryBuf.put(className); entryBuf.putDouble(min); entryBuf.putDouble(max); entryBuf.putShort(isDefault); bytes.add(entryBuf.array()); totalBytes += entryLength; } final ByteBuffer buf = ByteBuffer.allocate(totalBytes); buf.putInt(bytes.size()); for (final byte[] entryBytes : bytes) { buf.put(entryBytes); } return buf.array(); } public void fromBinary( final byte[] bytes ) { final ByteBuffer buf = ByteBuffer.wrap(bytes); final int numEntries = buf.getInt(); final Map<Class<? extends NumericDimensionDefinition>, ConstraintData> constraintsPerTypeOfDimensionDefinition = new HashMap<Class<? extends NumericDimensionDefinition>, ConstraintData>( numEntries); for (int i = 0; i < numEntries; i++) { final int classNameLength = buf.getInt(); final byte[] className = new byte[classNameLength]; buf.get(className); final double min = buf.getDouble(); final double max = buf.getDouble(); final boolean isDefault = buf.getShort() > 0; final String classNameStr = StringUtils.stringFromBinary(className); try { final Class<? extends NumericDimensionDefinition> cls = (Class<? extends NumericDimensionDefinition>) Class .forName(classNameStr); constraintsPerTypeOfDimensionDefinition.put( cls, new ConstraintData( new NumericRange( min, max), isDefault)); } catch (final ClassNotFoundException e) { LOGGER.warn( "Cannot find dimension definition class: " + classNameStr, e); } } this.constraintsPerTypeOfDimensionDefinition = constraintsPerTypeOfDimensionDefinition; } } public static class ConstraintData { protected NumericData range; protected boolean isDefault; public ConstraintData( final NumericData range, final boolean isDefault ) { super(); this.range = range; this.isDefault = isDefault; } public boolean intersects( final ConstraintData cd ) { final double i1 = cd.range.getMin(); final double i2 = cd.range.getMax(); final double j1 = range.getMin(); final double j2 = range.getMax(); return ((i1 < j2) || DoubleMath.fuzzyEquals( i1, j2, DOUBLE_TOLERANCE)) && ((i2 > j1) || DoubleMath.fuzzyEquals( i2, j1, DOUBLE_TOLERANCE)); } public ConstraintData merge( final ConstraintData cd ) { if (range.equals(cd.range)) { return new ConstraintData( range, isDefault); } return new ConstraintData( new NumericRange( Math.min( cd.range.getMin(), range.getMin()), Math.max( cd.range.getMax(), range.getMax())), false); // TODO: ideally, this would be set based on some // logic } @Override public int hashCode() { final int prime = 31; int result = 1; result = (prime * result) + (isDefault ? 1231 : 1237); result = (prime * result) + ((range == null) ? 0 : range.hashCode()); return result; } @Override public boolean equals( final Object obj ) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ConstraintData other = (ConstraintData) obj; if (isDefault != other.isDefault) { return false; } if (range == null) { if (other.range != null) { return false; } } else if (!range.equals(other.range)) { return false; } return true; } /** * Ignores 'default' indicator * * @param other * @return */ public boolean matches( final ConstraintData other ) { if (this == other) { return true; } if (range == null) { if (other.range != null) { return false; } } else if (!DoubleMath.fuzzyEquals( range.getMin(), other.range.getMin(), DOUBLE_TOLERANCE) || !DoubleMath.fuzzyEquals( range.getMax(), other.range.getMax(), DOUBLE_TOLERANCE)) { return false; } return true; } } /** * * A list of Constraint Sets. Each Constraint Set is an individual * hyper-cube query. * */ public static class Constraints { // these basic queries are tied to NumericDimensionDefinition types, not // ideal, but third-parties can and will nned to implement their own // queries if they implement their own dimension definitions private final List<ConstraintSet> constraintsSets = new LinkedList<ConstraintSet>(); public Constraints() {} public Constraints( final ConstraintSet constraintSet ) { constraintsSets.add(constraintSet); } public Constraints( final List<ConstraintSet> constraintSets ) { constraintsSets.addAll(constraintSets); } public Constraints merge( final Constraints constraints ) { return merge(constraints.constraintsSets); } public Constraints merge( final List<ConstraintSet> otherConstraintSets ) { if (otherConstraintSets.isEmpty()) { return this; } else if (isEmpty()) { return new Constraints( otherConstraintSets); } final List<ConstraintSet> newSets = new LinkedList<ConstraintSet>(); for (final ConstraintSet newSet : otherConstraintSets) { add( newSets, constraintsSets, newSet); } return new Constraints( newSets); } private static void add( final List<ConstraintSet> newSets, final List<ConstraintSet> currentSets, final ConstraintSet newSet ) { for (final ConstraintSet cs : currentSets) { newSets.add(cs.merge(newSet)); } } public boolean isEmpty() { return constraintsSets.isEmpty(); } public boolean matches( final Constraints constraints ) { if (constraints.isEmpty() != isEmpty()) { return false; } for (final ConstraintSet set : constraintsSets) { boolean foundMatch = false; for (final ConstraintSet otherSet : constraints.constraintsSets) { foundMatch |= set.matches(otherSet); } if (!foundMatch) { return false; } } return true; } @Override public int hashCode() { final int prime = 31; int result = 1; result = (prime * result) + ((constraintsSets == null) ? 0 : constraintsSets.hashCode()); return result; } @Override public boolean equals( final Object obj ) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Constraints other = (Constraints) obj; if (constraintsSets == null) { if (other.constraintsSets != null) { return false; } } else if (!constraintsSets.equals(other.constraintsSets)) { return false; } return true; } public List<MultiDimensionalNumericData> getIndexConstraints( final NumericIndexStrategy indexStrategy ) { if (constraintsSets.isEmpty()) { return Collections.emptyList(); } final List<MultiDimensionalNumericData> setRanges = new ArrayList<MultiDimensionalNumericData>( constraintsSets.size()); for (final ConstraintSet set : constraintsSets) { final MultiDimensionalNumericData mdSet = set.getIndexConstraints(indexStrategy); if (!mdSet.isEmpty()) { setRanges.add(mdSet); } } return setRanges; } /** * * @param index * @return true if all constrain sets match the index * * TODO: Should we allow each constraint target each index? */ public boolean isSupported( final PrimaryIndex index ) { for (final ConstraintSet set : constraintsSets) { if (!set.isSupported(index)) { return false; } } return true; } } private Constraints constraints; // field Id to constraint private Map<ByteArrayId, FilterableConstraints> additionalConstraints = Collections.emptyMap(); BasicQueryCompareOperation compareOp = BasicQueryCompareOperation.INTERSECTS; protected BasicQuery() {} public BasicQuery( final Constraints constraints ) { this.constraints = constraints; } public BasicQuery( final Constraints constraints, final BasicQueryCompareOperation compareOp ) { this.constraints = constraints; this.compareOp = compareOp; } public BasicQuery( final Constraints constraints, final Map<ByteArrayId, FilterableConstraints> additionalConstraints ) { super(); this.constraints = constraints; this.additionalConstraints = additionalConstraints; } @Override public List<QueryFilter> createFilters( final CommonIndexModel indexModel ) { final List<DistributableQueryFilter> filters = new ArrayList<DistributableQueryFilter>(); for (final ConstraintSet constraint : constraints.constraintsSets) { final DistributableQueryFilter filter = constraint.createFilter( indexModel, this); if (filter != null) { filters.add(filter); } } if (!filters.isEmpty()) { return Collections.<QueryFilter> singletonList(filters.size() == 1 ? filters.get(0) : new DistributableFilterList( false, filters)); } return Collections.emptyList(); } protected DistributableQueryFilter createQueryFilter( final MultiDimensionalNumericData constraints, final NumericDimensionField<?>[] orderedConstrainedDimensionFields, final NumericDimensionField<?>[] unconstrainedDimensionFields ) { return new BasicQueryFilter( constraints, orderedConstrainedDimensionFields, compareOp); } @Override public boolean isSupported( final Index<?, ?> index ) { return (index instanceof PrimaryIndex) ? constraints.isSupported((PrimaryIndex) index) : secondaryIndexSupports((SecondaryIndex) index); } public boolean secondaryIndexSupports( final SecondaryIndex index ) { if (additionalConstraints.containsKey(index.getFieldId())) { return true; } return false; } @Override public List<MultiDimensionalNumericData> getIndexConstraints( final NumericIndexStrategy indexStrategy ) { return constraints.getIndexConstraints(indexStrategy); } @Override public byte[] toBinary() { final List<byte[]> bytes = new ArrayList<byte[]>( constraints.constraintsSets.size()); int totalBytes = 4; for (final ConstraintSet c : constraints.constraintsSets) { bytes.add(c.toBinary()); totalBytes += (bytes.get(bytes.size() - 1).length + 4); } // TODO; additionalConstraints final ByteBuffer buf = ByteBuffer.allocate(totalBytes); buf.putInt(bytes.size()); for (final byte[] entryBytes : bytes) { buf.putInt(entryBytes.length); buf.put(entryBytes); } return buf.array(); } @Override public void fromBinary( final byte[] bytes ) { final ByteBuffer buf = ByteBuffer.wrap(bytes); final int numEntries = buf.getInt(); final List<ConstraintSet> sets = new LinkedList<ConstraintSet>(); for (int i = 0; i < numEntries; i++) { final byte[] d = new byte[buf.getInt()]; buf.get(d); final ConstraintSet cs = new ConstraintSet(); cs.fromBinary(d); sets.add(cs); } constraints = new Constraints( sets); // TODO; additionalConstraints } @SuppressWarnings("unchecked") @Override public List<ByteArrayRange> getSecondaryIndexConstraints( final SecondaryIndex<?> index ) { final List<ByteArrayRange> allRanges = new ArrayList<>(); final List<FilterableConstraints> queryConstraints = getSecondaryIndexQueryConstraints(index); for (final QueryConstraints queryConstraint : queryConstraints) { allRanges.addAll(index.getIndexStrategy().getQueryRanges( queryConstraint)); } return allRanges; } @Override public List<DistributableQueryFilter> getSecondaryQueryFilter( final SecondaryIndex<?> index ) { final List<DistributableQueryFilter> allFilters = new ArrayList<>(); final List<FilterableConstraints> queryConstraints = getSecondaryIndexQueryConstraints(index); for (final FilterableConstraints queryConstraint : queryConstraints) { final DistributableQueryFilter filter = queryConstraint.getFilter(); if (filter != null) { allFilters.add(filter); } } return allFilters; } public List<FilterableConstraints> getSecondaryIndexQueryConstraints( final SecondaryIndex<?> index ) { final List<FilterableConstraints> constraints = new ArrayList<>(); if (additionalConstraints.get(index.getFieldId()) != null) { constraints.add(additionalConstraints.get(index.getFieldId())); } return constraints; } public boolean isExact() { return exact; } public void setExact( final boolean exact ) { this.exact = exact; } }