Sesat > Docs + Support > Architecture Overview > Design Proposals > New design proposal for SearchCommand and AbstractSearchCommand > QueryBuilder code example

QueryBuilder interface

/** QueryBuilder provides a string representation of a Query Tree against of map of "transformed terms".
 *
 * It is similar in functionality to the QueryTransformer interface
 *  except that it does not transform terms but uses them to build the final string representation.
 */
public interface QueryBuilder extends Visitor {

    interface Context extends QueryContext, ResourceContext, SiteContext, DataModelContext{

        /**
         * Get the terms with their current transformed representations.
         * @return
         */
        Map<Clause, String> getTransformedTerms();

        /** Get the unescaped transformed term for the clause.
         *
         * @param clause
         * @return unescaped transformed term
         */
        String getTransformedTerm(Clause clause);

        /**
         * For evaluation acitions on individual (or the whole query) terms.
         * @return
         */
        TokenEvaluationEngine getTokenEvaluationEngine();

        /**
         * QueryTransformers must follow the same XorClause hints as the search command. *
         * @param visitor
         * @param clause
         */
        void visitXorClause(Visitor visitor, XorClause clause);

        /**
         * QueryTransformers needs information about supported field filters. *
         * @param clause
         * @return
         */
        String getFieldFilter(LeafClause clause);

        /** The collection of words that have special meaning/function within the query string
         *
         * @return collection of reserved words
         */
        Collection<String> getReservedWords();

        /** Escape the word.
         * The word need not be reserved or require escaping but should be escaped anyway.
         *
         * @param word
         * @return escaped version of the word
         */
        String escape(String word);
    }

    /** The Query String built from the Query's transformed clauses
     * 
     * @param query
     * @return string built from the Query's transformed clauses
     */
    String getQueryString(Query query);
}

Abstract QueryBuilder

/** Helper abstract implementation handling stringBuilder functionality behind the visitor pattern. **/
public abstract class AbstractQueryBuilder extends AbstractReflectionVisitor implements QueryBuilder {

    // Attributes ----------------------------------------------------
    
    private final Context context;
    private final QueryBuilderConfig config;
    private final StringBuilder sb = new StringBuilder(128);

    // Constructors --------------------------------------------------

    public AbstractQueryBuilder(final Context cxt, QueryBuilderConfig config) {

        context = cxt;
        this.config = config;
    }

    // Public --------------------------------------------------------
    
    public String getQueryString() {
        
        final Clause root = context.getQuery().getRootClause();
        sb.setLength(0);
        visit(root);
        return sb.toString().trim();
    }
    
    // Protected -----------------------------------------------------

    protected final Context getContext(){
        return context;
    }

    protected QueryBuilderConfig getConfig(){
        return config;
    }

    /** Gets the transformed term, escaping any reserved words. 
     * 
     * @param clause
     * @return
     */
    protected String getEscapedTransformedTerm(final Clause clause){
        
        return escape(context.getTransformedTerm(clause).toLowerCase());

    }
    
    /** Escapes any reserved words (including those fielded).
     * Case-insensitive.
     *
     * How to actually escape any matching words is left to the context to define via context.escape(word)
     * 
     * @param string
     * @return possibilly escaped string
     */
    protected String escape(final String string){
        
        for (String word : getWordsToEscape()) {

            // Case-insensitive check against word.
            // Term might already be prefixed by the TermPrefixTransformer.
            if (string.toLowerCase().endsWith(':' + word.toLowerCase()) || string.equalsIgnoreCase(word)) {

                final Pattern p = Pattern.compile(
                        Matcher.quoteReplacement(word),
                        Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);
                
                return p.matcher(word).replaceAll(context.escape(string));
            }
        }

        return string;
    }

    protected Collection<String> getWordsToEscape(){
        return context.getReservedWords();
    }
    
    protected final void appendToQueryRepresentation(final CharSequence addition) {
        sb.append(addition);
    }

    protected final void appendToQueryRepresentation(final char addition) {
        sb.append(addition);
    }

