Java tutorial
/* * Copyright 2007 the original author or authors. * * 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 grails.plugin.searchable.internal.compass.search; import grails.plugin.searchable.internal.compass.mapping.CompassMappingUtils; import grails.plugin.searchable.internal.compass.support.AbstractSearchableMethod; import grails.plugin.searchable.internal.compass.support.SearchableMethodUtils; import grails.plugin.searchable.internal.lucene.LuceneUtils; import groovy.lang.Closure; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.lucene.analysis.WhitespaceAnalyzer; import org.apache.lucene.index.Term; import org.apache.lucene.queryParser.ParseException; import org.codehaus.groovy.grails.commons.GrailsApplication; import org.compass.core.Compass; import org.compass.core.CompassCallback; import org.compass.core.CompassException; import org.compass.core.CompassQuery; import org.compass.core.CompassSession; import org.compass.core.engine.SearchEngineQueryParseException; import org.springframework.util.Assert; /** * @author Maurice Nicholson */ public class DefaultSuggestQueryMethod extends AbstractSearchableMethod { private SearchableCompassQueryBuilder compassQueryBuilder; private GrailsApplication grailsApplication; public DefaultSuggestQueryMethod(String methodName, Compass compass, GrailsApplication grailsApplication) { this(methodName, compass, grailsApplication, new HashMap()); } public DefaultSuggestQueryMethod(String methodName, Compass compass, GrailsApplication grailsApplication, Map defaultOptions) { super(methodName, compass, null, defaultOptions); this.grailsApplication = grailsApplication; } public Object invoke(Object[] args) { if (!CompassMappingUtils.hasSpellCheckMapping(getCompass())) { throw new IllegalStateException( "Suggestions are only available when classes are mapped with \"spellCheck\" options, either at the class " + "or property level. The simplest way to do this is add spellCheck \"include\" to the domain class searchable mapping closure. " + "See the plugin/Compass documentation Mapping sections for details."); } if (!"true".equals(getCompass().getSettings().getSetting("compass.engine.spellcheck.enable"))) { throw new IllegalStateException( "Suggestions are only available when the Compass Spell Check feature is enabled, but currently it is not. " + "Please set Compass setting 'compass.engine.spellcheck.enable' to 'true'. " + "One way to so this is to use the SearchableConfiguration.groovy file (run \"grails install-searchable-config\") and " + "add \"'compass.engine.spellcheck.enable': 'true'\" to the compassSettings Map. " + "Also see the Spell Check section in the Compass docs for additional settings."); } Object query = SearchableMethodUtils.getQueryArgument(args); if (query instanceof Closure) { throw new UnsupportedOperationException( "Closure queries are not support for query suggestions, only String queries."); } Assert.isInstanceOf(String.class, query, "Only String queries are supported for query suggestions"); SuggestQueryCompassCallback suggestQueryCallback = new SuggestQueryCompassCallback(getCompass(), getDefaultOptions(), args); Map options = getOptions(args); suggestQueryCallback.applyOptions(options); suggestQueryCallback.setGrailsApplication(grailsApplication); suggestQueryCallback.setCompassQueryBuilder(compassQueryBuilder); return doInCompass(suggestQueryCallback); } public SearchableCompassQueryBuilder getCompassQueryBuilder() { return compassQueryBuilder; } public void setCompassQueryBuilder(SearchableCompassQueryBuilder compassQueryBuilder) { this.compassQueryBuilder = compassQueryBuilder; } public Map getOptions(Object[] args) { Map options = new HashMap(getDefaultOptions()); // clone to avoid corrupting original options.putAll(SearchableMethodUtils.getOptionsArgument(args, null)); return options; } public static class SuggestQueryCompassCallback implements CompassCallback { private Map defaultOptions; private Object[] args; private SearchableCompassQueryBuilder compassQueryBuilder; private GrailsApplication grailsApplication; private boolean userFriendly; private boolean emulateCapitalisation; private boolean escape; private boolean allowSame; public SuggestQueryCompassCallback(Compass compass, Map defaultOptions, Object[] args) { this.defaultOptions = defaultOptions; this.args = args; } public Object doInCompass(CompassSession session) throws CompassException { Map options = SearchableMethodUtils.getOptionsArgument(args, defaultOptions); options.put("analyzer", "searchableplugin_simple"); CompassQuery original = compassQueryBuilder.buildQuery(grailsApplication, session, options, args); String queryString = original.getSuggestedQuery().toString(); String suggestedString = queryString; if (options.containsKey("class")) { // Strip the additional junk from around the query - +(what test) +(alias:B) Pattern pattern = Pattern.compile("\\+\\((.+)\\) \\+\\(alias:.+\\)"); Matcher matcher = pattern.matcher(queryString); if (!matcher.matches()) { return queryString; } suggestedString = matcher.group(1); } String originalString = (String) SearchableMethodUtils.getQueryArgument(args); try { return new SuggestedQueryStringBuilder(originalString, suggestedString).userFriendly(userFriendly) .emulateCapitalisation(emulateCapitalisation).escape(escape).allowSame(allowSame) .toSuggestedQueryString(); } catch (ParseException ex) { throw new SearchEngineQueryParseException("Failed to parse one of the queries: orignal [" + originalString + "], suggested: [" + suggestedString + "]", ex); } } public void setCompassQueryBuilder(SearchableCompassQueryBuilder compassQueryBuilder) { this.compassQueryBuilder = compassQueryBuilder; } public void setGrailsApplication(GrailsApplication grailsApplication) { this.grailsApplication = grailsApplication; } public void applyOptions(Map options) { if (options == null) { return; } userFriendly = SearchableMethodUtils.getBool(options, "userFriendly", true); emulateCapitalisation = SearchableMethodUtils.getBool(options, "emulateCapitalisation", true); escape = SearchableMethodUtils.getBool(options, "escape", false); allowSame = SearchableMethodUtils.getBool(options, "allowSame", true); } } public static class SuggestedQueryStringBuilder { private static final Log LOG = LogFactory.getLog(SuggestedQueryStringBuilder.class); private static final String defaultField = "$SuggestedQueryStringUtils_defaultField$"; private String original; private String suggested; private boolean userFriendly = true; private boolean emulateCapitalisation = true; private boolean escape = false; private boolean allowSame = true; /** * Create a suggested query string builder with the given original and suggested query strings * @param original the original query - probably from a user * @param suggested the suggested query - probably from the Compass suggestion engine */ public SuggestedQueryStringBuilder(String original, String suggested) { this.original = original; this.suggested = suggested; } /** * Enable/disable whether queries suggested by {@link #toSuggestedQueryString} are user-frienly, * ie, look like the user's original query. * This feature is enabled by default * If you disable this feature, the emulateCapitalisation setting is ignored and the suggested query * is returned by {@link # toSuggestedQueryString} as-is * @param userFriendly true or false to enable or disable * @return this */ public SuggestedQueryStringBuilder userFriendly(boolean userFriendly) { this.userFriendly = userFriendly; return this; } /** * Enable/disable the emulation of capitalised words. * This feature is enabled by default * @param emulateCapitalisation true or false to enable or disable * @return this */ public SuggestedQueryStringBuilder emulateCapitalisation(boolean emulateCapitalisation) { this.emulateCapitalisation = emulateCapitalisation; return this; } /** * Enable/disable whether to allow the same query to be suggested as the original * This is enabled by default * @param allowSame true or false to enable or disable * @return this */ public SuggestedQueryStringBuilder allowSame(boolean allowSame) { this.allowSame = allowSame; return this; } /** * Get the suggested query based on the options set * @return the suggested query string or null if allowSame is false and the queries match * @throws ParseException if either original or suggested query is invalid */ public String toSuggestedQueryString() throws ParseException { if (!userFriendly) { return suggested; } Term[] originalTerms = LuceneUtils.realTermsForQueryString(defaultField, escape ? LuceneUtils.cleanQuery(original) : original, WhitespaceAnalyzer.class); Term[] suggestedTerms = LuceneUtils.realTermsForQueryString(defaultField, suggested, WhitespaceAnalyzer.class); if (originalTerms.length != suggestedTerms.length) { LOG.warn("Expected the same number of terms for original query [" + original + "] and suggested query [" + suggested + "], " + "but original query has [" + originalTerms.length + "] terms and suggested query has [" + suggestedTerms.length + "] terms " + "so unable to provide user friendly version. Returning suggested query as-is."); return suggested; } StringBuilder userFriendly = new StringBuilder(original); int offset = 0; for (int i = 0; i < originalTerms.length; i++) { Term originalTerm = originalTerms[i]; boolean noField = originalTerm.field().equals(defaultField); String snippet = noField ? originalTerm.text() : originalTerm.field() + ":" + originalTerm.text(); int pos = userFriendly.indexOf(snippet, offset); Term suggestedTerm = suggestedTerms[i]; String replacement = getReplacement(originalTerm, noField, suggestedTerm); userFriendly.replace(pos, pos + snippet.length(), replacement); offset = pos; } String suggestion = userFriendly.toString(); if (!allowSame && suggestion.equals(original)) { return null; } return suggestion; } public SuggestedQueryStringBuilder escape(boolean escape) { this.escape = escape; return this; } private String getReplacement(Term originalTerm, boolean noField, Term suggestedTerm) { String replacement = noField ? suggestedTerm.text() : originalTerm.field() + ":" + suggestedTerm.text(); if (emulateCapitalisation) { boolean upperCase = true; boolean firstUpperCase = false; final String original = originalTerm.text(); for (int i = 0; i < original.length(); i++) { if (!Character.isUpperCase(original.charAt(i))) { upperCase = false; break; } if (i == 0) { firstUpperCase = true; } } if (upperCase) { return replacement.toUpperCase(); } if (firstUpperCase) { return replacement.substring(0, 1).toUpperCase() + (replacement.length() > 1 ? replacement.substring(1) : ""); } } return replacement; } } }