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

com.github.ydespreaux.spring.data.elasticsearch.core.CriteriaFilterProcessor Maven / Gradle / Ivy

/*
 * Copyright (C) 2018 Yoann Despréaux
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING . If not, write to the
 * Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * Please send bugreports with examples or suggestions to [email protected]
 */
package com.github.ydespreaux.spring.data.elasticsearch.core;

import com.github.ydespreaux.spring.data.elasticsearch.core.query.Criteria;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.index.query.*;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import java.util.*;
import java.util.stream.Collectors;


/**
 * CriteriaFilterProcessor generate filter-related queries for a {@link Criteria} object
 *
 * @author Yoann Despréaux
 * @since 1.0.0
 */
@Slf4j
final class CriteriaFilterProcessor {

    /**
     * @param array
     * @param clazz
     * @return
     */
    private static boolean isType(Object[] array, Class clazz) {
        for (Object o : array) {
            if (!clazz.isInstance(o)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Create query filter
     *
     * @param criteria
     * @return
     */
    final Optional createFilterFromCriteria(Criteria criteria) {
        List fbList = new LinkedList<>();
        criteria.getCriteriaChain().forEach(chainedCriteria -> {
            if (chainedCriteria.isOr()) {
                BoolQueryBuilder fb = QueryBuilders.boolQuery();
                createFilterFragmentForCriteria(chainedCriteria).forEach(fb::should);
                if (fb.hasClauses()) {
                    fbList.add(fb);
                }
            } else if (chainedCriteria.isNegating()) {
                fbList.addAll(buildNegationFilter(criteria.getField().getName(), criteria.getFilterCriteriaEntries()));
            } else {
                fbList.addAll(createFilterFragmentForCriteria(chainedCriteria));
            }
        });
        QueryBuilder filter = null;
        if (!fbList.isEmpty()) {
            if (fbList.size() == 1) {
                filter = fbList.get(0);
            } else {
                filter = QueryBuilders.boolQuery();
                fbList.forEach(((BoolQueryBuilder) filter)::must);
            }
        }
        return Optional.ofNullable(filter);
    }

    /**
     * @param chainedCriteria
     * @return
     */
    private List createFilterFragmentForCriteria(Criteria chainedCriteria) {
        String fieldName = chainedCriteria.getField().getName();
        Assert.notNull(fieldName, "Unknown field");
        return chainedCriteria.getFilterCriteriaEntries().stream()
                .map(entry -> processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    /**
     * @param fieldName
     * @param entries
     * @return
     */
    private List buildNegationFilter(String fieldName, Set entries) {
        return entries.stream()
                .map(entry -> processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    /**
     * @param key
     * @param value
     * @param fieldName
     * @return
     */
    @Nullable
    private QueryBuilder processCriteriaEntry(Criteria.OperationKey key, @NonNull Object value, String fieldName) {
        if (Criteria.OperationKey.WITHIN == key) {
            return processWithinFilter(value, fieldName);
        } else if (Criteria.OperationKey.BOX == key) {
            return processBoxFilter(value, fieldName);
        }
        if (log.isWarnEnabled()) {
            log.warn("Key operator {} is not a filter operator", key);
        }
        return null;
    }

    private QueryBuilder processWithinFilter(Object value, String fieldName) {
        GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders.geoDistanceQuery(fieldName);
        Assert.isTrue(value instanceof Object[], "Value of a geo distance filter should be an array of two values.");
        Object[] valArray = (Object[]) value;
        Assert.noNullElements(valArray, "Geo distance filter takes 2 not null elements array as parameter.");
        Assert.isTrue(valArray.length == 2, "Geo distance filter takes a 2-elements array as parameter.");
        Assert.isTrue(valArray[0] instanceof GeoPoint || valArray[0] instanceof String || valArray[0] instanceof Point, "First element of a geo distance filter must be a GeoPoint, a Point or a text");
        Assert.isTrue(valArray[1] instanceof String || valArray[1] instanceof Distance, "Second element of a geo distance filter must be a text or a Distance");

        StringBuilder dist = new StringBuilder();
        if (valArray[1] instanceof Distance) {
            extractDistanceString((Distance) valArray[1], dist);
        } else {
            dist.append((String) valArray[1]);
        }
        if (valArray[0] instanceof GeoPoint) {
            GeoPoint loc = (GeoPoint) valArray[0];
            geoDistanceQueryBuilder.point(loc.getLat(), loc.getLon()).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
        } else if (valArray[0] instanceof Point) {
            Point point = (Point) valArray[0];
            geoDistanceQueryBuilder.point(point.getX(), point.getY()).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
        } else {
            String loc = (String) valArray[0];
            if (loc.contains(",")) {
                String[] c = loc.split(",");
                geoDistanceQueryBuilder.point(Double.parseDouble(c[0]), Double.parseDouble(c[1])).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
            } else {
                geoDistanceQueryBuilder.geohash(loc).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
            }
        }
        return geoDistanceQueryBuilder;

    }

    private QueryBuilder processBoxFilter(Object value, String fieldName) {
        GeoBoundingBoxQueryBuilder filter = QueryBuilders.geoBoundingBoxQuery(fieldName);
        Assert.isTrue(value instanceof Object[], "Value of a boundedBy filter should be an array of one or two values.");
        Object[] valArray = (Object[]) value;
        Assert.noNullElements(valArray, "Geo boundedBy filter takes a not null element array as parameter.");

        if (valArray.length == 1) {
            //GeoEnvelop
            Assert.isTrue(valArray[0] instanceof Box, "single-element of boundedBy filter must be type of Box");
            Box box = (Box) valArray[0];
            filter.setCorners(box.getFirst().getX(), box.getFirst().getY(), box.getSecond().getX(), box.getSecond().getY());
        } else if (valArray.length == 2) {
            Assert.isTrue(isType(valArray, GeoPoint.class) || isType(valArray, String.class), " both elements of boundedBy filter must be type of GeoPoint or text(format lat,lon or geohash)");
            if (valArray[0] instanceof GeoPoint) {
                GeoPoint topLeft = (GeoPoint) valArray[0];
                GeoPoint bottomRight = (GeoPoint) valArray[1];
                filter.setCorners(topLeft.getLat(), topLeft.getLon(), bottomRight.getLat(), bottomRight.getLon());
            } else {
                String topLeft = (String) valArray[0];
                String bottomRight = (String) valArray[1];
                if (topLeft.contains(",") && bottomRight.contains(",")) {
                    String[] topLeftValues = topLeft.split(",");
                    String[] bottomRightValues = bottomRight.split(",");
                    filter.setCorners(Double.parseDouble(topLeftValues[0]), Double.parseDouble(topLeftValues[1]), Double.parseDouble(bottomRightValues[0]), Double.parseDouble(bottomRightValues[1]));
                } else {
                    filter.setCorners(topLeft, bottomRight);
                }
            }
        } else {
            //error
            Assert.isTrue(false, "Geo distance filter takes a 1-elements array(GeoBox) or 2-elements array(GeoPoints or Strings(format lat,lon or geohash)).");
        }
        return filter;
    }

    /**
     * extract the distance string from a {@link org.springframework.data.geo.Distance} object.
     *
     * @param distance distance object to extract string from
     * @param sb       StringBuilder to build the distance string
     */
    private void extractDistanceString(Distance distance, StringBuilder sb) {
        // handle Distance object
        sb.append((int) distance.getValue());
        Metrics metric = (Metrics) distance.getMetric();
        if (metric == Metrics.KILOMETERS) {
            sb.append("km");
        } else if (metric == Metrics.MILES) {
            sb.append("mi");
        } else {
            throw new IllegalArgumentException("Distance.metric must be km or mi");
        }
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy