public static String addKeywordIfNeeded(String name, List<EdmAnnotation> annotations) {
    boolean isAnalyzed = false;
    Optional<EdmAnnotation> analyzedAnnotation = annotations.stream()
            .filter(annotation -> annotation.getTerm().getName()
                    .equals(AnnotationProvider.ANALYZED_TERM_NAME))
            .findFirst();
    if (analyzedAnnotation.isPresent()) {
        isAnalyzed = (Boolean) (analyzedAnnotation.get().getExpression().asConstant()
                .asPrimitive());
    }
    return isAnalyzed ? addKeyword(name) : name;
}
 public static List<Aggregate> getAggregations(ApplyOption applyOption) {
    return getItems(applyOption, e -> e.getKind() == ApplyItem.Kind.AGGREGATE, Aggregate.class);
}
 public static List<GroupBy> getGroupByItems(ApplyOption applyOption) {
    return getItems(applyOption, e -> e.getKind() == ApplyItem.Kind.GROUP_BY, GroupBy.class);
}
 public static <T> T throwNotImplemented(String msg) throws ODataApplicationException {
    throw new ODataApplicationException(msg, HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(),
            Locale.ROOT);
}
 public class ElasticEdmEntitySet extends EdmEntitySetImpl {

    private ElasticCsdlEntitySet csdlEntitySet;
    private ElasticEdmProvider provider;

    /**
     * Initialize fields.
     * 
     * @param provider
     *            the EDM provider
     * @param container
     *            the EDM entity container
     * @param entitySet
     *            the EDM entity set
     */
    public ElasticEdmEntitySet(ElasticEdmProvider provider, EdmEntityContainer container,
            ElasticCsdlEntitySet entitySet) {
        super(provider, container, entitySet);
        this.provider = provider;
        this.csdlEntitySet = entitySet;
    }

    /**
     * Get's index name in Elasticsearch.
     * 
     * @return index name
     */
    public String getESIndex() {
        return csdlEntitySet.getESIndex();
    }

    /**
     * Get's type name in Elasticsearch.
     * 
     * @return type name
     */
    public String getESType() {
        return csdlEntitySet.getESType();
    }

    @Override
    public ElasticEdmEntityType getEntityType() {
        EdmEntityType entityType = provider.getEntityType(new FullQualifiedName(
                csdlEntitySet.getTypeFQN().getNamespace(), csdlEntitySet.getESType()));
        return entityType != null ? (ElasticEdmEntityType) entityType
                : (ElasticEdmEntityType) provider.getEntityType(csdlEntitySet.getTypeFQN());
    }

    /**
     * Sets index to entity set.
     * 
     * @param esIntex
     *            ES index
     */
    public void setESIndex(String esIntex) {
        csdlEntitySet.setESIndex(esIntex);
    }

    /**
     * Sets type to entity set.
     * 
     * @param esType
     *            ES type
     */
    public void setESType(String esType) {
        csdlEntitySet.setESType(esType);
    }

    /**
     * Override because of if child entity type has name which starts with as
     * parent entity type name, then wrong entity set is returning.
     */
    @Override
    public EdmBindingTarget getRelatedBindingTarget(final String path) {
        if (path == null) {
            return null;
        }
        EdmBindingTarget bindingTarget = null;
        boolean found = false;
        for (Iterator<EdmNavigationPropertyBinding> itor = getNavigationPropertyBindings()
                .iterator(); itor.hasNext() && !found;) {
            EdmNavigationPropertyBinding binding = itor.next();
            checkBinding(binding);
            // Replace 'startsWith' to 'equals'
            if (path.equals(binding.getPath())) {
                Target target = new Target(binding.getTarget(), getEntityContainer());
                EdmEntityContainer entityContainer = getEntityContainer(
                        target.getEntityContainer());
                try {
                    bindingTarget = entityContainer.getEntitySet(target.getTargetName());
                    if (bindingTarget == null) {
                        throw new EdmException("Cannot find EntitySet " + target.getTargetName());
                    }
                } catch (EdmException e) {
                    bindingTarget = entityContainer.getSingleton(target.getTargetName());
                    if (bindingTarget == null) {
                        throw new EdmException("Cannot find Singleton " + target.getTargetName(),
                                e);
                    }
                } finally {
                    found = bindingTarget != null;
                }
            }
        }
        return bindingTarget;
    }

    private void checkBinding(EdmNavigationPropertyBinding binding) {
        if (binding.getPath() == null || binding.getTarget() == null) {
            throw new EdmException(
                    "Path or Target in navigation property binding must not be null!");
        }
    }

    private EdmEntityContainer getEntityContainer(FullQualifiedName containerName) {
        EdmEntityContainer entityContainer = edm.getEntityContainer(containerName);
        if (entityContainer == null) {
            throw new EdmException("Cannot find entity container with name: " + containerName);
        }
        return entityContainer;
    }

} public class ElasticEdmEntityType extends EdmEntityTypeImpl {

    private ElasticCsdlEntityType csdlEntityType;
    private Map<String, ElasticEdmProperty> propertiesCash;
    private Map<String, ElasticEdmNavigationProperty> navigationPropertiesCash;

    /**
     * Constructor to initialize entity type.
     * 
     * @param edm
     *            EDM provider
     * @param name
     *            entity type FQN
     * @param entityType
     *            CSDL entity type
     */
    public ElasticEdmEntityType(Edm edm, FullQualifiedName name, ElasticCsdlEntityType entityType) {
        super(edm, name, entityType);
        this.csdlEntityType = entityType;
    }

    /**
     * Get's index name in Elasticsearch.
     * 
     * @return index name
     */
    public String getESIndex() {
        return csdlEntityType.getESIndex();
    }

    /**
     * Get's type name in Elasticsearch.
     * 
     * @return type name
     */
    public String getESType() {
        return csdlEntityType.getESType();
    }

    /**
     * Get's CSDL property name by Elasticsearch field name.
     * 
     * @param esFieldName
     *            Elasticsearch field name
     * @return found property
     */
    public ElasticEdmProperty findPropertyByEField(String esFieldName) {
        for (Entry<String, ElasticEdmProperty> entry : getESProperties().entrySet()) {
            ElasticEdmProperty property = entry.getValue();
            if (property.getEField().equals(esFieldName)) {
                return property;
            }
        }
        return null;
    }

    /**
     * Gets Elasticsearch properties map.
     * 
     * @return properties map, with key - property name and value -
     *         {@link ElasticEdmProperty} instance
     */
    public Map<String, ElasticEdmProperty> getESProperties() {
        if (propertiesCash == null) {
            Map<String, ElasticEdmProperty> localPorperties = new LinkedHashMap<>();
            List<CsdlProperty> typeProperties = csdlEntityType.getProperties();
            for (CsdlProperty property : typeProperties) {
                if (property instanceof ElasticCsdlProperty) {
                    localPorperties.put(property.getName(),
                            new ElasticEdmProperty(edm, (ElasticCsdlProperty) property));
                }
            }
            propertiesCash = Collections.unmodifiableMap(localPorperties);
        }
        return propertiesCash;
    }

    @Override
    public Map<String, EdmProperty> getProperties() {
        Map<String, EdmProperty> properties = new HashMap<>();
        for (Entry<String, ElasticEdmProperty> entry : getESProperties().entrySet()) {
            properties.put(entry.getKey(), entry.getValue());
        }
        return properties;
    }

    /**
     * Gets Elasticsearch navigation properties map.
     * 
     * @return properties map, with key - property name and value -
     *         {@link ElasticEdmNavigationProperty} instance
     */
    public Map<String, ElasticEdmNavigationProperty> getESNavigationProperties() {
        if (navigationPropertiesCash == null) {
            Map<String, ElasticEdmNavigationProperty> localNavigationProperties = new LinkedHashMap<>();
            List<CsdlNavigationProperty> structuredTypeNavigationProperties = csdlEntityType
                    .getNavigationProperties();
            for (CsdlNavigationProperty property : structuredTypeNavigationProperties) {
                if (property instanceof ElasticCsdlNavigationProperty) {
                    localNavigationProperties.put(property.getName(),
                            new ElasticEdmNavigationProperty(edm,
                                    (ElasticCsdlNavigationProperty) property));
                }
            }
            navigationPropertiesCash = Collections.unmodifiableMap(localNavigationProperties);
        }
        return navigationPropertiesCash;
    }

    @Override
    public Map<String, EdmNavigationProperty> getNavigationProperties() {
        Map<String, EdmNavigationProperty> navigationProperties = new HashMap<>();
        for (Entry<String, ElasticEdmNavigationProperty> entry : getESNavigationProperties()
                .entrySet()) {
            navigationProperties.put(entry.getKey(), entry.getValue());
        }
        return navigationProperties;
    }

} public class ElasticEdmProperty extends EdmPropertyImpl {

    private ElasticCsdlProperty csdlProperty;

    /**
     * Constructor to initialize property.
     * 
     * @param edm
     *            EDM provider
     * @param property
     *            CSDL property
     */
    public ElasticEdmProperty(Edm edm, ElasticCsdlProperty property) {
        super(edm, property);
        this.csdlProperty = property;
    }

    /**
     * Get's field name in Elasticsearch.
     * 
     * @return field name
     */
    public String getEField() {
        return csdlProperty.getESField();
    }
} public class ESQueryBuilder<T extends ESQueryBuilder<T>> {

    /** Query builder. */
    protected BoolQueryBuilder query;
    /** Parent child query builder. */
    protected QueryBuilder parentChildQuery;
    /** Filter query builders. */
    protected List<QueryBuilder> filters;

    /**
     * Default constructor.
     */
    public ESQueryBuilder() {
        this.query = QueryBuilders.boolQuery();
        this.filters = new ArrayList<>();
    }

    /**
     * Method adds query depending on current and next URI resource segment.
     * 
     * @param segment
     *            current segment
     * @param nextSegment
     *            next segment
     * @return casted queryBuilder
     * @throws ODataApplicationException
     *             if any error occurred
     */
    @SuppressWarnings("unchecked")
    public T addSegmentQuery(UriResource segment, UriResource nextSegment)
            throws ODataApplicationException {
        ElasticEdmEntityType type = (ElasticEdmEntityType) ((UriResourcePartTyped) segment)
                .getType();
        String esType = type.getESType();
        List<String> ids = collectIds(segment);
        if (nextSegment == null) {
            addIdQuery(esType, ids);
        } else {
            if (nextSegment.getKind() == UriResourceKind.primitiveProperty) {
                addIdQuery(esType, ids);
            } else {
                if (((UriResourceNavigationPropertyImpl) nextSegment).getProperty()
                        .isCollection()) {
                    addParentQuery(esType, ids);
                } else {
                    addChildQuery(esType, ids);
                }
            }
        }
        return (T) this;
    }

    /**
     * Retrieves ids from uri resource part.
     *
     * @param segment
     *            uri resource part
     * @return ids list
     * @throws ODataApplicationException
     *             odata app exception
     */
    protected List<String> collectIds(UriResource segment) throws ODataApplicationException {
        List<UriParameter> keyPredicates;
        if (segment instanceof UriResourceNavigation) {
            keyPredicates = ((UriResourceNavigation) segment).getKeyPredicates();
        } else {
            keyPredicates = ((UriResourceEntitySet) segment).getKeyPredicates();
        }
        if (keyPredicates.size() > 1) {
            throwNotImplemented("Composite Keys are not supported");
        }
        return keyPredicates.stream().map(param -> param.getText().replaceAll("\'", ""))
                .collect(Collectors.toList());
    }

    /**
     * Adds a query that <b>must</b> appear in the matching documents but will
     * not contribute to scoring.
     * 
     * @param query
     *            query to add
     * @return builder instance
     */
    @SuppressWarnings("unchecked")
    public T addFilter(QueryBuilder query) {
        if (query != null) {
            filters.add(query);
        }
        return (T) this;
    }

    /**
     * Adds new level of parent query.
     * 
     * @param type
     *            parent type
     * @param ids
     *            list of ids of parent documents we are looking for
     */
    private void addParentQuery(String type, List<String> ids) {
        QueryBuilder parentQuery = ids.isEmpty() ? QueryBuilders.matchAllQuery()
                : buildIdQuery(type, ids);
        QueryBuilder resultQuery = getParentChildResultQuery(parentQuery);
        parentChildQuery = QueryBuilders.hasParentQuery(type, resultQuery, false);
    }

    /**
     * Adds new level of child query.
     * 
     * @param type
     *            child type
     * @param ids
     *            list of ids of child documents we are looking for
     */
    private void addChildQuery(String type, List<String> ids) {
        QueryBuilder childQuery = ids.isEmpty() ? QueryBuilders.matchAllQuery()
                : buildIdQuery(type, ids);
        QueryBuilder resultQuery = getParentChildResultQuery(childQuery);
        parentChildQuery = QueryBuilders.hasChildQuery(type, resultQuery, ScoreMode.None);
    }

    /**
     * Builds must query with existing {@link #parentChildQuery} and new query,
     * or just returns new query, if {@link #parentChildQuery} is null. Note: we
     * can't initialize {@link #parentChildQuery} in the beginning, because we
     * don't know what type it will be: has_parent or has_child.
     * 
     * @param query
     *            new query
     * @return raw es query
     */
    protected QueryBuilder getParentChildResultQuery(QueryBuilder query) {
        return parentChildQuery != null
                ? QueryBuilders.boolQuery().must(parentChildQuery).must(query) : query;
    }

    /**
     * Adds ids query to the current level.
     * 
     * @param type
     *            type
     * @param ids
     *            list of ids
     */
    protected void addIdQuery(String type, List<String> ids) {
        if (!ids.isEmpty()) {
            query.must(buildIdQuery(type, ids));
        }
    }

    /**
     * Creates query for specific id of type.
     * 
     * @param type
     *            type name
     * @param ids
     *            list of ids
     * @return query for id
     */
    protected QueryBuilder buildIdQuery(String type, List<String> ids) {
        return new IdsQueryBuilder().types(type).addIds(ids.toArray(new String[1]));
    }

    /**
     * Returns raw Elasticsearch query.
     * 
     * @return query builder
     */
    public QueryBuilder build() {
        BoolQueryBuilder resultQuery = QueryBuilders.boolQuery();
        if (query.hasClauses()) {
            resultQuery.must(query);
        }
        if (parentChildQuery != null) {
            resultQuery.must(parentChildQuery);
        }
        filters.forEach(resultQuery::filter);
        return resultQuery;
    }

} public class Pagination {
    /** Default skip value. */
    public static final int SKIP_DEFAULT = 0;
    /** Default top value. */
    public static final int TOP_DEFAULT = 25;
    private int top;
    private int skip;
    private List<Sort> orderBy;

    /**
     * Initializes pagination with all the data.
     * 
     * @param top
     *            top count
     * @param skip
     *            skip count
     * @param orderBy
     *            order by field
     */
    public Pagination(int top, int skip, List<Sort> orderBy) {
        this.top = top;
        this.skip = skip;
        this.orderBy = orderBy;
    }

    public int getTop() {
        return top;
    }

    public void setTop(int top) {
        this.top = top;
    }

    public int getSkip() {
        return skip;
    }

    public void setSkip(int skip) {
        this.skip = skip;
    }

    public List<Sort> getOrderBy() {
        return orderBy;
    }

    public void setOrderBy(List<Sort> orderBy) {
        this.orderBy = orderBy;
    }
} public class Sort {

    /** Default sort direction. */
    public static final Direction DEFAULT_DIRECTION = Direction.ASC;

    private String property;
    private Direction direction;

    /**
     * Creates a new {@link Sort} instance using the given property with default
     * direction.
     * 
     * @param property
     *            must not be {@literal null}
     */
    public Sort(String property) {
        this(property, DEFAULT_DIRECTION);
    }

    /**
     * Creates a new {@link Sort} instance using the given property with
     * specific {@link Direction} direction.
     * 
     * @param property
     *            must not be {@literal null}
     * @param direction
     *            direction to sort
     */
    public Sort(String property, Direction direction) {
        this.property = property;
        this.direction = direction;
    }

    public Direction getDirection() {
        return direction;
    }

    public String getProperty() {
        return property;
    }

    public void setDirection(Direction direction) {
        this.direction = direction;
    }

    public void setProperty(String property) {
        this.property = property;
    }

    /**
     * Enumeration for sort directions.
     * 
     * @author rdidyk
     */
    public enum Direction {
        /** Ascending direction. */
        ASC,
        /** Descending direction. */
        DESC;

        /**
         * Returns the {@link Direction} enum for the given {@link String}
         * value.
         * 
         * @param value
         *            direction value
         * @return direction based on value
         * @throws IllegalArgumentException
         *             in case the given value cannot be parsed into an enum
         *             value.
         */
        public static Direction fromString(String value) {
            for (Direction direction : values()) {
                if (direction.toString().equalsIgnoreCase(value)) {
                    return direction;
                }
            }
            throw new IllegalArgumentException(String.format(
                    "Invalid value '%s' for orders given! Has to be either 'desc' or 'asc' (case insensitive).",
                    value));
        }
    }

} public enum Direction {
    /** Ascending direction. */
    ASC,
    /** Descending direction. */
    DESC;

    /**
     * Returns the {@link Direction} enum for the given {@link String}
     * value.
     * 
     * @param value
     *            direction value
     * @return direction based on value
     * @throws IllegalArgumentException
     *             in case the given value cannot be parsed into an enum
     *             value.
     */
    public static Direction fromString(String value) {
        for (Direction direction : values()) {
            if (direction.toString().equalsIgnoreCase(value)) {
                return direction;
            }
        }
        throw new IllegalArgumentException(String.format(
                "Invalid value '%s' for orders given! Has to be either 'desc' or 'asc' (case insensitive).",
                value));
    }
} @Getter
public class AggregateQuery extends Query {

