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

io.sphere.internal.util.SearchUtil Maven / Gradle / Ivy

There is a newer version: 0.72.1
Show newest version
package io.sphere.internal.util;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import com.google.common.collect.Ranges;
import io.sphere.client.AttributeProductSort;
import io.sphere.client.ProductSort;
import io.sphere.client.filters.expressions.FilterType;
import io.sphere.client.QueryParam;
import io.sphere.client.model.Money;
import io.sphere.client.shop.model.Category;
import static io.sphere.internal.util.ListUtil.*;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Duration;
import org.joda.time.format.ISODateTimeFormat;

import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

// date     yyyy-MM-dd                  ISODateTimeFormat.date().print(ld)
// time     HH:mm:ss.SSS                ISODateTimeFormat.time().print(lt)
// datetime yyyy-MM-ddTHH:mm:ss.SSSZZ   ISODateTimeFormat.dateTime().print(dt.withZone(DateTimeZone.UTC))

/** Helpers for constructing search queries to the backend. */
public class SearchUtil {
    public static final class Names {
        public static final String categories = "categories.id";
        public static final String priceFull = "variants.price.centAmount";
        public static final String centAmount = ".centAmount";
    }

    public static final class QueryParamNames {
        public static final String price = "price";
    }

    // ------------------------------------------------------------------
    // Filters
    // ------------------------------------------------------------------

    /** Creates a query parameter for a {@link io.sphere.client.filters.expressions.FilterExpression}. */
    public static List createFilterParams(FilterType filterType, String attribute, String queryString) {
        switch (filterType) {
            case RESULTS_AND_FACETS: return list(new QueryParam("filter.query", attribute + ":" + queryString));
            case RESULTS: return list(new QueryParam("filter", attribute + ":" + queryString));
            case FACETS: return list(new QueryParam("filter.facets", attribute + ":" + queryString));
            case SMART: return list(
                    new QueryParam("filter", attribute + ":" + queryString),
                    new QueryParam("filter.facets", attribute + ":" + queryString));
            default: return list(new QueryParam("filter.query", attribute + ":" + queryString));
        }
    }

    // ------------------------------------------------------------------
    // Facets
    // ------------------------------------------------------------------

    /** Creates a query parameter for a {@link io.sphere.client.facets.expressions.FacetExpression}. */
    public static QueryParam createTermFacetParam(String attribute) {
        return new QueryParam("facet", attribute);
    }

    /** Creates a query parameter for a {@link io.sphere.client.facets.expressions.FacetExpression}. */
    public static QueryParam createRangeFacetParam(String attribute, String ranges) {
        return new QueryParam("facet", attribute + (Strings.isNullOrEmpty(ranges) ? "" : ":range " + ranges));
    }

    // ------------------------------------------------------------------
    // Upper bound adjustment for range facets
    // ------------------------------------------------------------------
    // ElasticSearch range facets are always [lower_inclusive, upper_exclusive),
    // without any possibility of configuration.
    // This doesn't match how our filters work: [lower_inclusive, upper_inclusive].
    // To get around this, we adjust the upper bound of facet ranges manually.
    // For example, if a user defines a facet [0, 10], [10, 20] we transform these
    // intervals into [0, 10.001), [10, 20.001) for the facet, and keep [0, 10], [10, 20]
    // for filters.
    // As a result, a product with the exact value of 10 will be counted (by the facet)
    // and returned (by the filter) for both intervals, which makes typical multiselect
    // facets behave in a consistent way.

    // Long: smallest step 1
    public static long adjustLongForSearch(long upperEndpoint) {
        return upperEndpoint + 1;
    }
    public static long adjustLongBackFromSearch(long searchUpperEndpoint) {
        return searchUpperEndpoint - 1;
    }

    // Double: smallest step 0.001 and round all numbers to 3 decimal places
    public static double adjustDoubleForSearch(double upperEndpoint) {
        BigDecimal rounded = new BigDecimal(upperEndpoint).setScale(3, RoundingMode.HALF_UP);
        return rounded.add(new BigDecimal(0.001)).setScale(3, RoundingMode.HALF_UP).doubleValue();
    }
    public static double adjustDoubleBackFromSearch(double searchUpperEndpoint) {
        return new BigDecimal(searchUpperEndpoint).subtract(new BigDecimal(0.001)).setScale(3, RoundingMode.HALF_UP).doubleValue();
    }

    // DateTime: smallest step 1ms
    public static DateTime adjustDateTimeForSearch(DateTime upperEndpoint) {
        return upperEndpoint.plus(Duration.millis(1));
    }
    public static DateTime adjustDateTimeBackFromSearch(DateTime searchUpperEndpoint) {
        return searchUpperEndpoint.minus(Duration.millis(1));
    }

    public static Function, Range> adjustLongFacetRange = new Function, Range>() {
        @Override public Range apply(@Nullable Range range) {
            if (!range.hasUpperBound()) return range;
            return range.span(Ranges.closed(range.upperEndpoint(), adjustLongForSearch(range.upperEndpoint())));
        }
    };

    public static Function, Range> adjustDoubleFacetRange = new Function, Range>() {
        @Override public Range apply(@Nullable Range range) {
            if (!range.hasUpperBound()) return range;
            return range.span(Ranges.closed(range.upperEndpoint(), adjustDoubleForSearch(range.upperEndpoint())));
        }
    };

    public static Function, Range> adjustDateTimeFacetRange = new Function, Range>() {
        @Override public Range apply(@Nullable Range range) {
            if (!range.hasUpperBound()) return range;
            return range.span(Ranges.closed(range.upperEndpoint(), adjustDateTimeForSearch(range.upperEndpoint())));
        }
    };




    // ------------------------------------------------------------------
    // Sort
    // ------------------------------------------------------------------

    private static final QueryParam qpPriceAsc = new QueryParam("sort", "price asc");
    private static final QueryParam qpPriceDesc = new QueryParam("sort", "price desc");
    private static final QueryParam qpNameAsc = new QueryParam("sort", "name asc");
    private static final QueryParam qpNameDesc = new QueryParam("sort", "name desc");
    // 'Pattern-match' on the sort object.
    // Another (more type safe) option would be to have a ProductSort.createParam() method.
    // That method  would, however, need to be public and pollute the public API.
    public static QueryParam createSortParam(ProductSort sort) {
        if (sort == ProductSort.relevance) return null;
        if (sort == ProductSort.price.asc) return qpPriceAsc;
        if (sort == ProductSort.price.desc) return qpPriceDesc;
        if (sort == ProductSort.name.asc) return qpNameAsc;
        if (sort == ProductSort.name.desc) return qpNameDesc;
        if (sort instanceof AttributeProductSort) {
            final String sortExpression = ((AttributeProductSort) sort).toSphereSort();
            return new QueryParam("sort", sortExpression);
        }
        throw new IllegalArgumentException("Unknown sort: " + sort);
    }

    // ------------------------------------------------------------------
    // Categories
    // ------------------------------------------------------------------

    /** Returns ids of given categories (and optionally also of all their subtrees). */
    public static ImmutableList getCategoryIds(boolean includeSubcategories, Iterable categories) {
        if (categories == null) {
            return ImmutableList.of();
        }
        ImmutableList.Builder builder = ImmutableList.builder();
        for (Category c: categories) {
            if (c == null) continue;
            if (includeSubcategories) {
                for (Category subCategory: getSubtree(c)) {
                    builder.add(subCategory.getId());
                }
            } else {
                builder.add(c.getId());
            }
        }
        return builder.build();
    }

    /** Returns a list of the whole subtree for this category, starting with the category itself. */
    private static List getSubtree(Category category) {
        List subtree = new ArrayList();
        addSubtreeRecursive(category, subtree);
        return subtree;
    }
    private static void addSubtreeRecursive(Category root, List list) {
        if (root == null) return;
        list.add(root);
        for (Category child: root.getChildren()) {
            addSubtreeRecursive(child, list);
        }
    }

    // ------------------------------------------------------------------
    // Backend query construction
    // ------------------------------------------------------------------

    public static String formatRange(String range) {
        return "range" + range;
    }

    /** Joins strings using ','. */
    public static final Joiner joinCommas = Joiner.on(',');

    /** Quotes a string. */
    public static final Function addQuotes = new Function() {
        public String apply(String s) {
            return "\"" + s + "\"";
        }
    };

    public static final Function stringToParam = new Function() {
        public String apply(String value) {
            return addQuotes.apply(value);
        }
    };

    public static final Function doubleToParam = new Function() {
        public String apply(Double value) {
            return value.toString();
        }
    };

    public static final Function decimalToParam = new Function() {
        public String apply(BigDecimal value) {
            return value.toString();
        }
    };


    public static final Function dateTimeToParam = new Function() {
        public String apply(DateTime dateTime) {
            return addQuotes.apply(ISODateTimeFormat.dateTime().print(dateTime.withZone(DateTimeZone.UTC)));
        }
    };

    public static final Function toCents = new Function() {
        public Long apply(BigDecimal amount) {
            return Money.amountToCents(amount);
        }
    };

    // ------------------------------------------------------------------
    // Backend query construction - ranges
    // ------------------------------------------------------------------

    public static final Function, String> doubleRangeToParam = new Function, String>() {
        public String apply(Range range) {
            return rangeToParam(range);
        }
    };

    public static final Function, String> longRangeToParam = new Function, String>() {
        public String apply(Range range) {
            return rangeToParam(range);
        }
    };

    public static final Function, String> decimalRangeToParam = new Function, String>() {
        public String apply(Range range) {
            return rangeToParam(range);
        }
    };

    public static final Function, String> dateTimeRangeToParam = new Function, String>() {
        public String apply(Range range) {
            if (range == null)
                throw new IllegalArgumentException("range");
            String f = range.hasLowerBound() ? dateTimeToParam.apply(range.lowerEndpoint()) : "*";
            String t = range.hasUpperBound() ? dateTimeToParam.apply(range.upperEndpoint()) : "*";
            return "(" + f + " to " + t + ")";
        }
    };

    public static  String rangeToParam(Range range) {
        if (range == null)
            throw new IllegalArgumentException("range");
        String f = range.hasLowerBound() ? range.lowerEndpoint().toString() : "*";
        String t = range.hasUpperBound() ? range.upperEndpoint().toString() : "*";
        return "(" + f + " to " + t + ")";
    }

    /** Multiplies range by 100 and rounds to integer (conversion from units to 'cents'). */
    public static Function, Range> toCentRange = new Function, Range>() {
        public Range apply(Range range) {
            if (range == null)
                return null;
            Range downTo = range.hasLowerBound() ?
                    Ranges.downTo(toCents.apply(range.lowerEndpoint()), range.lowerBoundType()) :
                    Ranges.all();
            Range upTo = range.hasUpperBound() ?
                    Ranges.upTo(toCents.apply(range.upperEndpoint()), range.upperBoundType()) :
                    Ranges.all();
            return downTo.intersection(upTo);
        }
    };
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy