/* Copyright (2008-2012) Schibsted ASA
* This file is part of Possom.
*
* Possom is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Possom 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Possom. If not, see <http://www.gnu.org/licenses/>.
*/
package no.sesat.search.mode.command.querybuilder;
import java.util.Collection;
import java.util.regex.Pattern;
import no.sesat.search.mode.config.querybuilder.QueryBuilderConfig;
import no.sesat.search.query.Clause;
import no.sesat.search.query.BinaryClause;
import no.sesat.search.query.LeafClause;
import no.sesat.search.query.NotClause;
import no.sesat.search.query.UnaryClause;
import no.sesat.search.query.XorClause;
import no.sesat.commons.visitor.AbstractReflectionVisitor;
/** Abstract QueryBuilder providing basic support for mantaining context and stringBuilder fields (and related methods).
*
* <br/><br/>
*
* Any instance will have getQuery(..) called multiple times.
* It is therefore paramount that inside this method state fields are reset before the visitor is visited. <br/>
* For example a subclass will override getQuery(..) such:
* <pre>
* @Override
* public String getQueryString() {
* // myVariable needs to be reset before every visit.
* myVariable = 0;
* return super.getQueryString();
* }
* </pre>
*
* @version $Id$
*/
public abstract class AbstractQueryBuilder extends AbstractReflectionVisitor implements QueryBuilder {
// Constants -----------------------------------------------------
//private static final Logger LOG = Logger.getLogger(AbstractQueryBuilder.class);
// Attributes ----------------------------------------------------
private final Context context;
private final QueryBuilderConfig config;
private final StringBuilder sb = new StringBuilder(128);
// Static --------------------------------------------------------
// Constructors --------------------------------------------------
public AbstractQueryBuilder(final Context cxt, QueryBuilderConfig config) {
context = cxt;
this.config = config;
}
// Public --------------------------------------------------------
@Override
public String getQueryString() {
final Clause root = context.getQuery().getRootClause();
sb.setLength(0);
visit(root);
return sb.toString().trim();
}
// Package protected ---------------------------------------------
// 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));
}
/** 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()) {
word = Pattern.quote(word);
// Case-insensitive check against word.
// Term might already be prefixed by a field.
final String regexp = "(.*:)?" + word;
if (string.toLowerCase().matches(regexp)) {
final Pattern p = Pattern.compile(word, Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);
final String term = string.contains(":") && !string.contains("\\:")
? string.substring(string.indexOf(':') + 1)
: string;
return p.matcher(string).replaceAll(context.escape(term));
}
}
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) {
boolean result = false;
// FIXME handle XorClause to call isEmptyLeaf on only the child that getContext().visitXorClause(..) does.
// if(clause instanceof XorClause){}else{
if(clause instanceof BinaryClause){
final BinaryClause c = (BinaryClause)clause;
result = isEmptyLeaf(c.getFirstClause()) && isEmptyLeaf(c.getSecondClause());
}else if(clause instanceof UnaryClause){
final UnaryClause c = (UnaryClause)clause;
result = isEmptyLeaf(c.getFirstClause());
}else if(clause instanceof LeafClause){
final LeafClause c = (LeafClause)clause;
final String tt = 0 == context.getTransformedTerm(c).length()
? null
: context.getTransformedTerm(c);
result =
// no field and a valid term
null == c.getField() && null == tt
// or, a field that is an accepted filter
|| null != c.getField() && null != context.getFieldFilter(c);
}
return result;
}
protected boolean isNextLeafInsideNotClause(final Clause clause){
boolean result = false;
if(clause instanceof NotClause){
result = !isEmptyLeaf(clause);
}else if(clause instanceof UnaryClause){
result = isNextLeafInsideNotClause(((UnaryClause)clause).getFirstClause());
}
return result;
}
protected void visitImpl(final XorClause clause) {
getContext().visitXorClause(this, clause);
}
// Private -------------------------------------------------------
// Inner classes -------------------------------------------------
}