    @NonNull
    private final List<AggregationBuilder> aggregations;
    private final List<PipelineAggregationBuilder> pipelineAggregations;

    /**
     * Constructor to initialize parameters.
     * 
     * @param index
     *            index name
     * @param types
     *            types name
     * @param queryBuilder
     *            main query builder
     * @param aggregations
     *            simple aggregations
     */
    public AggregateQuery(String index, String[] types, QueryBuilder queryBuilder,
            AggregationBuilder aggregations) {
        super(index, types, queryBuilder, null);
        this.aggregations = Arrays.asList(aggregations);
        this.pipelineAggregations = Collections.emptyList();
    }

    /**
     * Constructor to initialize parameters.
     * 
     * @param index
     *            index name
     * @param types
     *            types name
     * @param queryBuilder
     *            main query builder
     * @param aggregations
     *            simple aggregations
     * @param pipelineAggregations
     *            pipeline aggregations
     */
    public AggregateQuery(String index, String[] types, QueryBuilder queryBuilder,
            List<AggregationBuilder> aggregations,
            List<PipelineAggregationBuilder> pipelineAggregations) {
        super(index, types, queryBuilder, null);
        this.aggregations = aggregations;
        this.pipelineAggregations = pipelineAggregations;
    }

} @AllArgsConstructor
@Getter
@Setter
public class Query {

    @NonNull
    private String index;
    @NonNull
    private String[] types;
    @NonNull
    private QueryBuilder queryBuilder;
    private Pagination pagination;

} @Getter
public class AggregateRequest extends BaseRequest {
    private final String countAlias;

    /**
     * Constructor to initialize query and entity.
     *
     * @param query
     *            aggregate query
     * @param entitySet
     *            the edm entity set
     */
    public AggregateRequest(AggregateQuery query, ElasticEdmEntitySet entitySet) {
        this(query, entitySet, null, null);
    }

    /**
     * Constructor to initialize query and entity.
     *
     * @param query
     *            aggregate query
     * @param entitySet
     *            the edm entity set
     * @param countAlias
     *            name of count alias, or null if no count option applied
     */
    public AggregateRequest(AggregateQuery query, ElasticEdmEntitySet entitySet,
            String countAlias) {
        this(query, entitySet, null, countAlias);
    }

    /**
     * Constructor to initialize query and entity.
     *
     * @param query
     *            aggregate query
     * @param entitySet
     *            the edm entity set
     * @param pagination
     *            pagination information
     */
    public AggregateRequest(AggregateQuery query, ElasticEdmEntitySet entitySet,
            Pagination pagination) {
        this(query, entitySet, pagination, null);
    }

