| Sesat > Docs + Support > Architecture Overview > Design Proposals > New design proposal for SearchCommand and AbstractSearchCommand > QueryBuilder code example |
/** 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); }
/** 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); } }
/** * 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); } } }
/** 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()); } } } }
/** 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); } } }