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

com.querydsl.lucene5.LuceneSerializer Maven / Gradle / Ivy

There is a newer version: 5.1.0
Show newest version
/*
 * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
 *
 * 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
 * http://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.querydsl.lucene5;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortedNumericSortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.WildcardQuery;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.NumericUtils;

import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.querydsl.core.QueryMetadata;
import com.querydsl.core.types.Constant;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Operation;
import com.querydsl.core.types.Operator;
import com.querydsl.core.types.Ops;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.ParamExpression;
import com.querydsl.core.types.ParamNotSetException;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.PathMetadata;
import com.querydsl.core.types.PathType;

/**
 * Serializes Querydsl queries to Lucene queries.
 *
 * @author vema
 *
 */
public class LuceneSerializer {
    private static final Map, SortField.Type> sortFields = new HashMap, SortField.Type>();

    static {
        sortFields.put(Integer.class, SortField.Type.INT);
        sortFields.put(Float.class, SortField.Type.FLOAT);
        sortFields.put(Long.class, SortField.Type.LONG);
        sortFields.put(Double.class, SortField.Type.DOUBLE);
        sortFields.put(BigDecimal.class, SortField.Type.DOUBLE);
        sortFields.put(BigInteger.class, SortField.Type.LONG);
    }

    private static final Splitter WS_SPLITTER = Splitter.on(Pattern
            .compile("\\s+"));

    public static final LuceneSerializer DEFAULT = new LuceneSerializer(false,
            true);

    private final boolean lowerCase;

    private final boolean splitTerms;

    private final Locale sortLocale;

    public LuceneSerializer(boolean lowerCase, boolean splitTerms) {
        this(lowerCase, splitTerms, Locale.getDefault());
    }

    public LuceneSerializer(boolean lowerCase, boolean splitTerms,
            Locale sortLocale) {
        this.lowerCase = lowerCase;
        this.splitTerms = splitTerms;
        this.sortLocale = sortLocale;
    }