    /**
     * Constructor to initialize query and entity.
     * 
     * @param query
     *            aggregate query
     * @param entitySet
     *            the edm entity set
     * @param pagination
     *            pagination information
     * @param countAlias
     *            name of count alias, or null if no count option applied
     */
    public AggregateRequest(AggregateQuery query, ElasticEdmEntitySet entitySet,
            Pagination pagination, String countAlias) {
        super(query, entitySet, pagination);
        this.countAlias = countAlias;
    }

    @Override
    public SearchResponse execute() throws ODataApplicationException {
        return ESClient.getInstance().executeRequest(getQuery());
    }

    @Override
    public AggregateQuery getQuery() {
        return (AggregateQuery) super.getQuery();
    }

} public interface ESRequest {

    /**
     * Gets query.
     * 
     * @return query
     */
    Query getQuery();

    /**
     * Gets pagination.
     *
     * @return pagination
     */
    Pagination getPagination();

    /**
     * Gets entity set.
     * 
     * @return the edm entity set
     */
    ElasticEdmEntitySet getEntitySet();

    /**
     * Executes request and returns search response.
     * 
     * @return found data
     * @throws ODataApplicationException
     *             if any error appeared during executing request
     */
    SearchResponse execute() throws ODataApplicationException;

}
import static com.hevelian.olastic.core.elastic.utils.ElasticUtils.addKeywordIfNeeded;
import static com.hevelian.olastic.core.utils.ApplyOptionUtils.getAggregations;
import static com.hevelian.olastic.core.utils.ApplyOptionUtils.getGroupByItems;
import static com.hevelian.olastic.core.utils.ProcessorUtils.throwNotImplemented;
import static java.util.Collections.reverse;
import static java.util.stream.Collectors.toMap;
import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.olingo.commons.api.http.HttpStatusCode;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceKind;
import org.apache.olingo.server.api.uri.queryoption.apply.GroupBy;
import org.apache.olingo.server.api.uri.queryoption.apply.GroupByItem;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import com.hevelian.olastic.core.edm.ElasticEdmEntitySet;
import com.hevelian.olastic.core.edm.ElasticEdmEntityType;
import com.hevelian.olastic.core.edm.ElasticEdmProperty;
import com.hevelian.olastic.core.elastic.builders.ESQueryBuilder;
import com.hevelian.olastic.core.elastic.pagination.Pagination;
import com.hevelian.olastic.core.elastic.pagination.Sort;
import com.hevelian.olastic.core.elastic.pagination.Sort.Direction;
import com.hevelian.olastic.core.elastic.queries.AggregateQuery;
import com.hevelian.olastic.core.elastic.queries.Query;
import com.hevelian.olastic.core.elastic.requests.AggregateRequest;
import com.hevelian.olastic.core.elastic.requests.ESRequest;
package com.hevelian.olastic.core.elastic.requests.creators;