    protected final int getQueryRepresentationLength() {
        return sb.length();
    }

    protected final void insertToQueryRepresentation(final int offset, final CharSequence addition) {
        sb.insert(offset, addition);
    }

    protected boolean isEmptyLeaf(final Clause clause) {
        return false;
    }

    protected boolean isEmptyLeaf(final LeafClause clause) {
        
        final String tt = 0 == context.getTransformedTerm(clause).length()
                ? null
                : context.getTransformedTerm(clause);
        
        return
                // no field and a valid term
                null == clause.getField() && null == tt
                // or, a field that is an accepted filter
                || null != clause.getField() && null != context.getFieldFilter(clause);

    }
    
    protected boolean isEmptyLeaf(final DoubleOperatorClause clause) {

        return isEmptyLeaf(clause.getFirstClause()) && isEmptyLeaf(clause.getSecondClause());

    }
    
    protected void visitImpl(final XorClause clause) {
        getContext().visitXorClause(this, clause);
    }
}

Infix QueryBuilder implementation

/** 
 * Largely mimics the Query tree layout replacing OperatorClauses with the RESERVED_WORDS.
 */
public class InfixQueryBuilder extends AbstractQueryBuilder{

    // Constructors --------------------------------------------------

    public InfixQueryBuilder(final Context cxt, final QueryBuilderConfig config) {
        super(cxt, config);
    }

    // protected ----------------------------------------------
    
    
    @Override
    protected InfixQueryBuilderConfig getConfig() {
        return (InfixQueryBuilderConfig) super.getConfig();
    }

    @Override
    protected Collection<String> getWordsToEscape() {

        final Collection<String> words = new HashSet<String>(super.getWordsToEscape());

        words.add(getConfig().getAndInfix());
        words.add(getConfig().getNotPrefix());
        words.add(getConfig().getOrInfix());

        return words;
    }


    protected void visitImpl(final LeafClause clause) {

        appendToQueryRepresentation(getEscapedTransformedTerm(clause));
    }

    protected void visitImpl(final OperationClause clause) {

        clause.getFirstClause().accept(this);
    }

    protected void visitImpl(final AndClause clause) {

        clause.getFirstClause().accept(this);
        appendToQueryRepresentation(' ' + getConfig().getAndInfix() + ' ');
        clause.getSecondClause().accept(this);
    }

    protected void visitImpl(final OrClause clause) {

        clause.getFirstClause().accept(this);
        appendToQueryRepresentation(' ' + getConfig().getOrInfix() + ' ');
        clause.getSecondClause().accept(this);
    }

    protected void visitImpl(final DefaultOperatorClause clause) {

        clause.getFirstClause().accept(this);
        appendToQueryRepresentation(' ');
        clause.getSecondClause().accept(this);
    }

    protected void visitImpl(final NotClause clause) {

        final String childsTerm = getEscapedTransformedTerm(clause.getFirstClause());
        if (childsTerm != null && childsTerm.length() > 0) {
            appendToQueryRepresentation(getConfig().getNotPrefix());
            clause.getFirstClause().accept(this);
        }
    }

    protected void visitImpl(final AndNotClause clause) {

        final String childsTerm = getEscapedTransformedTerm(clause.getFirstClause());
        if (childsTerm != null && childsTerm.length() > 0) {
            appendToQueryRepresentation(getConfig().getNotPrefix());
            clause.getFirstClause().accept(this);
        }
    }
}

Prefix QueryBuilder implementation

/** QueryBuilder prefixing terms depending on their inclusion/exclusion.
 */
public class PrefixQueryBuilder extends AbstractQueryBuilder{

    // Attributes ----------------------------------------------------

    // third state variable. TRUE --> must have clause, FALSE --> must not have clause, null --> optional clause.
    private Boolean clauseState = Boolean.TRUE;
    
    // Constructors --------------------------------------------------

    public PrefixQueryBuilder(final Context cxt, final QueryBuilderConfig config) {
        super(cxt, config);
    }

    // Protected -----------------------------------------------------


