All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.datastore.BaseEntityComparator Maven / Gradle / Ivy

Go to download

API for Google App Engine standard environment with some of the dependencies shaded (repackaged)

There is a newer version: 2.0.27
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Licensed 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
 *
 *     https://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 com.google.appengine.api.datastore;

import com.google.apphosting.datastore.DatastoreV3Pb;
import com.google.apphosting.datastore.DatastoreV3Pb.Query.Filter;
import com.google.apphosting.datastore.DatastoreV3Pb.Query.Order;
import com.google.apphosting.datastore.DatastoreV3Pb.Query.Order.Direction;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.Nullable;

/** Base class for Entity comparators. */
abstract class BaseEntityComparator implements Comparator {

  static final Comparator> MULTI_TYPE_COMPARATOR = new MultiTypeComparator();

  static final Comparator ORDER_PROPERTY_COMPARATOR =
      new Comparator() {
        @Override
        public int compare(Order o1, Order o2) {
          return o1.getProperty().compareTo(o2.getProperty());
        }
      };

  // default sort order is by Key in ascending order
  static final Order KEY_ASC_ORDER =
      new Order().setProperty(Entity.KEY_RESERVED_PROPERTY).setDirection(Order.Direction.ASCENDING);

  final List orders;
  final Map filters;

  BaseEntityComparator(List orders, List filters) {
    this.orders = makeAdjustedOrders(orders, filters);
    this.filters = makeFilterMatchers(orders, filters);
  }

  /**
   * Get a {@link List} with comparable representations of the Entity's values with the given
   * property name.
   *
   * @return A {@link List} of comparable property values, or {@code null} if the entity has no
   *     property with the given name.
   */
  abstract @Nullable List> getComparablePropertyValues(
      EntityT entity, String property);

  private static List makeAdjustedOrders(List orders, List filters) {
    // Making arbitrary guess about order implied by exists filters.
    List existsOrders = Lists.newArrayList();
    for (Filter filter : filters) {
      if (filter.getOpEnum() == Filter.Operator.EXISTS) {
        existsOrders.add(
            new Order()
                .setProperty(filter.getProperty(0).getName())
                .setDirection(Direction.ASCENDING));
      }
    }
    Collections.sort(existsOrders, ORDER_PROPERTY_COMPARATOR);

    List adjusted = new ArrayList(orders.size() + existsOrders.size() + 1);
    adjusted.addAll(orders);

    if (adjusted.isEmpty()) {
      // Check for a inequality filter to order by
      for (Filter filter : filters) {
        if (ValidatedQuery.INEQUALITY_OPERATORS.contains(filter.getOpEnum())) {
          // Only the first inequality is applied natively
          adjusted.add(
              new Order()
                  .setProperty(filter.getProperty(0).getName())
                  .setDirection(Direction.ASCENDING));
          break;
        }
      }
    }

    adjusted.addAll(existsOrders);

    if (adjusted.isEmpty() || !adjusted.get(adjusted.size() - 1).equals(KEY_ASC_ORDER)) {
      // make sure we're always sorted by the key as the final sort order.
      adjusted.add(KEY_ASC_ORDER);
    }
    return adjusted;
  }

  private static Map makeFilterMatchers(
      List orders, List filters) {
    Map filterMatchers = new HashMap();
    for (Filter filter : filters) {
      String name = filter.getProperty(0).getName();
      FilterMatcher filterMatcher = filterMatchers.get(name);
      if (filterMatcher == null) {
        filterMatcher = new FilterMatcher();
        filterMatchers.put(name, filterMatcher);
      }
      filterMatcher.addFilter(filter);
    }

    // order implies existence filter
    for (Order order : orders) {
      if (!filterMatchers.containsKey(order.getProperty())) {
        filterMatchers.put(order.getProperty(), FilterMatcher.MATCH_ALL);
      }
      if (order.getProperty().equals(KEY_ASC_ORDER.getProperty())) {
        // sort orders after a key sort are ignored
        break;
      }
    }

    return filterMatchers;
  }

  @Override
  public int compare(EntityT entityA, EntityT entityB) {
    int result;

    // Walk the list of sort properties
    // The first one that doesn't match determines our result
    for (Order order : orders) {
      String property = order.getProperty();

      Collection> aValues = getComparablePropertyValues(entityA, property);
      Collection> bValues = getComparablePropertyValues(entityB, property);

      // If one of the values is null, then this is a cursor that has been truncated for group by
      // properties.
      if (aValues == null || bValues == null) {
        return 0;
      }
      // No guarantee that these collections all contain values of the same
      // type so we have to use our own method to find the extreme.  If we're
      // sorting in ascending order we want to compare the minimum values.
      // If we're sorting in descending order we want to compare the maximum
      // values.
      boolean findMin = order.getDirectionEnum() == DatastoreV3Pb.Query.Order.Direction.ASCENDING;
      FilterMatcher matcher = filters.get(property);
      if (matcher == null) {
        matcher = FilterMatcher.MATCH_ALL;
      }
      Comparable extremeA = multiTypeExtreme(aValues, findMin, matcher);
      Comparable extremeB = multiTypeExtreme(bValues, findMin, matcher);

      // No guarantee than extremeA and extremeB are of the same type so use
      // our multi-type aware comparison method.
      result = MULTI_TYPE_COMPARATOR.compare(extremeA, extremeB);

      if (result != 0) {
        if (order.getDirectionEnum() == DatastoreV3Pb.Query.Order.Direction.DESCENDING) {
          result = -result;
        }
        return result;
      }
    }

    // All sort properties match, so these are equal
    return 0;
  }

  /**
   * Find either the smallest or largest element in a potentially heterogenous collection, depending
   * on the value of {@code findMin}.
   */
  static Comparable multiTypeExtreme(
      Collection> comparables, boolean findMin, FilterMatcher matcher) {
    boolean findMax = !findMin;
    Comparable extreme = FilterMatcher.NoValue.INSTANCE;
    for (Comparable value : comparables) {
      if (!matcher.considerValueForOrder(value)) {
        continue;
      }

      if (extreme == FilterMatcher.NoValue.INSTANCE) {
        extreme = value;
      } else if (findMin && MULTI_TYPE_COMPARATOR.compare(value, extreme) < 0) {
        extreme = value;
      } else if (findMax && MULTI_TYPE_COMPARATOR.compare(value, extreme) > 0) {
        extreme = value;
      }
    }
    if (extreme == FilterMatcher.NoValue.INSTANCE) {
      // Comparator.compare was given an entity that has no values that can be compared
      throw new IllegalArgumentException("Entity contains no relevant values.");
    }
    return extreme;
  }

  private static final class MultiTypeComparator implements Comparator> {
    @Override
    public int compare(Comparable o1, Comparable o2) {
      if (o1 == null) {
        if (o2 == null) {
          return 0;
        }
        // a null is always smaller than a non-null
        return -1;
      } else if (o2 == null) {
        // a non-null is always larger than a null
        return 1;
      }
      int comp1TypeRank = DataTypeTranslator.getTypeRank(o1.getClass());
      int comp2TypeRank = DataTypeTranslator.getTypeRank(o2.getClass());
      if (comp1TypeRank == comp2TypeRank) {
        // equal type ranks should guarantee a successful comparison
        return o1.compareTo(o2);
      }
      return Integer.compare(comp1TypeRank, comp2TypeRank);
    }
  }
}