/**
 * Class responsible for creating {@link AggregateRequest} instance for buckets
 * aggregations with metrics.
 * 
 * @author rdidyk
 */
public class BucketsAggregationsRequestCreator extends AbstractAggregationsRequestCreator {

    /**
     * Default constructor.
     */
    public BucketsAggregationsRequestCreator() {
        super();
    }

    /**
     * Constructor to initialize ES query builder.
     * 
     * @param queryBuilder
     *            ES query builder
     */
    public BucketsAggregationsRequestCreator(ESQueryBuilder<?> queryBuilder) {
        super(queryBuilder);
    }

    @Override
    public AggregateRequest create(UriInfo uriInfo) throws ODataApplicationException {
        ESRequest baseRequestInfo = getBaseRequestInfo(uriInfo);
        Query baseQuery = baseRequestInfo.getQuery();
        ElasticEdmEntitySet entitySet = baseRequestInfo.getEntitySet();
        ElasticEdmEntityType entityType = entitySet.getEntityType();

        List<GroupBy> groupByItems = getGroupByItems(uriInfo.getApplyOption());
        if (groupByItems.size() > 1) {
            throwNotImplemented("Combining Transformations per Group is not supported.");
        }
        Pagination pagination = getPagination(uriInfo);
        List<AggregationBuilder> bucketsQueries = getBucketsQueries(groupByItems.get(0), entityType,
                pagination);

        AggregateQuery aggregateQuery = new AggregateQuery(baseQuery.getIndex(),
                baseQuery.getTypes(), baseQuery.getQueryBuilder(), bucketsQueries,
                Collections.emptyList());
        return new AggregateRequest(aggregateQuery, entitySet, pagination, getCountAlias());
    }

