QueryBuilder.java revision 1190
0N/A/*
3909N/A * CDDL HEADER START
0N/A *
0N/A * The contents of this file are subject to the terms of the
0N/A * Common Development and Distribution License (the "License").
0N/A * You may not use this file except in compliance with the License.
2362N/A *
0N/A * See LICENSE.txt included in this distribution for the specific
2362N/A * language governing permissions and limitations under the License.
0N/A *
0N/A * When distributing Covered Code, include this CDDL HEADER in each
0N/A * file and include the License file at LICENSE.txt.
0N/A * If applicable, add the following below this CDDL HEADER, with the
0N/A * fields enclosed by brackets "[]" replaced with your own identifying
0N/A * information: Portions Copyright [yyyy] [name of copyright owner]
0N/A *
0N/A * CDDL HEADER END
0N/A */
0N/A/*
0N/A * Copyright 2010 Sun Micosystems. All rights reserved.
2362N/A * Use is subject to license terms.
2362N/A *
2362N/A * Portions Copyright 2011 Jens Elkner.
0N/A */
0N/Apackage org.opensolaris.opengrok.search;
0N/A
0N/Aimport java.util.ArrayList;
0N/Aimport java.util.Collections;
0N/Aimport java.util.Map;
0N/Aimport java.util.TreeMap;
0N/Aimport org.apache.lucene.queryParser.ParseException;
0N/Aimport org.apache.lucene.search.BooleanClause;
0N/Aimport org.apache.lucene.search.BooleanClause.Occur;
0N/Aimport org.apache.lucene.search.BooleanQuery;
0N/Aimport org.apache.lucene.search.Query;
0N/A
0N/A/**
0N/A * Helper class that builds a Lucene query based on provided search terms for
0N/A * the different fields.
0N/A */
0N/Apublic class QueryBuilder {
0N/A final static String FULL = "full";
0N/A final static String DEFS = "defs";
0N/A final static String REFS = "refs";
0N/A final static String PATH = "path";
0N/A final static String HIST = "hist";
0N/A /**
0N/A * A map containing the query text for each field. (We use a sorted map here
0N/A * only because we have tests that check the generated query string. If we
0N/A * had used a hash map, the order of the terms could have varied between
0N/A * platforms and it would be harder to test.)
0N/A */
0N/A private final Map<String, String> queries = new TreeMap<String, String>();
0N/A
0N/A /**
0N/A * Set search string for the "full" field.
3514N/A * @param freetext query string to set
0N/A * @return this instance
3514N/A */
10N/A public QueryBuilder setFreetext(String freetext) {
10N/A return addQueryText(FULL, freetext);
3514N/A }
0N/A /**
0N/A * Get search string for the "full" field.
0N/A * @return {@code null} if not set, the query string otherwise.
0N/A */
0N/A public String getFreetext() {
0N/A return getQueryText(FULL);
0N/A }
0N/A
0N/A /**
0N/A * Set search string for the "defs" field.
0N/A * @param defs query string to set
0N/A * @return this instance
0N/A */
0N/A public QueryBuilder setDefs(String defs) {
0N/A return addQueryText(DEFS, defs);
0N/A }
0N/A /**
0N/A * Get search string for the "full" field.
0N/A * @return {@code null} if not set, the query string otherwise.
0N/A */
0N/A public String getDefs() {
0N/A return getQueryText(DEFS);
0N/A }
0N/A
0N/A /**
0N/A * Set search string for the "refs" field.
0N/A * @param refs query string to set
0N/A * @return this instance
0N/A */
0N/A public QueryBuilder setRefs(String refs) {
0N/A return addQueryText(REFS, refs);
0N/A }
0N/A /**
0N/A * Get search string for the "refs" field.
0N/A * @return {@code null} if not set, the query string otherwise.
1220N/A */
1220N/A public String getRefs() {
1220N/A return getQueryText(REFS);
1220N/A }
1220N/A
1220N/A /** Set search string for the "path" field.
1220N/A * @param path query string to set
1220N/A * @return this instance
1220N/A */
1220N/A public QueryBuilder setPath(String path) {
1220N/A return addQueryText(PATH, path);
1220N/A }
1220N/A /**
1220N/A * Get search string for the "path" field.
1220N/A * @return {@code null} if not set, the query string otherwise.
0N/A */
0N/A public String getPath() {
0N/A return getQueryText(PATH);
0N/A }
0N/A
0N/A /**
0N/A * Set search string for the "hist" field.
0N/A * @param hist query string to set
0N/A * @return this instance
0N/A */
0N/A public QueryBuilder setHist(String hist) {
0N/A return addQueryText(HIST, hist);
0N/A }
0N/A /**
0N/A * Get search string for the "hist" field.
0N/A * @return {@code null} if not set, the query string otherwise.
0N/A */
0N/A public String getHist() {
0N/A return getQueryText(HIST);
0N/A }
0N/A
0N/A /**
914N/A * Get a map containing the query text for each of the fields that have
0N/A * been set.
914N/A * @return a possible empty map.
0N/A */
0N/A public Map<String, String> getQueries() {
2664N/A return Collections.unmodifiableMap(queries);
2664N/A }
2664N/A
2664N/A /**
2664N/A * Get the number of query fields set.
0N/A * @return the current number of fields with a none-empty query string.
0N/A */
2664N/A public int getSize() {
0N/A return queries.size();
0N/A }
2664N/A
0N/A /**
0N/A * Build a new query based on the query text that has been passed in to this
2664N/A * builder.
0N/A *
0N/A * @return a query, or {@code null} if no query text is available.
0N/A * @throws ParseException if the query text cannot be parsed
0N/A */
0N/A public Query build() throws ParseException {
0N/A if (queries.isEmpty()) {
0N/A // We don't have any text to parse
914N/A return null;
914N/A }
914N/A // Parse each of the query texts separately
914N/A ArrayList<Query> queryList = new ArrayList<Query>(queries.size());
914N/A for (Map.Entry<String, String> entry : queries.entrySet()) {
914N/A String field = entry.getKey();
914N/A String queryText = entry.getValue();
914N/A queryList.add(buildQuery(field, escapeQueryString(field, queryText)));
914N/A }
914N/A // If we only have one sub-query, return it directly
914N/A if (queryList.size() == 1) {
914N/A return queryList.get(0);
914N/A }
914N/A // We have multiple subqueries, so let's combine them into a
0N/A // BooleanQuery.
914N/A //
0N/A // If the subquery is a BooleanQuery, we pull out each clause and
0N/A // add it to the outer BooleanQuery so that any negations work on
0N/A // the query as a whole. One exception to this rule: If the query
0N/A // contains one or more Occur.SHOULD clauses and no Occur.MUST
0N/A // clauses, we keep it in a subquery so that the requirement that
0N/A // at least one of the Occur.SHOULD clauses must match (pulling them
0N/A // out would make all of them optional).
0N/A //
0N/A // All other types of subqueries are added directly to the outer
0N/A // query with Occur.MUST.
0N/A BooleanQuery combinedQuery = new BooleanQuery();
0N/A for (Query query : queryList) {
0N/A if (query instanceof BooleanQuery) {
0N/A BooleanQuery boolQuery = (BooleanQuery) query;
0N/A if (hasClause(boolQuery, Occur.SHOULD)
0N/A && !hasClause(boolQuery, Occur.MUST))
0N/A {
0N/A combinedQuery.add(query, Occur.MUST);
0N/A } else {
0N/A for (BooleanClause clause : boolQuery) {
0N/A combinedQuery.add(clause);
0N/A }
0N/A }
0N/A } else {
0N/A combinedQuery.add(query, Occur.MUST);
0N/A }
0N/A }
0N/A return combinedQuery;
0N/A }
0N/A
0N/A /**
0N/A * Add query text for the specified field.
0N/A *
0N/A * @param field the field to add query text for
0N/A * @param query the query text to set
0N/A * @return this object
0N/A */
0N/A private QueryBuilder addQueryText(String field, String query) {
0N/A if (query == null || query.isEmpty()) {
420N/A queries.remove(field);
420N/A } else {
420N/A queries.put(field, query);
420N/A }
420N/A return this;
3689N/A }
3689N/A
3689N/A private String getQueryText(String field) {
3689N/A return queries.get(field);
3689N/A }
420N/A
2807N/A /**
0N/A * Escape special characters in a query string.
0N/A *
0N/A * @param field the field for which the query string is provided
0N/A * @param query the query string to escape
0N/A * @return the escaped query string
0N/A */
0N/A private String escapeQueryString(String field, String query) {
0N/A return FULL.equals(field)
0N/A // The free text field may contain terms qualified with other
0N/A // field names, so we don't escape single colons.
0N/A ? query.replace("::", "\\:\\:")
0N/A // Other fields shouldn't use qualified terms, so escape colons
0N/A // so that we can search for them.
0N/A : query.replace(":", "\\:");
0N/A }
0N/A
0N/A /**
0N/A * Build a subquery against one of the fields.
0N/A *
0N/A * @param field the field to build the query against
0N/A * @param queryText the query text
0N/A * @return a parsed query
0N/A * @throws ParseException if the query text cannot be parsed
0N/A */
0N/A private Query buildQuery(String field, String queryText)
0N/A throws ParseException
0N/A {
0N/A return new CustomQueryParser(field).parse(queryText);
0N/A }
0N/A
0N/A /**
0N/A * Check if a BooleanQuery contains a clause of a given occur type.
0N/A *
0N/A * @param query the query to check
0N/A * @param occur the occur type to check for
0N/A * @return whether or not the query contains a clause of the specified type
0N/A */
0N/A private boolean hasClause(BooleanQuery query, Occur occur) {
0N/A for (BooleanClause clause : query) {
0N/A if (clause.getOccur().equals(occur)) {
0N/A return true;
0N/A }
0N/A }
0N/A return false;
0N/A }
0N/A}
0N/A