/*
* ConcourseConnect
* Copyright 2009 Concursive Corporation
* http://www.concursive.com
*
* This file is part of ConcourseConnect, an open source social business
* software and community platform.
*
* Concursive ConcourseConnect is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, version 3 of the License.
*
* Under the terms of the GNU Affero General Public License you must release the
* complete source code for any application that uses any part of ConcourseConnect
* (system header files and libraries used by the operating system are excluded).
* These terms must be included in any work that has ConcourseConnect components.
* If you are developing and distributing open source applications under the
* GNU Affero General Public License, then you are free to use ConcourseConnect
* under the GNU Affero General Public License.
*
* If you are deploying a web site in which users interact with any portion of
* ConcourseConnect over a network, the complete source code changes must be made
* available. For example, include a link to the source archive directly from
* your web site.
*
* For OEMs, ISVs, SIs and VARs who distribute ConcourseConnect with their
* products, and do not license and distribute their source code under the GNU
* Affero General Public License, Concursive provides a flexible commercial
* license.
*
* To anyone in doubt, we recommend the commercial license. Our commercial license
* is competitively priced and will eliminate any confusion about how
* ConcourseConnect can be used and distributed.
*
* ConcourseConnect 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 Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with ConcourseConnect. If not, see <http://www.gnu.org/licenses/>.
*
* Attribution Notice: ConcourseConnect is an Original Work of software created
* by Concursive Corporation
*/
package com.concursive.connect.web.modules.search.utils;
import com.concursive.commons.text.StringUtils;
import com.concursive.connect.indexer.IIndexerSearch;
import com.concursive.connect.indexer.IndexerFactory;
import com.concursive.connect.web.modules.common.social.geotagging.utils.LocationBean;
import com.concursive.connect.web.modules.common.social.geotagging.utils.LocationUtils;
import com.concursive.connect.web.modules.search.beans.SearchBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.StringTokenizer;
/**
* Utilities to work with search strings and Lucene queries
*
* @author matt rajkowski
* @created June 11, 2004
*/
public class SearchUtils {
private static Log LOG = LogFactory.getLog(SearchUtils.class);
private final static String DOUBLE_QUOTE = "\"";
private final static String WHITESPACE_AND_QUOTES = " \t\r\n\"";
private final static String QUOTES_ONLY = "\"";
/**
* Extracts the keywords into tokens, and then either concats them with AND
* if all words are required, or leaves the tokens alone
*
* @param searchText Description of the Parameter
* @param allWords Description of the Parameter
* @return Description of the Return Value
*/
public static String parseSearchText(String searchText, boolean allWords) {
StringBuffer sb = new StringBuffer();
boolean returnTokens = true;
String currentDelims = WHITESPACE_AND_QUOTES;
StringTokenizer parser = new StringTokenizer(searchText, currentDelims, returnTokens);
String token = null;
boolean spacer = false;
while (parser.hasMoreTokens()) {
token = parser.nextToken(currentDelims);
if (!isDoubleQuote(token)) {
if (hasText(token)) {
String gotToken = token.trim().toLowerCase();
if ("and".equals(gotToken) || "or".equals(gotToken) || "not".equals(gotToken)) {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append(gotToken.toUpperCase());
spacer = true;
} else {
if (spacer) {
if (sb.length() > 0) {
sb.append(" ");
}
spacer = false;
} else {
if (sb.length() > 0) {
if (allWords) {
sb.append(" AND ");
} else {
sb.append(" ");
}
}
}
if (gotToken.indexOf(" ") > -1) {
sb.append("\"").append(gotToken).append("\"");
} else {
sb.append(gotToken);
}
}
}
} else {
currentDelims = flipDelimiters(currentDelims);
}
}
return sb.toString();
}
/**
* Description of the Method
*
* @param searchText Description of the Parameter
* @return Description of the Return Value
*/
public static ArrayList<String> parseSearchTerms(String searchText) {
ArrayList<String> terms = new ArrayList<String>();
StringBuffer sb = new StringBuffer();
boolean returnTokens = true;
String currentDelims = WHITESPACE_AND_QUOTES;
StringTokenizer parser = new StringTokenizer(searchText, currentDelims, returnTokens);
String token = null;
while (parser.hasMoreTokens()) {
token = parser.nextToken(currentDelims);
if (!isDoubleQuote(token)) {
if (hasText(token)) {
String gotToken = token.trim().toLowerCase();
if ("and".equals(gotToken) || "or".equals(gotToken) || "not".equals(gotToken)) {
} else {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append(gotToken);
terms.add(sb.toString());
sb.setLength(0);
}
}
} else {
currentDelims = flipDelimiters(currentDelims);
}
}
return terms;
}
/**
* Description of the Method
*
* @param text Description of the Parameter
* @return Description of the Return Value
*/
private static boolean hasText(String text) {
return (text != null && !text.trim().equals(""));
}
/**
* Gets the doubleQuote attribute of the SearchUtils object
*
* @param text Description of the Parameter
* @return The doubleQuote value
*/
private static boolean isDoubleQuote(String text) {
return text.equals(DOUBLE_QUOTE);
}
/**
* Description of the Method
*
* @param delims Description of the Parameter
* @return Description of the Return Value
*/
private static String flipDelimiters(String delims) {
String result = null;
if (delims.equals(WHITESPACE_AND_QUOTES)) {
result = QUOTES_ONLY;
} else {
result = WHITESPACE_AND_QUOTES;
}
return result;
}
public static synchronized IIndexerSearch retrieveSearcher(int indexType) throws IOException {
// Get the shared searcher
return IndexerFactory.getInstance().getIndexerService().getIndexerSearch(indexType);
}
/**
* Generates the Lucene Query for finding public listings, user-public listings, and
* user-member listings
*
* @param search
* @param userId
* @return
*/
public static String generateProjectQueryString(SearchBean search, int userId, int instanceId, String projectList) {
LOG.info("Search Query: " + search.getQuery() + " (" + search.getLocation() + ")");
// The search portal is being used
String locationTerm = search.getParsedLocation();
// if search location is 5-digit number, then find the city and state to expand the search,
// but boost the zip code
if (StringUtils.hasText(locationTerm)) {
if (StringUtils.isNumber(locationTerm) && locationTerm.length() == 5) {
LocationBean location = LocationUtils.findLocationByZipCode(locationTerm);
if (location != null) {
locationTerm = "\"" + locationTerm + "\"^30 OR (\"" + location.getCity() + "\"^29 AND " + location.getState() + "^28)";
}
} else {
locationTerm = "\"" + locationTerm + "\"^30";
}
}
// Optimize the terms
StringBuffer titleValues = new StringBuffer();
StringBuffer keywordValues = new StringBuffer();
StringBuffer termValues = new StringBuffer();
// Find the phrase as-is in quotes for exactness, in the title
titleValues.append("\"").append(search.getParsedQuery()).append("\"^25");
// Find matches in the keywords
keywordValues.append("\"").append(search.getParsedQuery()).append("\"^24");
// Exact description match
termValues.append("\"").append(search.getParsedQuery()).append("\"^16");
// Find the words in the phrase
ArrayList<String> terms = search.getTerms();
int titleCount = 23;
int keywordCount = 19;
int count = 15;
for (String term : terms) {
if (titleValues.length() > 0) {
titleValues.append(" OR ");
}
if (keywordValues.length() > 0) {
keywordValues.append(" OR ");
}
if (termValues.length() > 0) {
termValues.append(" OR ");
}
if (count < 5) {
count = 5;
}
titleValues.append(term).append("^").append(titleCount);
--titleCount;
keywordValues.append(term).append("^").append(count);
--keywordCount;
termValues.append(term).append("^").append(count);
--count;
}
// Find wildcards for the words in the phrase
count = 4;
for (String term : terms) {
if (termValues.length() > 0) {
termValues.append(" OR ");
}
if (count < 1) {
count = 1;
}
termValues.append(term).append("*").append("^").append(count);
--count;
}
String thisQuery =
"(approved:1) " +
(instanceId > -1 ? "AND (instanceId:" + instanceId + ") " : "") +
"AND (" +
"(guests:1) " +
(userId > 0 ? "OR (participants:1) " : "") +
(StringUtils.hasText(projectList) ? "OR " + "(projectId:(" + projectList + ")) " : "") +
") " +
"AND (closed:0) " +
"AND (website:0) " +
(search.getProjectId() > -1 ? "AND (projectId:" + search.getProjectId() + ") " : "") +
(StringUtils.hasText(search.getQuery()) ?
"AND (title:(" + titleValues.toString() + ") OR (keywords:(" + keywordValues.toString() + ")) OR (" + termValues.toString() + ")) " : "") +
(StringUtils.hasText(locationTerm) ? "AND (location:(" + locationTerm + ")) " : "");
LOG.debug("Built Query: " + thisQuery);
return thisQuery;
}
/**
* Generates the Lucene Query for finding public data, user-public data, and
* user-member data
*
* @param search
* @param userId
* @param instanceId
* @param projectListings
* @return
* @throws SQLException
*/
public static String generateDataQueryString(SearchBean search, int userId, int instanceId, String projectListings) throws SQLException {
// Generate the string
return (StringUtils.hasText(search.getQuery()) ? "(" + search.getParsedQuery() + ") AND " : "") +
(instanceId > -1 ? "(instanceId:" + instanceId + ") AND " : "") +
"(" +
"(" +
"((guests:1) AND (membership:0)) " +
(userId > 0 ? " OR ((participants:1) AND (membership:0)) " : "") +
") " +
(StringUtils.hasText(projectListings) ? "OR " + "(projectId:(" + projectListings + "))" : "") +
") ";
}
public static String generateValidProjects(Connection db, int userId, int specificProjectId) throws SQLException {
return generateValidProjects(db, userId, specificProjectId, -1);
}
/**
* Generates a list of projects that the user has access to
*
* @param db
* @param userId
* @param specificProjectId
* @param specificCategoryId
* @return
* @throws SQLException
*/
public static String generateValidProjects(Connection db, int userId, int specificProjectId, int specificCategoryId) throws SQLException {
if (userId < 1) {
return "";
}
// @todo get ids from user cache
// @update cache everytime a user is added or removed from a project team
// get the projects for the user
// get the project permissions for each project
// if user has access to the data, then add to query
StringBuffer projectList = new StringBuffer();
PreparedStatement pst = db.prepareStatement(
"SELECT project_id " +
"FROM project_team " +
"WHERE user_id = ? " +
"AND status IS NULL " +
(specificProjectId > -1 ? "AND project_id = ? " : "") +
(specificCategoryId > -1 ? "AND project_id IN (SELECT project_id FROM projects WHERE category_id = ?) " : ""));
int i = 0;
pst.setInt(++i, userId);
if (specificProjectId > -1) {
pst.setInt(++i, specificProjectId);
}
if (specificCategoryId > -1) {
pst.setInt(++i, specificCategoryId);
}
ResultSet rs = pst.executeQuery();
while (rs.next()) {
int projectId = rs.getInt("project_id");
// these projects override the lower access projects
if (projectList.length() > 0) {
projectList.append(" OR ");
}
projectList.append(projectId);
}
rs.close();
pst.close();
return projectList.toString();
}
}