    /**
     * Get's buckets queries from {@link GroupBy} item in URL.
     *
     * @param groupBy
     *            groupBy instance
     * @param entityType
     *            entity type
     * @param pagination
     *            pagination information
     * @return list of fields
     * @throws ODataApplicationException
     *             if any error occurred
     */
    protected List<AggregationBuilder> getBucketsQueries(GroupBy groupBy,
            ElasticEdmEntityType entityType, Pagination pagination)
            throws ODataApplicationException {
        int size = pagination.getSkip() + pagination.getTop();
        Map<String, Boolean> orders = pagination.getOrderBy().stream()
                .collect(toMap(Sort::getProperty, order -> order.getDirection() == Direction.ASC));
        List<String> properties = getProperties(groupBy);
        reverse(properties);

        // Last because of reverse
        String lastProperty = properties.remove(0);
        String queryField = getQueryField(lastProperty, entityType);
        TermsAggregationBuilder groupByQuery = terms(lastProperty).field(queryField).size(size)
                .shardSize(getShardSize(size));
        getMetricsAggQueries(getAggregations(groupBy.getApplyOption()))
                .forEach(groupByQuery::subAggregation);
        List<Order> queryOrders = getQueryOrders(queryField, orders);
        if (!queryOrders.isEmpty()) {
            groupByQuery.order(queryOrders);
        }

        for (String property : properties) {
            queryField = getQueryField(property, entityType);
            groupByQuery = terms(property).field(queryField).size(size)
                    .subAggregation(groupByQuery);
            Boolean termOrder = orders.remove(queryField);
            if (termOrder != null) {
                groupByQuery.order(Terms.Order.term(termOrder));
            }
        }

        // Fields in $orderby are not same as $groupby fields!
        if (!orders.isEmpty()) {
            throw new ODataApplicationException(
                    "Ordering only by fields in $groupby or $aggregation options is allowed.",
                    HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ROOT);
        }
        // For now only one 'groupby' is supported and returned in the list
        return Arrays.asList(groupByQuery);
    }

    /**
     * Get's shard_size for terms aggregation.
     * 
     * @param size
     *            actual requested size
     * @return shard_size
     */
    protected int getShardSize(int size) {
        // The default shard_size will be size if the search request needs to go
        // to a single shard - by Elasticsearch documentation
        return size;
    }

    /**
     * Get's properties from {@link #groupBy} for aggregation query.
     * 
     * @param groupBy
     *            groupBy instance
     * @return list of properties
     * @throws ODataApplicationException
     *             if any error occurred
     */
    private static List<String> getProperties(GroupBy groupBy) throws ODataApplicationException {
        List<String> groupByProperties = new ArrayList<>();
        for (GroupByItem item : groupBy.getGroupByItems()) {
            List<UriResource> path = item.getPath();
            if (path.size() > 1) {
                throwNotImplemented("Grouping by navigation property is not supported yet.");
            }
            UriResource resource = path.get(0);
            if (resource.getKind() == UriResourceKind.primitiveProperty) {
                groupByProperties.add(resource.getSegmentValue());
            } else {
                throwNotImplemented("Grouping by complex type is not supported yet.");
            }
        }
        return groupByProperties;
    }

    /**
     * Get's list of orders for first query, because it has metrics aggregations
     * and also can have count and term order.
     * 
     * @param field
     *            query field
     * @param ordersMap
     *            orders map from URI
     * @return list of orders
     */
    private List<Order> getQueryOrders(String field, Map<String, Boolean> ordersMap) {
        List<Order> orders = new ArrayList<>();
        Boolean termOrder = ordersMap.remove(field);
        if (termOrder != null) {
            orders.add(Terms.Order.term(termOrder));
        }
        Boolean countOrder = ordersMap.remove(getCountAlias());
        if (countOrder != null) {
            orders.add(Terms.Order.count(countOrder));
        }
        for (String alias : getMetricAliases()) {
            Boolean aliasOrder = ordersMap.remove(alias);
            if (aliasOrder != null) {
                orders.add(Terms.Order.aggregation(alias, aliasOrder));
            }
        }
        return orders;
    }

    /**
     * Gets field for 'term' aggregation query by property from entity type.
     * 
     * @param propertyName
     *            property name
     * @param entityType
     *            entity type
     * @return field for query
     */
    private static String getQueryField(String propertyName, ElasticEdmEntityType entityType) {
        ElasticEdmProperty property = entityType.getESProperties().get(propertyName);