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