| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with this |
| * work for additional information regarding copyright ownership. The ASF |
| * licenses this file to You 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 |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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 org.apache.hugegraph.backend.query; |
| |
| import java.math.BigDecimal; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.hugegraph.backend.BackendException; |
| import org.apache.hugegraph.backend.id.Id; |
| import org.apache.hugegraph.backend.id.SplicingIdGenerator; |
| import org.apache.hugegraph.backend.query.Condition.Relation; |
| import org.apache.hugegraph.backend.query.Condition.RelationType; |
| import org.apache.hugegraph.perf.PerfUtil.Watched; |
| import org.apache.hugegraph.structure.HugeElement; |
| import org.apache.hugegraph.structure.HugeProperty; |
| import org.apache.hugegraph.type.HugeType; |
| import org.apache.hugegraph.type.define.HugeKeys; |
| import org.apache.hugegraph.util.CollectionUtil; |
| import org.apache.hugegraph.util.E; |
| import org.apache.hugegraph.util.InsertionOrderUtil; |
| import org.apache.hugegraph.util.LongEncoding; |
| import org.apache.hugegraph.util.NumericUtil; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Sets; |
| |
| public class ConditionQuery extends IdQuery { |
| |
| public static final char INDEX_SYM_MIN = '\u0000'; |
| public static final String INDEX_SYM_ENDING = "\u0000"; |
| public static final String INDEX_SYM_NULL = "\u0001"; |
| public static final String INDEX_SYM_EMPTY = "\u0002"; |
| public static final char INDEX_SYM_MAX = '\u0003'; |
| |
| // Note: here we use "new String" to distinguish normal string code |
| public static final String INDEX_VALUE_NULL = new String("<null>"); |
| public static final String INDEX_VALUE_EMPTY = new String("<empty>"); |
| |
| public static final Set<String> IGNORE_SYM_SET; |
| static { |
| List<String> list = new ArrayList<>(INDEX_SYM_MAX - INDEX_SYM_MIN); |
| for (char ch = INDEX_SYM_MIN; ch <= INDEX_SYM_MAX; ch++) { |
| list.add(String.valueOf(ch)); |
| } |
| IGNORE_SYM_SET = ImmutableSet.copyOf(list); |
| } |
| |
| private static final List<Condition> EMPTY_CONDITIONS = ImmutableList.of(); |
| |
| // Conditions will be contacted with `and` by default |
| private List<Condition> conditions = EMPTY_CONDITIONS; |
| |
| private OptimizedType optimizedType = OptimizedType.NONE; |
| private ResultsFilter resultsFilter = null; |
| private Element2IndexValueMap element2IndexValueMap = null; |
| |
| public ConditionQuery(HugeType resultType) { |
| super(resultType); |
| } |
| |
| public ConditionQuery(HugeType resultType, Query originQuery) { |
| super(resultType, originQuery); |
| } |
| |
| public ConditionQuery query(Condition condition) { |
| // Query by id (HugeGraph-259) |
| if (condition instanceof Relation) { |
| Relation relation = (Relation) condition; |
| if (relation.key().equals(HugeKeys.ID) && |
| relation.relation() == RelationType.EQ) { |
| E.checkArgument(relation.value() instanceof Id, |
| "Invalid id value '%s'", relation.value()); |
| super.query((Id) relation.value()); |
| return this; |
| } |
| } |
| |
| if (this.conditions == EMPTY_CONDITIONS) { |
| this.conditions = InsertionOrderUtil.newList(); |
| } |
| this.conditions.add(condition); |
| return this; |
| } |
| |
| public ConditionQuery query(List<Condition> conditions) { |
| for (Condition condition : conditions) { |
| this.query(condition); |
| } |
| return this; |
| } |
| |
| public ConditionQuery eq(HugeKeys key, Object value) { |
| // Filter value by key |
| return this.query(Condition.eq(key, value)); |
| } |
| |
| public ConditionQuery gt(HugeKeys key, Object value) { |
| return this.query(Condition.gt(key, value)); |
| } |
| |
| public ConditionQuery gte(HugeKeys key, Object value) { |
| return this.query(Condition.gte(key, value)); |
| } |
| |
| public ConditionQuery lt(HugeKeys key, Object value) { |
| return this.query(Condition.lt(key, value)); |
| } |
| |
| public ConditionQuery lte(HugeKeys key, Object value) { |
| return this.query(Condition.lte(key, value)); |
| } |
| |
| public ConditionQuery neq(HugeKeys key, Object value) { |
| return this.query(Condition.neq(key, value)); |
| } |
| |
| public ConditionQuery prefix(HugeKeys key, Id value) { |
| return this.query(Condition.prefix(key, value)); |
| } |
| |
| public ConditionQuery key(HugeKeys key, Object value) { |
| return this.query(Condition.containsKey(key, value)); |
| } |
| |
| public ConditionQuery scan(String start, String end) { |
| return this.query(Condition.scan(start, end)); |
| } |
| |
| @Override |
| public int conditionsSize() { |
| return this.conditions.size(); |
| } |
| |
| @Override |
| public Collection<Condition> conditions() { |
| return Collections.unmodifiableList(this.conditions); |
| } |
| |
| public void resetConditions(List<Condition> conditions) { |
| this.conditions = conditions; |
| } |
| |
| public void resetConditions() { |
| this.conditions = EMPTY_CONDITIONS; |
| } |
| |
| public void recordIndexValue(Id propertyId, Id id, Object indexValue) { |
| this.element2IndexValueMap().addIndexValue(propertyId, id, indexValue); |
| } |
| |
| public void selectedIndexField(Id indexField) { |
| this.element2IndexValueMap().selectedIndexField(indexField); |
| } |
| |
| public void removeElementLeftIndex(Id elementId) { |
| if (this.element2IndexValueMap == null) { |
| return; |
| } |
| this.element2IndexValueMap.removeElementLeftIndex(elementId); |
| } |
| |
| public boolean existLeftIndex(Id elementId) { |
| return this.getLeftIndexOfElement(elementId) != null; |
| } |
| |
| public Set<LeftIndex> getLeftIndexOfElement(Id elementId) { |
| if (this.element2IndexValueMap == null) { |
| return null; |
| } |
| return this.element2IndexValueMap.getLeftIndex(elementId); |
| } |
| |
| private Element2IndexValueMap element2IndexValueMap() { |
| if (this.element2IndexValueMap == null) { |
| this.element2IndexValueMap = new Element2IndexValueMap(); |
| } |
| return this.element2IndexValueMap; |
| } |
| |
| public List<Condition.Relation> relations() { |
| List<Condition.Relation> relations = new ArrayList<>(); |
| for (Condition c : this.conditions) { |
| relations.addAll(c.relations()); |
| } |
| return relations; |
| } |
| |
| public Relation relation(Id key) { |
| for (Relation r : this.relations()) { |
| if (r.key().equals(key)) { |
| return r; |
| } |
| } |
| return null; |
| } |
| |
| @Watched |
| public <T> T condition(Object key) { |
| List<Object> valuesEQ = InsertionOrderUtil.newList(); |
| List<Object> valuesIN = InsertionOrderUtil.newList(); |
| for (Condition c : this.conditions) { |
| if (c.isRelation()) { |
| Condition.Relation r = (Condition.Relation) c; |
| if (r.key().equals(key)) { |
| if (r.relation() == RelationType.EQ) { |
| valuesEQ.add(r.value()); |
| } else if (r.relation() == RelationType.IN) { |
| Object value = r.value(); |
| assert value instanceof List; |
| valuesIN.add(value); |
| } |
| } |
| } |
| } |
| if (valuesEQ.isEmpty() && valuesIN.isEmpty()) { |
| return null; |
| } |
| if (valuesEQ.size() == 1 && valuesIN.size() == 0) { |
| @SuppressWarnings("unchecked") |
| T value = (T) valuesEQ.get(0); |
| return value; |
| } |
| if (valuesEQ.size() == 0 && valuesIN.size() == 1) { |
| @SuppressWarnings("unchecked") |
| T value = (T) valuesIN.get(0); |
| return value; |
| } |
| |
| Set<Object> intersectValues = InsertionOrderUtil.newSet(); |
| for (Object value : valuesEQ) { |
| List<Object> valueAsList = ImmutableList.of(value); |
| if (intersectValues.isEmpty()) { |
| intersectValues.addAll(valueAsList); |
| } else { |
| CollectionUtil.intersectWithModify(intersectValues, |
| valueAsList); |
| } |
| } |
| for (Object value : valuesIN) { |
| @SuppressWarnings("unchecked") |
| List<Object> valueAsList = (List<Object>) value; |
| if (intersectValues.isEmpty()) { |
| intersectValues.addAll(valueAsList); |
| } else { |
| CollectionUtil.intersectWithModify(intersectValues, |
| valueAsList); |
| } |
| } |
| |
| if (intersectValues.size() == 0) { |
| return null; |
| } |
| E.checkState(intersectValues.size() == 1, |
| "Illegal key '%s' with more than one value: %s", |
| key, intersectValues); |
| @SuppressWarnings("unchecked") |
| T value = (T) intersectValues.iterator().next(); |
| return value; |
| } |
| |
| public void unsetCondition(Object key) { |
| this.conditions.removeIf(c -> c.isRelation() && ((Relation) c).key().equals(key)); |
| } |
| |
| public boolean containsCondition(HugeKeys key) { |
| for (Condition c : this.conditions) { |
| if (c.isRelation()) { |
| Condition.Relation r = (Condition.Relation) c; |
| if (r.key().equals(key)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| public boolean containsRelation(HugeKeys key, Condition.RelationType type) { |
| for (Relation r : this.relations()) { |
| if (r.key().equals(key) && r.relation().equals(type)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean containsRelation(Condition.RelationType type) { |
| for (Relation r : this.relations()) { |
| if (r.relation().equals(type)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean containsScanRelation() { |
| return this.containsRelation(Condition.RelationType.SCAN); |
| } |
| |
| public boolean containsContainsCondition(Id key) { |
| for (Relation r : this.relations()) { |
| if (r.key().equals(key)) { |
| return r.relation().equals(RelationType.CONTAINS) || |
| r.relation().equals(RelationType.TEXT_CONTAINS); |
| } |
| } |
| return false; |
| } |
| |
| public boolean allSysprop() { |
| for (Condition c : this.conditions) { |
| if (!c.isSysprop()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public boolean allRelation() { |
| for (Condition c : this.conditions) { |
| if (!c.isRelation()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public List<Condition> syspropConditions() { |
| this.checkFlattened(); |
| List<Condition> conds = new ArrayList<>(); |
| for (Condition c : this.conditions) { |
| if (c.isSysprop()) { |
| conds.add(c); |
| } |
| } |
| return conds; |
| } |
| |
| public List<Condition> syspropConditions(HugeKeys key) { |
| this.checkFlattened(); |
| List<Condition> conditions = new ArrayList<>(); |
| for (Condition condition : this.conditions) { |
| Relation relation = (Relation) condition; |
| if (relation.key().equals(key)) { |
| conditions.add(relation); |
| } |
| } |
| return conditions; |
| } |
| |
| public List<Condition> userpropConditions() { |
| this.checkFlattened(); |
| List<Condition> conds = new ArrayList<>(); |
| for (Condition c : this.conditions) { |
| if (!c.isSysprop()) { |
| conds.add(c); |
| } |
| } |
| return conds; |
| } |
| |
| public List<Condition> userpropConditions(Id key) { |
| this.checkFlattened(); |
| List<Condition> conditions = new ArrayList<>(); |
| for (Condition condition : this.conditions) { |
| Relation relation = (Relation) condition; |
| if (relation.key().equals(key)) { |
| conditions.add(relation); |
| } |
| } |
| return conditions; |
| } |
| |
| public List<Relation> userpropRelations() { |
| List<Relation> relations = new ArrayList<>(); |
| for (Relation r : this.relations()) { |
| if (!r.isSysprop()) { |
| relations.add(r); |
| } |
| } |
| return relations; |
| } |
| |
| public void resetUserpropConditions() { |
| this.conditions.removeIf(condition -> !condition.isSysprop()); |
| } |
| |
| public Set<Id> userpropKeys() { |
| Set<Id> keys = new LinkedHashSet<>(); |
| for (Relation r : this.relations()) { |
| if (!r.isSysprop()) { |
| Condition.UserpropRelation ur = (Condition.UserpropRelation) r; |
| keys.add(ur.key()); |
| } |
| } |
| return keys; |
| } |
| |
| /** |
| * This method is only used for secondary index scenario, |
| * its relation must be EQ |
| * @param fields the user property fields |
| * @return the corresponding user property serial values of fields |
| */ |
| public String userpropValuesString(List<Id> fields) { |
| List<Object> values = new ArrayList<>(fields.size()); |
| for (Id field : fields) { |
| boolean got = false; |
| for (Relation r : this.userpropRelations()) { |
| if (r.key().equals(field) && !r.isSysprop()) { |
| E.checkState(r.relation == RelationType.EQ || |
| r.relation == RelationType.CONTAINS, |
| "Method userpropValues(List<String>) only " + |
| "used for secondary index, " + |
| "relation must be EQ or CONTAINS, but got %s", |
| r.relation()); |
| values.add(r.serialValue()); |
| got = true; |
| } |
| } |
| if (!got) { |
| throw new BackendException( |
| "No such userprop named '%s' in the query '%s'", |
| field, this); |
| } |
| } |
| return concatValues(values); |
| } |
| |
| public Set<Object> userpropValues(Id field) { |
| Set<Object> values = new HashSet<>(); |
| for (Relation r : this.userpropRelations()) { |
| if (r.key().equals(field)) { |
| values.add(r.serialValue()); |
| } |
| } |
| return values; |
| } |
| |
| public Object userpropValue(Id field) { |
| Set<Object> values = this.userpropValues(field); |
| if (values.isEmpty()) { |
| return null; |
| } |
| E.checkState(values.size() == 1, |
| "Expect one user-property value of field '%s', " + |
| "but got '%s'", field, values.size()); |
| return values.iterator().next(); |
| } |
| |
| public boolean hasRangeCondition() { |
| // NOTE: we need to judge all the conditions, including the nested |
| for (Condition.Relation r : this.relations()) { |
| if (r.relation().isRangeType()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean hasSearchCondition() { |
| // NOTE: we need to judge all the conditions, including the nested |
| for (Condition.Relation r : this.relations()) { |
| if (r.relation().isSearchType()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean hasSecondaryCondition() { |
| // NOTE: we need to judge all the conditions, including the nested |
| for (Condition.Relation r : this.relations()) { |
| if (r.relation().isSecondaryType()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean hasNeqCondition() { |
| // NOTE: we need to judge all the conditions, including the nested |
| for (Condition.Relation r : this.relations()) { |
| if (r.relation() == RelationType.NEQ) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean matchUserpropKeys(List<Id> keys) { |
| Set<Id> conditionKeys = this.userpropKeys(); |
| return keys.size() > 0 && conditionKeys.containsAll(keys); |
| } |
| |
| @Override |
| public ConditionQuery copy() { |
| ConditionQuery query = (ConditionQuery) super.copy(); |
| query.originQuery(this); |
| if (query.conditions != EMPTY_CONDITIONS) { |
| query.conditions = InsertionOrderUtil.newList(this.conditions); |
| } |
| query.optimizedType = OptimizedType.NONE; |
| query.resultsFilter = null; |
| |
| return query; |
| } |
| |
| public ConditionQuery copyAndResetUnshared() { |
| ConditionQuery query = this.copy(); |
| // These fields should not be shared by multiple sub-query |
| query.optimizedType = OptimizedType.NONE; |
| query.resultsFilter = null; |
| return query; |
| } |
| |
| @Override |
| public boolean test(HugeElement element) { |
| if (!this.ids().isEmpty() && !super.test(element)) { |
| return false; |
| } |
| |
| /* |
| * Currently results-filter is used to filter unmatched results returned |
| * by search index, and there may be multiple results-filter for every |
| * sub-query like within() + Text.contains(). |
| * We can't use sub-query results-filter here for fresh element which is |
| * not committed to backend store, because it's not from a sub-query. |
| */ |
| if (this.resultsFilter != null && !element.fresh()) { |
| return this.resultsFilter.test(element); |
| } |
| |
| /* |
| * NOTE: seems need to keep call checkRangeIndex() for each condition, |
| * so don't break early even if test() return false. |
| */ |
| boolean valid = true; |
| for (Condition cond : this.conditions) { |
| valid &= cond.test(element); |
| valid &= this.element2IndexValueMap == null || |
| this.element2IndexValueMap.checkRangeIndex(element, cond); |
| } |
| return valid; |
| } |
| |
| public void checkFlattened() { |
| E.checkState(this.isFlattened(), |
| "Query has none-flatten condition: %s", this); |
| } |
| |
| public boolean isFlattened() { |
| for (Condition condition : this.conditions) { |
| if (!condition.isFlattened()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public boolean mayHasDupKeys(Set<HugeKeys> keys) { |
| Map<HugeKeys, Integer> keyCounts = new HashMap<>(); |
| for (Condition condition : this.conditions) { |
| if (!condition.isRelation()) { |
| // Assume may exist duplicate keys when has nested conditions |
| return true; |
| } |
| Relation relation = (Relation) condition; |
| if (keys.contains(relation.key())) { |
| int keyCount = keyCounts.getOrDefault(relation.key(), 0); |
| if (++keyCount > 1) { |
| return true; |
| } |
| keyCounts.put((HugeKeys) relation.key(), keyCount); |
| } |
| } |
| return false; |
| } |
| |
| public void optimized(OptimizedType optimizedType) { |
| assert this.optimizedType.ordinal() <= optimizedType.ordinal() : |
| this.optimizedType + " !<= " + optimizedType; |
| this.optimizedType = optimizedType; |
| |
| Query originQuery = this.originQuery(); |
| if (originQuery instanceof ConditionQuery) { |
| ConditionQuery cq = ((ConditionQuery) originQuery); |
| /* |
| * Two sub-query(flatten) will both set optimized of originQuery, |
| * here we just keep the higher one, this may not be a perfect way |
| */ |
| if (optimizedType.ordinal() > cq.optimized().ordinal()) { |
| cq.optimized(optimizedType); |
| } |
| } |
| } |
| |
| public OptimizedType optimized() { |
| return this.optimizedType; |
| } |
| |
| public void registerResultsFilter(ResultsFilter filter) { |
| assert this.resultsFilter == null; |
| this.resultsFilter = filter; |
| } |
| |
| public void updateResultsFilter() { |
| Query originQuery = this.originQuery(); |
| if (originQuery instanceof ConditionQuery) { |
| ConditionQuery originCQ = ((ConditionQuery) originQuery); |
| if (this.resultsFilter != null) { |
| originCQ.updateResultsFilter(this.resultsFilter); |
| } else { |
| originCQ.updateResultsFilter(); |
| } |
| } |
| } |
| |
| protected void updateResultsFilter(ResultsFilter filter) { |
| this.resultsFilter = filter; |
| Query originQuery = this.originQuery(); |
| if (originQuery instanceof ConditionQuery) { |
| ConditionQuery originCQ = ((ConditionQuery) originQuery); |
| originCQ.updateResultsFilter(filter); |
| } |
| } |
| |
| public ConditionQuery originConditionQuery() { |
| Query originQuery = this.originQuery(); |
| if (!(originQuery instanceof ConditionQuery)) { |
| return null; |
| } |
| |
| while (originQuery.originQuery() instanceof ConditionQuery) { |
| originQuery = originQuery.originQuery(); |
| } |
| return (ConditionQuery) originQuery; |
| } |
| |
| public static String concatValues(List<?> values) { |
| assert !values.isEmpty(); |
| List<Object> newValues = new ArrayList<>(values.size()); |
| for (Object v : values) { |
| newValues.add(concatValues(v)); |
| } |
| return SplicingIdGenerator.concatValues(newValues); |
| } |
| |
| public static String concatValues(Object value) { |
| if (value instanceof String) { |
| return escapeSpecialValueIfNeeded((String) value); |
| } if (value instanceof List) { |
| return concatValues((List<?>) value); |
| } else if (needConvertNumber(value)) { |
| return LongEncoding.encodeNumber(value); |
| } else { |
| return escapeSpecialValueIfNeeded(value.toString()); |
| } |
| } |
| |
| private static boolean needConvertNumber(Object value) { |
| // Numeric or date values should be converted to number from string |
| return NumericUtil.isNumber(value) || value instanceof Date; |
| } |
| |
| private static String escapeSpecialValueIfNeeded(String value) { |
| if (value.isEmpty()) { |
| // Escape empty String to INDEX_SYM_EMPTY (char `\u0002`) |
| value = INDEX_SYM_EMPTY; |
| } else if (value == INDEX_VALUE_EMPTY) { |
| value = ""; |
| } else if (value == INDEX_VALUE_NULL) { |
| value = INDEX_SYM_NULL; |
| } else { |
| char ch = value.charAt(0); |
| if (ch <= INDEX_SYM_MAX) { |
| /* |
| * Special symbols can't be used due to impossible to parse, |
| * and treat it as illegal value for the origin text property. |
| * TODO: escape special symbols |
| */ |
| E.checkArgument(false, |
| "Illegal leading char '\\u%s' " + |
| "in index property: '%s'", |
| (int) ch, value); |
| } |
| } |
| return value; |
| } |
| |
| public enum OptimizedType { |
| NONE, |
| PRIMARY_KEY, |
| SORT_KEYS, |
| INDEX, |
| INDEX_FILTER |
| } |
| |
| public static final class Element2IndexValueMap { |
| |
| private final Map<Id, Set<LeftIndex>> leftIndexMap; |
| private final Map<Id, Map<Id, Set<Object>>> filed2IndexValues; |
| private Id selectedIndexField; |
| |
| public Element2IndexValueMap() { |
| this.filed2IndexValues = new HashMap<>(); |
| this.leftIndexMap = new HashMap<>(); |
| } |
| |
| public void addIndexValue(Id indexField, Id elementId, |
| Object indexValue) { |
| if (!this.filed2IndexValues.containsKey(indexField)) { |
| this.filed2IndexValues.putIfAbsent(indexField, new HashMap<>()); |
| } |
| Map<Id, Set<Object>> element2IndexValueMap = |
| this.filed2IndexValues.get(indexField); |
| if (element2IndexValueMap.containsKey(elementId)) { |
| element2IndexValueMap.get(elementId).add(indexValue); |
| } else { |
| element2IndexValueMap.put(elementId, |
| Sets.newHashSet(indexValue)); |
| } |
| } |
| |
| public void selectedIndexField(Id indexField) { |
| this.selectedIndexField = indexField; |
| } |
| |
| public Set<Object> toRemoveIndexValues(Id indexField, Id elementId) { |
| if (!this.filed2IndexValues.containsKey(indexField)) { |
| return null; |
| } |
| return this.filed2IndexValues.get(indexField).get(elementId); |
| } |
| |
| public void addLeftIndex(Id elementId, Id indexField, |
| Set<Object> indexValues) { |
| LeftIndex leftIndex = new LeftIndex(indexValues, indexField); |
| if (this.leftIndexMap.containsKey(elementId)) { |
| this.leftIndexMap.get(elementId).add(leftIndex); |
| } else { |
| this.leftIndexMap.put(elementId, Sets.newHashSet(leftIndex)); |
| } |
| } |
| |
| public Set<LeftIndex> getLeftIndex(Id elementId) { |
| return this.leftIndexMap.get(elementId); |
| } |
| |
| public void removeElementLeftIndex(Id elementId) { |
| this.leftIndexMap.remove(elementId); |
| } |
| |
| public boolean checkRangeIndex(HugeElement element, Condition cond) { |
| // Not UserpropRelation |
| if (!(cond instanceof Condition.UserpropRelation)) { |
| return true; |
| } |
| |
| Condition.UserpropRelation propRelation = |
| (Condition.UserpropRelation) cond; |
| Id propId = propRelation.key(); |
| Set<Object> fieldValues = this.toRemoveIndexValues(propId, |
| element.id()); |
| if (fieldValues == null) { |
| // Not range index |
| return true; |
| } |
| |
| HugeProperty<Object> property = element.getProperty(propId); |
| if (property == null) { |
| // Property value has been deleted, so it's not matched |
| this.addLeftIndex(element.id(), propId, fieldValues); |
| return false; |
| } |
| |
| /* |
| * NOTE: If removing successfully means there is correct index, |
| * else we should add left-index values to left index map to |
| * wait the left-index to be removed. |
| */ |
| boolean hasRightValue = removeFieldValue(fieldValues, |
| property.value()); |
| if (fieldValues.size() > 0) { |
| this.addLeftIndex(element.id(), propId, fieldValues); |
| } |
| |
| /* |
| * NOTE: When query by more than one range index field, |
| * if current field is not the selected one, it can only be used to |
| * determine whether the index values matched, can't determine |
| * the element is valid or not. |
| */ |
| if (this.selectedIndexField != null) { |
| return !propId.equals(this.selectedIndexField) || hasRightValue; |
| } |
| |
| return hasRightValue; |
| } |
| |
| private static boolean removeFieldValue(Set<Object> values, |
| Object value) { |
| for (Object elem : values) { |
| if (numberEquals(elem, value)) { |
| values.remove(elem); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean numberEquals(Object number1, Object number2) { |
| // Same class compare directly |
| if (number1.getClass().equals(number2.getClass())) { |
| return number1.equals(number2); |
| } |
| |
| // Otherwise convert to BigDecimal to make two numbers comparable |
| Number n1 = NumericUtil.convertToNumber(number1); |
| Number n2 = NumericUtil.convertToNumber(number2); |
| BigDecimal b1 = BigDecimal.valueOf(n1.doubleValue()); |
| BigDecimal b2 = BigDecimal.valueOf(n2.doubleValue()); |
| return b1.compareTo(b2) == 0; |
| } |
| } |
| |
| public static final class LeftIndex { |
| |
| private final Set<Object> indexFieldValues; |
| private final Id indexField; |
| |
| public LeftIndex(Set<Object> indexFieldValues, Id indexField) { |
| this.indexFieldValues = indexFieldValues; |
| this.indexField = indexField; |
| } |
| |
| public Set<Object> indexFieldValues() { |
| return this.indexFieldValues; |
| } |
| |
| public Id indexField() { |
| return this.indexField; |
| } |
| } |
| |
| public interface ResultsFilter { |
| |
| boolean test(HugeElement element); |
| } |
| } |