Java tutorial
/* Copyright (2006-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; import java.util.Properties; import javax.xml.parsers.DocumentBuilder; import no.sesat.search.mode.command.querybuilder.BaseFilterBuilder; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.UndeclaredThrowableException; import java.util.Collection; import java.util.Collections; import no.sesat.commons.ioc.BaseContext; import no.sesat.commons.ioc.ContextWrapper; import no.sesat.search.datamodel.DataModel; import no.sesat.search.datamodel.generic.StringDataObject; import no.sesat.search.mode.config.BaseSearchConfiguration; import no.sesat.search.query.Clause; import no.sesat.search.query.LeafClause; import no.sesat.search.query.Query; import no.sesat.commons.visitor.Visitor; import no.sesat.search.query.XorClause; import no.sesat.search.query.parser.AbstractQueryParserContext; import no.sesat.search.query.parser.QueryParser; import no.sesat.search.query.parser.QueryParserImpl; import no.sesat.search.query.parser.TokenMgrError; import no.sesat.search.query.token.TokenEvaluationEngine; import no.sesat.search.query.token.TokenEvaluationEngineImpl; import no.sesat.search.query.token.TokenPredicate; import no.sesat.search.query.transform.QueryTransformer; import no.sesat.search.query.transform.QueryTransformerConfig; import no.sesat.search.query.transform.QueryTransformerFactory; import no.sesat.search.result.BasicResultList; import no.sesat.search.result.ResultItem; import no.sesat.search.result.ResultList; import no.sesat.search.result.handler.DataModelResultHandler; import no.sesat.search.result.handler.ResultHandler; import no.sesat.search.result.handler.ResultHandlerConfig; import no.sesat.search.result.handler.ResultHandlerFactory; import no.sesat.search.site.Site; import no.sesat.search.site.SiteContext; import no.sesat.search.site.config.BytecodeLoader; import no.sesat.search.site.config.DocumentLoader; import no.sesat.search.site.config.PropertiesLoader; import no.sesat.search.view.config.SearchTab; import org.apache.commons.lang.time.StopWatch; import org.apache.log4j.Logger; import org.apache.log4j.MDC; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import no.sesat.search.datamodel.access.DataModelAccessException; import no.sesat.search.mode.command.querybuilder.FilterBuilder; import no.sesat.search.mode.command.querybuilder.QueryBuilder; import no.sesat.search.mode.command.querybuilder.SesamSyntaxQueryBuilder; import no.sesat.search.mode.config.querybuilder.QueryBuilderConfig; import no.sesat.search.mode.config.querybuilder.QueryBuilderConfig.Controller; import no.sesat.search.query.token.DeadTokenEvaluationEngineImpl; import no.sesat.search.query.token.EvaluationException; import no.sesat.search.query.token.TokenPredicateUtility; import no.sesat.search.site.config.SiteClassLoaderFactory; import no.sesat.search.site.config.Spi; /** The base abstraction for Search Commands providing a large framework for commands to run against. * <br/><br/> * While the SearchCommand interface defines basic execution behavour this abstraction defines:<ul> * <li>delegation of the call method to the execute method so to provide a default implementation for handling * cancellations, thread renaming during execution, and avoidance of execution on blank queries, * <li>assigned queryBuilder and filterBuilder to express the query and filter as the index's requires,</li> * <li>delegation to the appropriate query to use (sometimes not the user's query),</li> * <li>handling and control of the query transformations as defined in the commands config,</li> * <li>handling and control of the result handlers as defined in the commands config,</li> * <li>helper methods, beyond the query transformers, for filter (and advanced-filter) construction,</li> * <li>assigned displayableQueryBuilder for constructing a user presentable version of the transformed query, * that in turn can be parsed again by sesat's query parser to return the same query.</li> * </ul> * <br/><br/> * * This command undertook a large refactoring in 2.18 to clean up internal concerns. * See the specification {@link http://sesat.no/new-design-proposal-for-searchcommand-and-abstractsearchcommand.html} * * @version <tt>$Id$</tt> */ public abstract class AbstractSearchCommand implements SearchCommand, Serializable { // Constants ----------------------------------------------------- private static final DataModelResultHandler DATAMODEL_HANDLER = new DataModelResultHandler(); private static final Logger LOG = Logger.getLogger(AbstractSearchCommand.class); protected static final Logger DUMP = Logger.getLogger("no.sesat.search.Dump"); private static final String ERR_PARSING = "Unable to create RunningQuery's query due to ParseException"; private static final String ERR_TRANSFORMED_QUERY_USED = "Cannot use transformedTerms Map once deprecated getTransformedQuery as been used"; private static final String ERR_HANDLING_CANCELLATION = "Cancellation (and now handling of) occurred to "; private static final String ERROR_RUNTIME = "RuntimeException occurred"; private static final String TRACE_NOT_TOKEN_PREDICATE = "Not a TokenPredicate "; // Attributes ---------------------------------------------------- /** The context to work against. */ protected transient final Context context; /** Assigned by initialiseQuery(). **/ private transient Query query = null; private transient TokenEvaluationEngine engine = null; private transient final QueryTransformerFactory.Context qtfContext; private transient final QueryBuilder.Context queryBuilderContext; private transient final QueryTransformer initialQueryTransformer; private transient final QueryBuilder queryBuilder; private transient final SesamSyntaxQueryBuilder displayableQueryBuilder; private transient final FilterBuilder filterBuilder; private transient final BaseSearchConfiguration baseSearchConfiguration; protected final String untransformedQuery; protected transient final DataModel datamodel; protected transient final Map<String, StringDataObject> datamodelParameters; private final Map<Clause, String> transformedTerms = new LinkedHashMap<Clause, String>(); private String transformedQuery; private String transformedQuerySesamSyntax; private final SearchCommandParameter offsetParameter; private final SearchCommandParameter userSortByParameter; // thread execution handling protected volatile boolean completed = false; // thread execution handling private volatile Thread thread = null; // Static -------------------------------------------------------- // Constructors -------------------------------------------------- /** Default constructor. Only constructor. * @param cxt The context to execute in. */ public AbstractSearchCommand(final SearchCommand.Context cxt) { LOG.trace("AbstractSearchCommand()"); assert null != cxt.getDataModel() : "Not allowed to pass in null datamodel"; assert null != cxt.getDataModel().getQuery() : "Not allowed to pass in null datamodel.query"; this.context = cxt; this.datamodel = cxt.getDataModel(); this.baseSearchConfiguration = (BaseSearchConfiguration) cxt.getSearchConfiguration(); this.datamodelParameters = Collections.unmodifiableMap(datamodel.getParameters().getValues()); // do not use this.getSearchConfiguration() in constructor -- it's a overridable method. final BaseSearchConfiguration bsc = (BaseSearchConfiguration) context.getSearchConfiguration(); initialiseQuery(); // getQuery() may be overridden so we need to be careful here transformedQuery = getQuery().getQueryString(); // A simple context for QueryTransformerFactory.Context qtfContext = new QueryTransformerFactory.Context() { @Override public Site getSite() { return context.getDataModel().getSite().getSite(); } @Override public BytecodeLoader newBytecodeLoader(final SiteContext site, final String name, final String jar) { return context.newBytecodeLoader(site, name, jar); } }; // Little more complicated context for QueryBuilder.Context (can be used for QueryTransformer.Context too) // dont use ContextWrapper.wrap(..) here as this context really gets hammered and we want to avoid reflection queryBuilderContext = new QueryBuilder.Context() { @Override public Site getSite() { return datamodel.getSite().getSite(); } /** @deprecated {@inheritDoc} **/ @Override public String getTransformedQuery() { return transformedQuery; } @Override public Query getQuery() { // Important that initialiseQuery() has been called first return getSearchCommandsQuery(); } @Override public TokenEvaluationEngine getTokenEvaluationEngine() { return engine; } @Override public void visitXorClause(final Visitor visitor, final XorClause clause) { searchCommandsVisitXorClause(visitor, clause); } @Override public String getFieldFilter(final LeafClause clause) { return getSearchCommandsFieldFilter(clause); } @Override public String getTransformedTerm(final Clause clause) { // unable to delegate to getTransformedTerm as it escapes reserved words // and we're not allowed to here final String transformedTerm = transformedTerms.get(clause); return null != transformedTerm ? transformedTerm : clause.getTerm(); } @Override public Collection<String> getReservedWords() { return getSearchCommandsReservedWords(); } @Override public String escape(final String word) { return searchCommandsEscape(word); } @Override public Map<Clause, String> getTransformedTerms() { return getSearchCommandsTransformedTerms(); } @Override public DocumentLoader newDocumentLoader(SiteContext siteCxt, String resource, DocumentBuilder builder) { return cxt.newDocumentLoader(siteCxt, resource, builder); } @Override public PropertiesLoader newPropertiesLoader(SiteContext siteCxt, String resource, Properties properties) { return cxt.newPropertiesLoader(siteCxt, resource, properties); } @Override public BytecodeLoader newBytecodeLoader(SiteContext siteContext, String className, String jarFileName) { return cxt.newBytecodeLoader(siteContext, className, jarFileName); } @Override public DataModel getDataModel() { return cxt.getDataModel(); } }; // construct the initialQueryTransformer and then initialise the map of transformed terms initialQueryTransformer = new QueryTransformerFactory(qtfContext) .getController(bsc.getInitialQueryTransformer()); initialQueryTransformer.setContext(queryBuilderContext); initialiseTransformedTerms(query); // construct the queryBuilder queryBuilder = constructQueryBuilder(cxt, queryBuilderContext); // construct the sesamSyntaxQueryBuilder displayableQueryBuilder = new SesamSyntaxQueryBuilder(queryBuilderContext, bsc); // FIXME (in 2.18) implement configuration lookup filterBuilder = new BaseFilterBuilder(queryBuilderContext, null); // run an initial queryBuilder run and store the untransformed resulting queryString. untransformedQuery = getQueryRepresentation(); // parameters offsetParameter = new NavigationSearchCommandParameter(context, getSearchConfiguration().getPagingParameter(), getSearchConfiguration().getPagingParameter(), BaseSearchCommandParameter.Origin.REQUEST); userSortByParameter = new NavigationSearchCommandParameter(context, getSearchConfiguration().getUserSortParameter(), getSearchConfiguration().getUserSortParameter(), BaseSearchCommandParameter.Origin.REQUEST); } /** Set (or reset) the transformed terms back to the state before any queryTransformers were run. * @param query the query that the transformedTerms map will be constructed from. This should match getQuery() */ protected final void initialiseTransformedTerms(final Query query) { initialQueryTransformer.visit(query.getRootClause()); } // Public -------------------------------------------------------- public abstract ResultList<ResultItem> execute(); /** * Use this always instead of datamodel.getQuery().getQuery() * because the command could be running off a different query string. * * @return */ public Query getQuery() { return query; } /** * Returns the query as it is after the query transformers and command specific query builder * have been applied to it. * * @return The transformed query. */ public String getTransformedQuery() { return transformedQuery; } @Override public String toString() { return getSearchConfiguration().getId() + ' ' + datamodel.getQuery().getString(); } // SearchCommand overrides --------------------------------------------------- @Override public BaseSearchConfiguration getSearchConfiguration() { return baseSearchConfiguration; } /** * Called by thread executor * * @return */ @Override public ResultList<ResultItem> call() { MDC.put(Site.NAME_KEY, datamodel.getSite().getSite().getName()); MDC.put("UNIQUE_ID", datamodel.getParameters().getUniqueId()); thread = Thread.currentThread(); final String t = thread.getName(); final String statName = getSearchConfiguration().getStatisticalName(); if (statName != null && statName.length() > 0) { Thread.currentThread().setName(t + " [" + getSearchConfiguration().getStatisticalName() + ']'); } else { Thread.currentThread().setName(t + " [" + getClass().getSimpleName() + ']'); } try { try { LOG.trace("call()"); performQueryTransformation(); checkForCancellation(); final ResultList<ResultItem> result = performExecution(); checkForCancellation(); performResultHandling(result); checkForCancellation(); completed = true; thread = null; return result; } catch (UndeclaredThrowableException ute) { if (ute.getCause() instanceof DataModelAccessException && isCancelled()) { // This is partially expected because the datamodel's // controlLevel would have moved on through the process stack. LOG.trace("Cancelled command threw underlying exception", ute.getCause()); return new BasicResultList<ResultItem>(); } throw ute; } } catch (RuntimeException rte) { LOG.error(ERROR_RUNTIME, rte); return new BasicResultList<ResultItem>(); } finally { // restore thread name Thread.currentThread().setName(t); } } /** * Handles cancelling the command. * Inserts an "-1" result list. And does the result handling on it. * Returns true if cancellation action was taken. */ @Override public synchronized boolean handleCancellation() { if (!completed) { LOG.error(ERR_HANDLING_CANCELLATION + getSearchConfiguration().getId() + " [" + getClass().getSimpleName() + ']'); if (null != thread) { thread.interrupt(); thread = null; } performResultHandling(new BasicResultList<ResultItem>()); } return !completed; } /** Has the command been cancelled. * Calling this method only makes sense once the call() method has been. **/ @Override public synchronized boolean isCancelled() { return null == thread && !completed; } // Protected ----------------------------------------------------- /** Construct from scratch, and return the query builder to use. * Default implementation returns the query builder that is configured from the BaseSearchConfiguration. * * <br/> * * This method is intended to be overridden, but it called from the constructor. * So it is important the overrides do not reference "this", * or any other fields as they will likely not be initialised yet. * * @param cxt search command's context * @param queryBuilderContext the query builder context * @return */ protected QueryBuilder constructQueryBuilder(final SearchCommand.Context cxt, final QueryBuilder.Context queryBuilderContext) { return QueryBuilderFactory.getController(queryBuilderContext, ((BaseSearchConfiguration) cxt.getSearchConfiguration()).getQueryBuilder()); } protected Collection<String> getReservedWords() { return Collections.<String>emptySet(); } /** * @param visitor * @param clause */ protected void visitXorClause(final Visitor visitor, final XorClause clause) { // determine which branch in the query-tree we want to use. // Both branches to a XorClause should never be used. switch (clause.getHint()) { default: clause.getFirstClause().accept(visitor); break; } } /** Get the results from another search command waiting if neccessary. * @param id * @param datamodel * @return * @throws java.lang.InterruptedException */ protected final ResultList<ResultItem> getSearchResult(final String id, final DataModel datamodel) throws InterruptedException { synchronized (datamodel.getSearches()) { while (null == datamodel.getSearch(id)) { // we're not going to hang around waiting if we've been already left out in the cold checkForCancellation(); // next line releases the monitor so it is possible to call this method from different threads datamodel.getSearches().wait(1000); } } return datamodel.getSearch(id).getResults(); } protected void performQueryTransformation() { applyQueryTransformers(getQuery(), getSearchConfiguration().getQueryTransformers()); } /** Handles the execution process. Will determine whether to call execute() and wrap it with timing info. * @return */ protected final ResultList<ResultItem> performExecution() { final StopWatch watch = new StopWatch(); watch.start(); final String notNullQuery = null != getTransformedQuery() ? getTransformedQuery().trim() : ""; Integer hitCount = null; try { // we will be executing the command IF there's a valid query or filter, // or if the configuration specifies that we should run anyway. boolean executeQuery = null != datamodel.getQuery() && "*".equals(datamodel.getQuery().getString()); executeQuery |= notNullQuery.length() > 0 || getSearchConfiguration().isRunBlank(); executeQuery |= null != getFilter() && 0 < getFilter().length(); LOG.info("executeQuery==" + executeQuery + " ; query:" + notNullQuery + " ; filter:" + getFilter()); final ResultList<ResultItem> result = executeQuery ? execute() : new BasicResultList<ResultItem>(); if (!executeQuery) { // sent hit count to zero since we have intentionally avoiding searching. result.setHitCount(0); } hitCount = result.getHitCount(); LOG.debug("Hits is " + getSearchConfiguration().getId() + ':' + hitCount); return result; } finally { watch.stop(); LOG.info("Search " + getSearchConfiguration().getId() + " took " + watch); statisticsInfo("<search-command id=\"" + getSearchConfiguration().getId() + "\" name=\"" + getSearchConfiguration().getStatisticalName() + "\" type=\"" + getClass().getSimpleName() + "\">" + (hitCount != null ? "<hits>" + hitCount + "</hits>" : "<failure/>") + "<time>" + watch + "</time>" + "</search-command>"); } } /** * Perform (delegating out to) all registered result handlers for this command. * Also performs some hardcoded result handling, eg DataModelResultHandler. * * @param result */ protected final void performResultHandling(final ResultList<ResultItem> result) { // Build the context each result handler will need. final ResultHandler.Context resultHandlerContext = ContextWrapper.wrap(ResultHandler.Context.class, new BaseContext() { public Site getSite() { return context.getDataModel().getSite().getSite(); } public ResultList<ResultItem> getSearchResult() { return result; } public SearchTab getSearchTab() { return datamodel.getPage().getCurrentTab(); } public Query getQuery() { return getSearchCommandsQuery(); } public String getDisplayQuery() { return getTransformedQuerySesamSyntax(); } }, context); // process listed result handlers for (ResultHandlerConfig resultHandlerConfig : getSearchConfiguration().getResultHandlers()) { ResultHandlerFactory.getController(resultHandlerContext, resultHandlerConfig) .handleResult(resultHandlerContext, datamodel); } // The DataModel result handler is a hardcoded feature of the architecture DATAMODEL_HANDLER.handleResult(resultHandlerContext, datamodel); } /** * Returns the offset in the result set. If paging is enabled for the * current search configuration the offset to the current page will be * added to the parameter. * * @param i the current offset. * @return i plus the offset of the current page. * * @deprecated instead use getOffset() + i */ protected int getCurrentOffset(final int i) { return i + getOffset(); } /** * Returns the offset applicable to this command. * Zero if the command has no "offset" navigator configured, * the value of the offset parameter otherwise. * * @return the offset. */ protected int getOffset() { return null != offsetParameter.getValue() ? Integer.parseInt(offsetParameter.getValue()) : 0; } @Override public boolean isPaginated() { return offsetParameter.isActive(); } /** * Returns the userSortBy applicable to this command and request. * Null if the command has no "sort" navigator configured, * the value of the user's userSortBy parameter. * * This method does not return any command configuration's sort-by attribute (as some subclasses have). * * @return the userSortBy. returns null when false == isUserSortable(). */ protected String getUserSortBy() { return userSortByParameter.getValue(); } @Override public boolean isUserSortable() { return userSortByParameter.isActive(); } /** * Returns parameter value. * Changed since 2.16.1 so that only request parameters are searched. * * @param paramName the name of the parameter to look for. * @return the parameter value, unescaped, or null if parameter does not exist. */ protected String getParameter(final String paramName) { return datamodelParameters.containsKey(paramName) ? datamodelParameters.get(paramName).getString() : null; } // <-- Query Representation state methods (useful while the inbuilt visitor is in operation) protected QueryBuilder getQueryBuilder() { return queryBuilder; } protected synchronized String getQueryRepresentation() { return getQueryBuilder().getQueryString(); } protected FilterBuilder getFilterBuilder() { return filterBuilder; } /** * @todo rename to getFilterString * * @return */ protected String getFilter() { return filterBuilder.getFilterString(); } // Query Representation state methods --> protected final Map<Clause, String> getTransformedTerms() { return transformedTerms; } /** Get a string parameter (first if array exists). * * @param paramName parameter name * @return null when array is null * * @deprecated use getParameter(string) instead */ protected final String getSingleParameter(final String paramName) { final Map<String, Object> parameters = datamodel.getJunkYard().getValues(); return parameters.get(paramName) instanceof String[] ? ((String[]) parameters.get(paramName))[0] : (String) parameters.get(paramName); } /** * Use this always instead of context.getTokenEvaluationEngine() * because the command could be running off a different query string. * * @return */ protected TokenEvaluationEngine getEngine() { return engine; } /** * Uses QueryParser to create a new Query with all evaluation enabled. * * XXX Very expensive method to call! Consider disabling evaluation. * * @param queryString the new query string to parse. * @return newly constructed Query. */ protected final ReconstructedQuery createQuery(final String queryString) { return createQuery(queryString, true); } /** * Uses QueryParser to create a new Query with the option to disable evaluation. * * XXX Very expensive method to call! It helps to disable evaluation. * * @param queryString the new query string to parse. * @param evaluationEnabled whether to enable evaluation. if false the DeadTokenEvaluationEngineImpl is used. * @return newly constructed Query. */ protected final ReconstructedQuery createQuery(final String queryString, final boolean evaluationEnabled) { LOG.debug("createQuery(" + queryString + ')'); ReconstructedQuery reconstructedQuery = null; if (datamodel.getQuery().getQuery().getQueryString().trim().equalsIgnoreCase(queryString.trim())) { // return original query and engine reconstructedQuery = new ReconstructedQuery(datamodel.getQuery().getQuery(), context.getTokenEvaluationEngine()); } else { final TokenEvaluationEngine newEngine; final TokenEvaluationEngine.Context tokenEvalFactoryCxt = ContextWrapper .wrap(TokenEvaluationEngine.Context.class, context, new BaseContext() { public String getQueryString() { return queryString; } public Site getSite() { return datamodel.getSite().getSite(); } public String getUniqueId() { return datamodel.getParameters().getUniqueId(); } }); if (evaluationEnabled) { // This will among other things perform the evaluator searches - local and remote. newEngine = new TokenEvaluationEngineImpl(tokenEvalFactoryCxt); } else { // no evaluators will be called. newEngine = new DeadTokenEvaluationEngineImpl(tokenEvalFactoryCxt); } // queryStr parser final QueryParser parser = new QueryParserImpl(new AbstractQueryParserContext() { @Override public TokenEvaluationEngine getTokenEvaluationEngine() { return newEngine; } }); try { reconstructedQuery = new ReconstructedQuery(parser.getQuery(), engine); } catch (TokenMgrError ex) { // Errors (as opposed to exceptions) are fatal. LOG.fatal(ERR_PARSING, ex); } } return reconstructedQuery; } /** * Escape the word (whether it requires escaping or not). * * Default escaping for strings is to enclose in quotes, ie to phrase the word. * Default escaping for the ':' character is "\\:". * * Override this to match back-end (index) specific escaping. * * @param word The term to escape * @return The escaped version of term. */ protected String escape(final String word) { if (":".equals(word)) { return "\\:"; } else { return '"' + word + '"'; } } /** * Returns null when no field exists. * @param clause * @return */ protected final String getFieldFilter(final LeafClause clause) { String field = null; if (null != clause.getField()) { final Map<String, String> fieldFilters = getSearchConfiguration().getFieldFilterMap(); if (fieldFilters.containsKey(clause.getField())) { field = fieldFilters.get(clause.getField()); } else { for (String fieldFilter : fieldFilters.keySet()) { try { final TokenPredicate tp = TokenPredicateUtility.getTokenPredicate(fieldFilter); // if the field is the token then mask the field and include the term. // XXX why are we checking the known and possible predicates? boolean result = clause.getKnownPredicates().contains(tp); result |= clause.getPossiblePredicates().contains(tp) && getEngine().evaluateTerm(tp, clause.getField()); if (result) { field = fieldFilters.get(fieldFilter); break; } } catch (EvaluationException ie) { LOG.error("failed to check possible predicate with term " + clause.getField()); } catch (IllegalArgumentException iae) { LOG.trace(TRACE_NOT_TOKEN_PREDICATE + fieldFilter); } } } } return field; } protected final void statisticsInfo(final String msg) { final Map<String, Object> parameters = datamodel.getJunkYard().getValues(); final StringBuffer logger = (StringBuffer) parameters.get("no.sesat.Statistics"); if (null != logger) { logger.append(msg); } } /** * Returns the query as it is after the query transformers have been applied to it. * * It is normalised. * * @return */ protected String getTransformedQuerySesamSyntax() { // if it's been nulled then return the original query return null != transformedQuerySesamSyntax ? transformedQuerySesamSyntax.replaceAll(" +", " ") // also normalise it : context.getDataModel().getQuery().getString(); } protected void updateTransformedQuerySesamSyntax() { setTransformedQuerySesamSyntax(displayableQueryBuilder.getQueryString()); } protected void setTransformedQuerySesamSyntax(final String sesamSyntax) { transformedQuerySesamSyntax = sesamSyntax; } // Private ------------------------------------------------------- private void initialiseQuery() { // use the query or something search-command specific final String queryParameter = getSearchConfiguration().getQueryParameter(); if (queryParameter != null && queryParameter.length() > 0) { // It's not the query we are looking for but a string held in a different parameter. final StringDataObject queryToUse = datamodelParameters.get(queryParameter); if (queryToUse != null) { final ReconstructedQuery recon = createQuery(queryToUse.getString()); query = recon.getQuery(); engine = recon.getEngine(); return; } } query = datamodel.getQuery().getQuery(); engine = context.getTokenEvaluationEngine(); } private void applyQueryTransformers(final Query query, final List<QueryTransformerConfig> transformers) { if (transformers != null && transformers.size() > 0) { boolean touchedTransformedQuery = false; final QueryTransformerFactory queryTransformerFactory = new QueryTransformerFactory(qtfContext); final Map<String, Object> junkYard = datamodel.getJunkYard().getValues(); for (QueryTransformerConfig transformerConfig : transformers) { final QueryTransformer transformer = queryTransformerFactory.getController(transformerConfig); final boolean ttq = touchedTransformedQuery; final QueryTransformer.Context qtCxt = ContextWrapper.wrap(QueryTransformer.Context.class, new BaseContext() { public Map<Clause, String> getTransformedTerms() { if (ttq) { throw new IllegalStateException(ERR_TRANSFORMED_QUERY_USED); } return transformedTerms; } }, queryBuilderContext); transformer.setContext(qtCxt); final String newTransformedQuery = transformer.getTransformedQuery(); touchedTransformedQuery |= (!transformedQuery.equals(newTransformedQuery)); if (touchedTransformedQuery) { transformedQuery = newTransformedQuery; } else { transformer.visit(query.getRootClause()); transformedQuery = getQueryRepresentation(); } addFilterString(transformer.getFilter(junkYard)); addFilterString(transformer.getFilter()); LOG.debug(transformer.getClass().getSimpleName() + "--> TransformedQuery=" + transformedQuery); LOG.debug(transformer.getClass().getSimpleName() + "--> Filter=" + filterBuilder.getFilterString()); } } else { transformedQuery = getQueryRepresentation(); } updateTransformedQuerySesamSyntax(); } /** Makes presumption that filter is in format "field:value". * Must be overridden if QueryTransformers return filters in an alternative format. * * @param filter */ protected void addFilterString(final String filter) { if (null != filter && filter.length() > 0) { final int pos = filter.indexOf(":"); if (pos > 0) { filterBuilder.addFilter(filter.substring(0, pos), filter.substring(pos + 1)); } else { filterBuilder.addFilter(null, filter); } } } /** If the command has been cancelled will throw the appropriate SearchCommandException. * Calling this method only makes sense once the call() method has been, * otherwise it is guaranteed to throw the exception. * @throws SearchCommandException when cancellation has occurred. **/ private void checkForCancellation() throws SearchCommandException { if (isCancelled()) { throw new SearchCommandException("cancelled", new InterruptedException()); } } /** Wrapper around getQuery(). Nothing more than a different method signature. **/ private Query getSearchCommandsQuery() { return getQuery(); } /** Wrapper around visitXorClause(). Nothing more than a different method signature. **/ private void searchCommandsVisitXorClause(final Visitor visitor, final XorClause clause) { visitXorClause(visitor, clause); } /** Wrapper around getReservedWords(). Nothing more than a different method signature. **/ private String getSearchCommandsFieldFilter(final LeafClause clause) { return getFieldFilter(clause); } /** Wrapper around getQuery(). Nothing more than a different method signature. **/ private Collection<String> getSearchCommandsReservedWords() { return getReservedWords(); } /** Wrapper around escape(). Nothing more than a different method signature. **/ private String searchCommandsEscape(final String word) { return escape(word); } /** Wrapper around getTransformedTerms(). Nothing more than a different method signature. **/ private Map<Clause, String> getSearchCommandsTransformedTerms() { return getTransformedTerms(); } /** * * @return */ protected int getResultsToReturn() { return context.getSearchConfiguration().getResultsToReturn(); } // Inner classes ------------------------------------------------- /** * see createQuery(string) */ protected static class ReconstructedQuery { private final Query query; private final TokenEvaluationEngine engine; ReconstructedQuery(final Query query, final TokenEvaluationEngine engine) { this.query = query; this.engine = engine; } /** * @return */ public Query getQuery() { return query; } /** * @return */ public TokenEvaluationEngine getEngine() { return engine; } } protected static final class QueryBuilderFactory { // Constants ----------------------------------------------------- // Attributes ---------------------------------------------------- // Static -------------------------------------------------------- // Constructors -------------------------------------------------- /** Creates a new instance of QueryBuilderFactory */ private QueryBuilderFactory() { } // Public -------------------------------------------------------- /** * * @param context * @param config * @return */ public static QueryBuilder getController(final QueryBuilder.Context context, final QueryBuilderConfig config) { final String name = "no.sesat.search.mode.command.querybuilder." + config.getClass().getAnnotation(Controller.class).value(); try { final SiteClassLoaderFactory.Context ctlContext = ContextWrapper .wrap(SiteClassLoaderFactory.Context.class, new BaseContext() { public Spi getSpi() { return Spi.SEARCH_COMMAND_CONTROL; } }, context); final ClassLoader ctlLoader = SiteClassLoaderFactory.instanceOf(ctlContext).getClassLoader(); @SuppressWarnings("unchecked") final Class<? extends QueryBuilder> cls = (Class<? extends QueryBuilder>) ctlLoader.loadClass(name); final Constructor<? extends QueryBuilder> constructor = cls .getConstructor(QueryBuilder.Context.class, QueryBuilderConfig.class); return constructor.newInstance(context, config); } catch (ClassNotFoundException ex) { throw new IllegalArgumentException(ex); } catch (NoSuchMethodException ex) { throw new IllegalArgumentException(ex); } catch (InvocationTargetException ex) { throw new IllegalArgumentException(ex); } catch (InstantiationException ex) { throw new IllegalArgumentException(ex); } catch (IllegalAccessException ex) { throw new IllegalArgumentException(ex); } } // Package protected --------------------------------------------- // Protected ----------------------------------------------------- // Private ------------------------------------------------------- // Inner classes ------------------------------------------------- } }