package org.jboss.capedwarf.datastore.query;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.google.appengine.api.datastore.DatastoreNeedIndexException;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Projection;
import com.google.appengine.api.datastore.Query;
import com.google.common.collect.Sets;
import org.jboss.capedwarf.common.app.Application;
import org.jboss.capedwarf.datastore.KindUtils;
import org.jboss.capedwarf.shared.compatibility.Compatibility;
import org.jboss.capedwarf.shared.config.ApplicationConfiguration;
import org.jboss.capedwarf.shared.config.IndexesXml;
import org.jboss.capedwarf.shared.reflection.ReflectionUtils;
/**
* @author <a href="mailto:mluksa@redhat.com">Marko Luksa</a>
* @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
*/
public class Indexes {
private static final Set<String> KEY_RESERVED_PROPERTY_AS_SET = Collections.singleton(Entity.KEY_RESERVED_PROPERTY);
public static IndexesXml.Index getIndex(Query query) {
for (IndexesXml.Index index : ApplicationConfiguration.getInstance().getIndexesXml().getIndexes().values()) {
if (indexMatches(index, query)) {
return index;
}
}
return null;
}
public static void checkIfExplicitlyDefinedIndexIsRequired(Query query) {
if (needsExplicitlyDefinedIndex(query)) {
throw newDatastoreNeedIndexException("No matching index found", createIndexXml(query));
}
}
private static boolean needsExplicitlyDefinedIndex(Query query) {
if (Application.isDevelopmentEnv()) {
if (ApplicationConfiguration.getInstance().getIndexesXml().isAutoGenerate()) {
return false;
}
} else {
if (Compatibility.getInstance().isEnabled(Compatibility.Feature.DISABLE_INDEX_CHECK_IN_PRODUCTION)) {
return false;
}
}
if (isStatsQuery(query)) {
return false;
}
if (query.getSortPredicates().size() > 1) {
return true;
}
Set<String> sortOrders = new HashSet<>();
for (Query.SortPredicate sort : query.getSortPredicates()) {
if (isDescendingKeySort(sort)) {
return true;
}
sortOrders.add(sort.getPropertyName());
}
Set<String> inequalityFilters = new HashSet<>();
Set<String> equalityFilters = new HashSet<>();
for (Query.FilterPredicate predicate : getFilterPredicates(query)) {
Set<String> set = isEqualityFilter(predicate) ? equalityFilters : inequalityFilters;
set.add(predicate.getPropertyName());
}
Set<String> nonKeySortOrders = Sets.difference(sortOrders, KEY_RESERVED_PROPERTY_AS_SET);
Set<String> otherNonKeySortOrders = Sets.difference(nonKeySortOrders, equalityFilters);
if (has(equalityFilters) && has(otherNonKeySortOrders)) {
return true;
}
Set<String> nonKeyEqualityFilters = Sets.difference(equalityFilters, KEY_RESERVED_PROPERTY_AS_SET);
Set<String> nonKeyInequalityFilters = Sets.difference(inequalityFilters, KEY_RESERVED_PROPERTY_AS_SET);
if (has(nonKeyEqualityFilters) && has(nonKeyInequalityFilters)) {
return true;
}
if (isAncestorQuery(query)) {
return has(nonKeyInequalityFilters) || has(nonKeySortOrders);
} else {
Set<String> otherEqualityFilters = Sets.difference(equalityFilters, nonKeyInequalityFilters);
return has(nonKeyInequalityFilters) && has(otherEqualityFilters);
}
}
private static boolean isStatsQuery(Query query) {
return KindUtils.match(query.getKind(), KindUtils.Type.STATS);
}
private static boolean has(Set<String> set) {
return !set.isEmpty();
}
private static boolean isDescendingKeySort(Query.SortPredicate sort) {
return sort.getPropertyName().equals(Entity.KEY_RESERVED_PROPERTY)
&& sort.getDirection() == Query.SortDirection.DESCENDING;
}
private static boolean isEqualityFilter(Query.FilterPredicate filter) {
return filter.getOperator() == Query.FilterOperator.EQUAL
|| filter.getOperator() == Query.FilterOperator.IN;
}
private static boolean isAncestorQuery(Query query) {
return query.getAncestor() != null;
}
private static DatastoreNeedIndexException newDatastoreNeedIndexException(String message, String missingIndexDefinitionXml) {
DatastoreNeedIndexException ex = new DatastoreNeedIndexException(message);
ReflectionUtils.invokeInstanceMethod(ex, "setMissingIndexDefinitionXml", String.class, missingIndexDefinitionXml);
return ex;
}
private static String createIndexXml(Query query) {
StringBuilder sb = new StringBuilder();
sb.append("<datastore-index kind=\"").append(query.getKind()).append("\" ancestor=\"").append(query.getAncestor() != null).append("\" source=\"manual\">\n");
for (String property : getIndexProperties(query)) {
sb.append(" <property name=\"").append(property).append("\" direction=\"asc\"/>\n"); // TODO: proper direction (when necessary)
}
sb.append("</datastore-index>\n");
return sb.toString();
}
private static List<String> getIndexProperties(Query query) {
Set<String> filterProperties = getFilterProperties(query);
Set<String> sortProperties = getSortProperties(query);
Set<String> projectionProperties = getProjectionProperties(query);
removeDuplicates(filterProperties, sortProperties, projectionProperties);
List<String> properties = new ArrayList<>();
properties.addAll(filterProperties);
properties.addAll(sortProperties);
properties.addAll(projectionProperties);
return properties;
}
private static boolean indexMatches(IndexesXml.Index index, Query query) {
if (!index.getKind().equals(query.getKind())) {
return false;
}
Set<String> filterProperties = getFilterProperties(query);
Set<String> sortProperties = getSortProperties(query);
Set<String> projectionProperties = getProjectionProperties(query);
removeDuplicates(filterProperties, sortProperties, projectionProperties);
List<String> indexProperties = index.getPropertyNames();
while (!indexProperties.isEmpty()) {
String property = indexProperties.get(0);
if (!filterProperties.isEmpty()) {
if (filterProperties.remove(property)) {
indexProperties.remove(0);
} else {
return false;
}
} else if (!sortProperties.isEmpty()) {
if (sortProperties.remove(property)) {
indexProperties.remove(0);
} else {
return false;
}
} else if (!projectionProperties.isEmpty()) {
if (projectionProperties.remove(property)) {
indexProperties.remove(0);
} else {
return false;
}
} else {
return false;
}
}
return indexProperties.isEmpty() && filterProperties.isEmpty() && sortProperties.isEmpty() && projectionProperties.isEmpty();
}
private static void removeDuplicates(Set<String> filterProperties, Set<String> sortProperties, Set<String> projectionProperties) {
sortProperties.removeAll(filterProperties);
projectionProperties.removeAll(filterProperties);
projectionProperties.removeAll(sortProperties);
}
private static Set<String> getFilterProperties(Query query) {
Set<String> set = new HashSet<>();
for (Query.FilterPredicate predicate : getFilterPredicates(query)) {
set.add(predicate.getPropertyName());
}
return set;
}
@SuppressWarnings("deprecation")
private static Set<Query.FilterPredicate> getFilterPredicates(Query query) {
Set<Query.FilterPredicate> set = new HashSet<>();
addFilterPredicatesToSet(set, query.getFilter());
for (Query.FilterPredicate predicate : query.getFilterPredicates()) {
addFilterPredicatesToSet(set, predicate);
}
return set;
}
private static void addFilterPredicatesToSet(Set<Query.FilterPredicate> set, Query.Filter filter) {
if (filter == null) {
return;
}
if (filter instanceof Query.FilterPredicate) {
Query.FilterPredicate predicate = (Query.FilterPredicate) filter;
set.add(predicate);
} else if (filter instanceof Query.CompositeFilter) {
Query.CompositeFilter composite = (Query.CompositeFilter) filter;
for (Query.Filter subFilter : composite.getSubFilters()) {
addFilterPredicatesToSet(set, subFilter);
}
} else {
throw new IllegalArgumentException("Unsupported filter type " + filter);
}
}
private static Set<String> getSortProperties(Query query) {
Set<String> set = new HashSet<>();
for (Query.SortPredicate sortPredicate : query.getSortPredicates()) {
set.add(sortPredicate.getPropertyName());
}
return set;
}
private static Set<String> getProjectionProperties(Query query) {
Set<String> set = new HashSet<>();
for (Projection projection : query.getProjections()) {
set.add(Projections.getPropertyName(projection));
}
return set;
}
}