    @Override
    protected PrefixQueryBuilderConfig getConfig() {
        return (PrefixQueryBuilderConfig) super.getConfig();
    }

    @Override
    protected Collection<String> getWordsToEscape() {

        final Collection<String> words = new HashSet<String>(super.getWordsToEscape());

        words.add(getConfig().getAndPrefix());
        words.add(getConfig().getNotPrefix());
        words.add(getConfig().getOrPrefix());

        return words;
    }

    protected void visitImpl(final LeafClause clause) {

        insertClauseStatePrefix(clause);
        super.visitImpl(clause);
    }

    protected void visitImpl(final PhraseClause clause) {

        if (clause.getField() == null) {
            insertClauseStatePrefix(clause);
            appendToQueryRepresentation(getEscapedTransformedTerm(clause));
        }
    }

    protected void visitImpl(final AndClause clause) {

        clauseState = Boolean.TRUE;
        clause.getFirstClause().accept(this);
        appendToQueryRepresentation(' ');
        clauseState = Boolean.TRUE;
        clause.getSecondClause().accept(this);
    }

    protected void visitImpl(final OrClause clause) {

        clauseState = null;
        clause.getFirstClause().accept(this);
        appendToQueryRepresentation(' ');
        clauseState = null;
        clause.getSecondClause().accept(this);
    }

    protected void visitImpl(final DefaultOperatorClause clause) {

        clauseState = Boolean.TRUE;
        clause.getFirstClause().accept(this);
        appendToQueryRepresentation(' ');
        clauseState = Boolean.TRUE;
        clause.getSecondClause().accept(this);
    }

    protected void visitImpl(final NotClause clause) {

        if(getConfig().getSupportsNot()){
            final String childsTerm = getEscapedTransformedTerm(clause.getFirstClause());
            if (childsTerm != null && childsTerm.length() > 0) {
                clauseState = Boolean.FALSE;
                clause.getFirstClause().accept(this);
            }
        }
    }

    protected void visitImpl(final AndNotClause clause) {

        if(getConfig().getSupportsNot()){
            final String childsTerm = getEscapedTransformedTerm(clause.getFirstClause());
            if (childsTerm != null && childsTerm.length() > 0) {
                clauseState = Boolean.FALSE;
                clause.getFirstClause().accept(this);
            }
        }
    }

    private void insertClauseStatePrefix(final Clause clause){

        if(!isEmptyLeaf(clause)){
            if(Boolean.TRUE == clauseState){
                appendToQueryRepresentation(getConfig().getAndPrefix());
            }else if(Boolean.FALSE == clauseState){
                appendToQueryRepresentation(getConfig().getNotPrefix());
            }else{
                appendToQueryRepresentation(getConfig().getOrPrefix());
            }
        }
    }
}

Sesam Syntax QueryBuilder implementation

/** Query builder for creating a query syntax similar to sesam's own.
 * Currently is basically a PrefixQueryBuilder with OrClauses wrapped in () parenthesis.
 */
public class SesamSyntaxQueryBuilder extends PrefixQueryBuilder{

    // Constructors --------------------------------------------------

    public SesamSyntaxQueryBuilder(final Context cxt, final QueryBuilderConfig config) {
        super(cxt, config);
    }

    // AbstractReflectionVisitor implementation ----------------------------------------------

    private boolean insideOr = false;

    @Override
    protected void visitImpl(final OrClause clause) {

        boolean wasInside = insideOr;
        if (!insideOr) {
            appendToQueryRepresentation('(');
        }
        insideOr = true;
        super.visitImpl(clause);
        insideOr = wasInside;
        if (!insideOr) {
            appendToQueryRepresentation(')');
        }
    }

    /** Overridden so to avoid visiting any FULLNAME_ON_LEFT.
     */
    @Override
    protected void visitImpl(final XorClause clause) {

        switch (clause.getHint()) {
            case FULLNAME_ON_LEFT:
                clause.getSecondClause().accept(this);
                break;
            default:
                super.visitImpl( clause);
        }
    }
    
}
 © 2007-2009 Schibsted ASA
Contact us