    private Query toQuery(Operation operation, QueryMetadata metadata) {
        Operator op = operation.getOperator();
        if (op == Ops.OR) {
            return toTwoHandSidedQuery(operation, Occur.SHOULD, metadata);
        } else if (op == Ops.AND) {
            return toTwoHandSidedQuery(operation, Occur.MUST, metadata);
        } else if (op == Ops.NOT) {
            BooleanQuery bq = new BooleanQuery();
            bq.add(new BooleanClause(toQuery(operation.getArg(0), metadata),
                    Occur.MUST_NOT));
            bq.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST));
            return bq;
        } else if (op == Ops.LIKE) {
            return like(operation, metadata);
        } else if (op == Ops.LIKE_IC) {
            throw new IgnoreCaseUnsupportedException();
        } else if (op == Ops.EQ) {
            return eq(operation, metadata, false);
        } else if (op == Ops.EQ_IGNORE_CASE) {
            throw new IgnoreCaseUnsupportedException();
        } else if (op == Ops.NE) {
            return ne(operation, metadata, false);
        } else if (op == Ops.STARTS_WITH) {
            return startsWith(metadata, operation, false);
        } else if (op == Ops.STARTS_WITH_IC) {
            throw new IgnoreCaseUnsupportedException();
        } else if (op == Ops.ENDS_WITH) {
            return endsWith(operation, metadata, false);
        } else if (op == Ops.ENDS_WITH_IC) {
            throw new IgnoreCaseUnsupportedException();
        } else if (op == Ops.STRING_CONTAINS) {
            return stringContains(operation, metadata, false);
        } else if (op == Ops.STRING_CONTAINS_IC) {
            throw new IgnoreCaseUnsupportedException();
        } else if (op == Ops.BETWEEN) {
            return between(operation, metadata);
        } else if (op == Ops.IN) {
            return in(operation, metadata, false);
        } else if (op == Ops.NOT_IN) {
            return notIn(operation, metadata, false);
        } else if (op == Ops.LT) {
            return lt(operation, metadata);
        } else if (op == Ops.GT) {
            return gt(operation, metadata);
        } else if (op == Ops.LOE) {
            return le(operation, metadata);
        } else if (op == Ops.GOE) {
            return ge(operation, metadata);
        } else if (op == LuceneOps.LUCENE_QUERY) {
            @SuppressWarnings("unchecked")
            // this is the expected type
            Constant expectedConstant = (Constant) operation
                    .getArg(0);
            return expectedConstant.getConstant();
        }
        throw new UnsupportedOperationException("Illegal operation "
                + operation);
    }

    private Query toTwoHandSidedQuery(Operation operation, Occur occur,
            QueryMetadata metadata) {
        Query lhs = toQuery(operation.getArg(0), metadata);
        Query rhs = toQuery(operation.getArg(1), metadata);
        BooleanQuery bq = new BooleanQuery();
        bq.add(createBooleanClause(lhs, occur));
        bq.add(createBooleanClause(rhs, occur));
        return bq;
    }

    /**
     * If the query is a BooleanQuery and it contains a single Occur.MUST_NOT
     * clause it will be returned as is. Otherwise it will be wrapped in a
     * BooleanClause with the given Occur.
     */
    private BooleanClause createBooleanClause(Query query, Occur occur) {
        if (query instanceof BooleanQuery) {
            BooleanClause[] clauses = ((BooleanQuery) query).getClauses();
            if (clauses.length == 1
                    && clauses[0].getOccur().equals(Occur.MUST_NOT)) {
                return clauses[0];
            }
        }
        return new BooleanClause(query, occur);
    }

    protected Query like(Operation operation, QueryMetadata metadata) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        String field = toField(path);
        String[] terms = convert(path, operation.getArg(1));
        if (terms.length > 1) {
            BooleanQuery bq = new BooleanQuery();
            for (String s : terms) {
                bq.add(new WildcardQuery(new Term(field, "*" + s + "*")),
                        Occur.MUST);
            }
            return bq;
        }
        return new WildcardQuery(new Term(field, terms[0]));
    }

    protected Query eq(Operation operation, QueryMetadata metadata,
            boolean ignoreCase) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        String field = toField(path);

        if (Number.class.isAssignableFrom(operation.getArg(1).getType())) {
            @SuppressWarnings("unchecked")
            // guarded by previous check
            Constant rightArg = (Constant) operation
                    .getArg(1);
            return new TermQuery(new Term(field,
                    convertNumber(rightArg.getConstant())));
        }

        return eq(field, convert(path, operation.getArg(1), metadata),
                ignoreCase);
    }

    private BytesRef convertNumber(Number number) {
        if (Integer.class.isInstance(number) || Byte.class.isInstance(number)
                || Short.class.isInstance(number)) {
            BytesRefBuilder ref = new BytesRefBuilder();
            NumericUtils.intToPrefixCoded(number.intValue(), 0, ref);
            return ref.get();
        } else if (Double.class.isInstance(number)
                || BigDecimal.class.isInstance(number)) {
            BytesRefBuilder ref = new BytesRefBuilder();
            long l = NumericUtils.doubleToSortableLong(number.doubleValue());
            NumericUtils.longToPrefixCoded(l, 0, ref);
            return ref.get();
        } else if (Long.class.isInstance(number)
                || BigInteger.class.isInstance(number)) {
            BytesRefBuilder ref = new BytesRefBuilder();
            NumericUtils.longToPrefixCoded(number.longValue(), 0, ref);
            return ref.get();
        } else if (Float.class.isInstance(number)) {
            BytesRefBuilder ref = new BytesRefBuilder();
            int i = NumericUtils.floatToSortableInt(number.floatValue());
            NumericUtils.intToPrefixCoded(i, 0, ref);
            return ref.get();
        } else {
            throw new IllegalArgumentException("Unsupported numeric type "
                    + number.getClass().getName());
        }
    }

    protected Query eq(String field, String[] terms, boolean ignoreCase) {
        if (terms.length > 1) {
            PhraseQuery pq = new PhraseQuery();
            for (String s : terms) {
                pq.add(new Term(field, s));
            }
            return pq;
        }
        return new TermQuery(new Term(field, terms[0]));
    }

    protected Query in(Operation operation, QueryMetadata metadata,
            boolean ignoreCase) {
        Path path = getPath(operation.getArg(0));
        String field = toField(path);
        @SuppressWarnings("unchecked")
        // this is the expected type
        Constant> expectedConstant = (Constant>) operation
                .getArg(1);
        Collection values = expectedConstant.getConstant();
        BooleanQuery bq = new BooleanQuery();
        if (Number.class.isAssignableFrom(path.getType())) {
            for (Object value : values) {
                TermQuery eq = new TermQuery(new Term(field, convertNumber((Number) value)));
                bq.add(eq, Occur.SHOULD);
            }
        } else {
            for (Object value : values) {
                String[] str = convert(path, value);
                bq.add(eq(field, str, ignoreCase), Occur.SHOULD);
            }
        }
        return bq;
    }

    protected Query notIn(Operation operation, QueryMetadata metadata,
            boolean ignoreCase) {
        BooleanQuery bq = new BooleanQuery();
        bq.add(new BooleanClause(in(operation, metadata, false), Occur.MUST_NOT));
        bq.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST));
        return bq;
    }

    protected Query ne(Operation operation, QueryMetadata metadata,
            boolean ignoreCase) {
        BooleanQuery bq = new BooleanQuery();
        bq.add(new BooleanClause(eq(operation, metadata, ignoreCase),
                Occur.MUST_NOT));
        bq.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST));
        return bq;
    }

    protected Query startsWith(QueryMetadata metadata, Operation operation,
            boolean ignoreCase) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        String field = toField(path);
        String[] terms = convertEscaped(path, operation.getArg(1), metadata);
        if (terms.length > 1) {
            BooleanQuery bq = new BooleanQuery();
            for (int i = 0; i < terms.length; ++i) {
                String s = i == 0 ? terms[i] + "*" : "*" + terms[i] + "*";
                bq.add(new WildcardQuery(new Term(field, s)), Occur.MUST);
            }
            return bq;
        }
        return new PrefixQuery(new Term(field, terms[0]));
    }

    protected Query stringContains(Operation operation,
            QueryMetadata metadata, boolean ignoreCase) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        String field = toField(path);
        String[] terms = convertEscaped(path, operation.getArg(1), metadata);
        if (terms.length > 1) {
            BooleanQuery bq = new BooleanQuery();
            for (String s : terms) {
                bq.add(new WildcardQuery(new Term(field, "*" + s + "*")),
                        Occur.MUST);
            }
            return bq;
        }
        return new WildcardQuery(new Term(field, "*" + terms[0] + "*"));
    }

    protected Query endsWith(Operation operation, QueryMetadata metadata,
            boolean ignoreCase) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        String field = toField(path);
        String[] terms = convertEscaped(path, operation.getArg(1), metadata);
        if (terms.length > 1) {
            BooleanQuery bq = new BooleanQuery();
            for (int i = 0; i < terms.length; ++i) {
                String s = i == terms.length - 1 ? "*" + terms[i] : "*"
                        + terms[i] + "*";
                bq.add(new WildcardQuery(new Term(field, s)), Occur.MUST);
            }
            return bq;
        }
        return new WildcardQuery(new Term(field, "*" + terms[0]));
    }

    protected Query between(Operation operation, QueryMetadata metadata) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        // TODO Phrase not properly supported
        return range(path, toField(path), operation.getArg(1),
                operation.getArg(2), true, true, metadata);
    }

    protected Query lt(Operation operation, QueryMetadata metadata) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        return range(path, toField(path), null, operation.getArg(1), false,
                false, metadata);
    }

    protected Query gt(Operation operation, QueryMetadata metadata) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        return range(path, toField(path), operation.getArg(1), null, false,
                false, metadata);
    }

    protected Query le(Operation operation, QueryMetadata metadata) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        return range(path, toField(path), null, operation.getArg(1), true,
                true, metadata);
    }

    protected Query ge(Operation operation, QueryMetadata metadata) {
        verifyArguments(operation);
        Path path = getPath(operation.getArg(0));
        return range(path, toField(path), operation.getArg(1), null, true,
                true, metadata);
    }

    protected Query range(Path leftHandSide, String field,
            @Nullable Expression min, @Nullable Expression max,
            boolean minInc, boolean maxInc, QueryMetadata metadata) {
        if (min != null && Number.class.isAssignableFrom(min.getType())
                || max != null && Number.class.isAssignableFrom(max.getType())) {
            @SuppressWarnings("unchecked")
            // guarded by previous check
            Constant minConstant = (Constant) min;
            @SuppressWarnings("unchecked")
            // guarded by previous check
            Constant maxConstant = (Constant) max;

            Class numType = minConstant != null ? minConstant
                    .getType() : maxConstant.getType();
            // this is not necessarily safe, but compile time checking
            // on the user side mandates these types to be interchangeable
            @SuppressWarnings("unchecked")
            Class unboundedNumType = (Class) numType;
            return numericRange(unboundedNumType, field,
                    minConstant == null ? null : minConstant.getConstant(),
                    maxConstant == null ? null : maxConstant.getConstant(),
                    minInc, maxInc);
        }
        return stringRange(leftHandSide, field, min, max, minInc, maxInc,
                metadata);
    }

    protected  NumericRangeQuery numericRange(
            Class clazz, String field, @Nullable N min, @Nullable N max,
            boolean minInc, boolean maxInc) {
        if (clazz.equals(Integer.class)) {
            return NumericRangeQuery.newIntRange(field, (Integer) min,
                    (Integer) max, minInc, maxInc);
        } else if (clazz.equals(Double.class)) {
            return NumericRangeQuery.newDoubleRange(field, (Double) min,
                    (Double) max, minInc, minInc);
        } else if (clazz.equals(Float.class)) {
            return NumericRangeQuery.newFloatRange(field, (Float) min,
                    (Float) max, minInc, minInc);
        } else if (clazz.equals(Long.class)) {
            return NumericRangeQuery.newLongRange(field, (Long) min,
                    (Long) max, minInc, minInc);
        } else if (clazz.equals(Byte.class) || clazz.equals(Short.class)) {
            return NumericRangeQuery.newIntRange(field,
                    min != null ? min.intValue() : null,
                    max != null ? max.intValue() : null, minInc, maxInc);
        } else {
            throw new IllegalArgumentException("Unsupported numeric type "
                    + clazz.getName());
        }
    }

    protected Query stringRange(Path leftHandSide, String field,
            @Nullable Expression min, @Nullable Expression max,
            boolean minInc, boolean maxInc, QueryMetadata metadata) {

        if (min == null) {
            return TermRangeQuery.newStringRange(field, null,
                    convert(leftHandSide, max, metadata)[0], minInc, maxInc);
        } else if (max == null) {
            return TermRangeQuery.newStringRange(field,
                    convert(leftHandSide, min, metadata)[0], null, minInc,
                    maxInc);
        } else {
            return TermRangeQuery.newStringRange(field,
                    convert(leftHandSide, min, metadata)[0],
                    convert(leftHandSide, max, metadata)[0], minInc, maxInc);
        }
    }

    private Path getPath(Expression leftHandSide) {
        if (leftHandSide instanceof Path) {
            return (Path) leftHandSide;
        } else if (leftHandSide instanceof Operation) {
            Operation operation = (Operation) leftHandSide;
            if (operation.getOperator() == Ops.LOWER
                    || operation.getOperator() == Ops.UPPER) {
                return (Path) operation.getArg(0);
            }
        }
        throw new IllegalArgumentException("Unable to transform "
                + leftHandSide + " to path");
    }

    /**
     * template method, override to customize
     *
     * @param path
     *            path
     * @return field name
     */
    protected String toField(Path path) {
        PathMetadata md = path.getMetadata();
        if (md.getPathType() == PathType.COLLECTION_ANY) {
            return toField(md.getParent());
        } else {
            String rv = md.getName();
            if (md.getParent() != null) {
                Path parent = md.getParent();
                if (parent.getMetadata().getPathType() != PathType.VARIABLE) {
                    rv = toField(parent) + "." + rv;
                }
            }
            return rv;
        }
    }

    private void verifyArguments(Operation operation) {
        List> arguments = operation.getArgs();
        for (int i = 1; i < arguments.size(); ++i) {
            if (!(arguments.get(i) instanceof Constant)
                    && !(arguments.get(i) instanceof ParamExpression)
                    && !(arguments.get(i) instanceof PhraseElement)
                    && !(arguments.get(i) instanceof TermElement)) {
                throw new IllegalArgumentException(
                        "operand was of unsupported type "
                                + arguments.get(i).getClass().getName());
            }
        }
    }

    /**
     * template method
     *
     * @param leftHandSide
     *            left hand side
     * @param rightHandSide
     *            right hand side
     * @return results
     */
    protected String[] convert(Path leftHandSide,
            Expression rightHandSide, QueryMetadata metadata) {
        if (rightHandSide instanceof Operation) {
            Operation operation = (Operation) rightHandSide;
            if (operation.getOperator() == LuceneOps.PHRASE) {
                return Iterables.toArray(
                        WS_SPLITTER.split(operation.getArg(0).toString()),
                        String.class);
            } else if (operation.getOperator() == LuceneOps.TERM) {
                return new String[] {operation.getArg(0).toString() };
            } else {
                throw new IllegalArgumentException(rightHandSide.toString());
            }
        } else if (rightHandSide instanceof ParamExpression) {
            Object value = metadata.getParams().get(rightHandSide);
            if (value == null) {
                throw new ParamNotSetException(
                        (ParamExpression) rightHandSide);
            }
            return convert(leftHandSide, value);

        } else if (rightHandSide instanceof Constant) {
            return convert(leftHandSide,
                    ((Constant) rightHandSide).getConstant());
        } else {
            throw new IllegalArgumentException(rightHandSide.toString());
        }
    }

    /**
     * template method
     *
     * @param leftHandSide
     *            left hand side
     * @param rightHandSide
     *            right hand side
     * @return results
     */
    protected String[] convert(Path leftHandSide, Object rightHandSide) {
        String str = rightHandSide.toString();
        if (lowerCase) {
            str = str.toLowerCase();
        }
        if (splitTerms) {
            if (str.equals("")) {
                return new String[] {str };
            } else {
                return Iterables.toArray(WS_SPLITTER.split(str), String.class);
            }
        } else {
            return new String[] {str };
        }
    }

    private String[] convertEscaped(Path leftHandSide,
            Expression rightHandSide, QueryMetadata metadata) {
        String[] str = convert(leftHandSide, rightHandSide, metadata);
        for (int i = 0; i < str.length; i++) {
            str[i] = QueryParser.escape(str[i]);
        }
        return str;
    }

    public Query toQuery(Expression expr, QueryMetadata metadata) {
        if (expr instanceof Operation) {
            return toQuery((Operation) expr, metadata);
        } else {
            return toQuery(ExpressionUtils.extract(expr), metadata);
        }
    }

    public Sort toSort(List> orderBys) {
        List sorts = new ArrayList(orderBys.size());
        for (OrderSpecifier order : orderBys) {
            if (!(order.getTarget() instanceof Path)) {
                throw new IllegalArgumentException(
                        "argument was not of type Path.");
            }
            Class type = order.getTarget().getType();
            boolean reverse = !order.isAscending();
            Path path = getPath(order.getTarget());
            if (Number.class.isAssignableFrom(type)) {
                sorts.add(new SortedNumericSortField(toField(path), sortFields.get(type),
                        reverse));
            } else {
                sorts.add(new SortField(toField(path), SortField.Type.STRING,
                        reverse));
            }
        }
        Sort sort = new Sort();
        sort.setSort(sorts.toArray(new SortField[sorts.size()]));
        return sort;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy