/*
* Copyright (C) 2012 The CyanogenMod Project
*
* 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 com.cyanogenmod.filemanager.util;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.BackgroundColorSpan;
import android.text.style.StyleSpan;
import android.util.Log;
import com.cyanogenmod.filemanager.model.FileSystemObject;
import com.cyanogenmod.filemanager.model.Query;
import com.cyanogenmod.filemanager.model.SearchResult;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A helper class with useful methods for deal with search results.
*/
public final class SearchHelper {
private static final String TAG = SearchHelper.class.getSimpleName();
private static final String REGEXP_WILCARD = "*"; //$NON-NLS-1$
private static final String REGEXP_WILCARD_JAVA = ".*"; //$NON-NLS-1$
/**
* Constructor of SearchHelper.
*/
private SearchHelper() {
super();
}
/**
* Method that create a regular expression from a user query.
*
* @param query The query requested by the user
* @param javaRegExp If returns a java regexp
* @return String The regular expressions of the query to match an ignore case search
*/
@SuppressWarnings("boxing")
public static String toRegExp(final String query, boolean javaRegExp) {
//Check that all is correct
if (query == null || query.trim().length() == 0) {
return ""; //$NON-NLS-1$
}
// If the regexp for java, then prepare the query
String q = query;
if (javaRegExp) {
q = prepareQuery(q);
}
return String.format(
"%s%s%s", //$NON-NLS-1$;
javaRegExp ? REGEXP_WILCARD_JAVA : REGEXP_WILCARD,
q, javaRegExp ? REGEXP_WILCARD_JAVA : REGEXP_WILCARD);
}
/**
* Method that cleans and prepares the query of the user to conform with a valid regexp.
*
* @param query The query requested by the user
* @return String The prepared the query
*/
private static String prepareQuery(String query) {
StringBuilder sb = new StringBuilder(query.length());
for (int i = 0; i < query.length(); ++i) {
char ch = query.charAt(i);
if (Character.isLetterOrDigit(ch) ||
ch == ' ' ||
ch == '\'') {
sb.append(ch);
} else if (ch == '*') {
sb.append(".*"); //$NON-NLS-1$
}
}
return sb.toString();
}
/**
* Method that returns the name string highlighted with the match query.
*
* @param result The result to highlight
* @param queries The list of queries that parameterized the search
* @param highlightedColor The highlight color
* @return CharSequence The name string highlighted
*/
public static CharSequence getHighlightedName(
SearchResult result, List queries, int highlightedColor) {
String name = result.getFso().getName();
int cc = queries.size();
for (int i = 0; i < cc; i++) {
//Get the query removing wildcards
String query =
queries.get(i)
.replace(".", "[.]") //$NON-NLS-1$//$NON-NLS-2$
.replace("*", ".*"); //$NON-NLS-1$//$NON-NLS-2$
Pattern pattern;
try {
pattern = Pattern.compile(query, Pattern.CASE_INSENSITIVE);
} catch (PatternSyntaxException e) {
Log.w(TAG, "Invalid regex syntax. Using literal query. Error=" + e);
pattern = Pattern.compile(query, Pattern.CASE_INSENSITIVE | Pattern.LITERAL);
}
Matcher matcher = pattern.matcher(name);
Spannable span = new SpannableString(name);
if (matcher.find()) {
//Highlight the match
span.setSpan(
new BackgroundColorSpan(highlightedColor),
matcher.start(), matcher.end(), 0);
span.setSpan(
new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), 0);
return span;
}
}
// Something is wrong!!!. Name should be matched by some of the queries
// No highlight terms
return name;
}
/**
* Method that returns the name but not highlight the search terms
*
* @param result The result to highlight
* @return CharSequence The non highlighted name string
*/
public static CharSequence getNonHighlightedName(SearchResult result) {
String name = result.getFso().getName();
Spannable span = new SpannableString(name);
span.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
return span;
}
/**
* Method that converts the list of file system object to a search result.
*
* @param files The files to convert
* @param queries The terms of the search
* @return List The files converted
*/
public static List convertToResults(List files, Query queries) {
//Converts the list of files in a list of search results
List results = new ArrayList(files.size());
int cc = files.size();
for (int i = 0; i < cc; i++) {
FileSystemObject fso = files.get(i);
results.add( convertToResult(fso, queries) );
}
return results;
}
/**
* Method that converts a file system object to a search result.
*
* @param fso FileSystemObject that needs to be converted to a SearchResult
* @param queries The terms of the search
* @return SearchResult
*/
public static SearchResult convertToResult(FileSystemObject fso, Query queries) {
double relevance = calculateRelevance(fso, queries);
return new SearchResult(relevance, fso);
}
/**
* Method that calculates the relevance of a file system object for the terms
* of a query.
*
* The algorithm is described as:
*
* By Name:
*
* - 3 points if the term matches the name
* - 2 points if the term starts or ends in the name
* - 1 point if the term has other matches in the name
*
*
* By Accuracy:
*
* - 3 points if the term is the more accuracy (1st term)
* - 1 point if the term is the less accuracy (last term)
* - 2 points in other cases
*
*
*
* Relevance = By Name * By Accuracy
*
* @param fso The file system object
* @param queries The terms of the search
* @return double A value from 1 to 10 where 10 has more relevance
*/
public static double calculateRelevance(FileSystemObject fso, Query queries) {
double relevance = 1.0; //Minimum relevance (is in the result so has some relevance)
List terms = queries.getQueries();
String name = fso.getName();
int cc = terms.size();
for (int i = 0; i < cc; i++) {
String query =
terms.get(i)
.replace(".", "[.]") //$NON-NLS-1$//$NON-NLS-2$
.replace("*", ".*"); //$NON-NLS-1$//$NON-NLS-2$
Pattern pattern;
try {
pattern = Pattern.compile(query, Pattern.CASE_INSENSITIVE);
} catch (PatternSyntaxException e) {
Log.w(TAG, "Invalid regex syntax. Using literal query. Error=" + e);
pattern = Pattern.compile(query, Pattern.CASE_INSENSITIVE | Pattern.LITERAL);
}
Matcher matcher = pattern.matcher(name);
if (matcher.find()) {
//By name
double byNameRelevance = 1.0;
if (matcher.group().length() == name.length()) {
byNameRelevance = 3.0;
} else if (name.startsWith(matcher.group()) || name.endsWith(matcher.group())) {
byNameRelevance = 2.0;
}
//By accuracy
double byNameAccuracy = 1.0;
if (i == 0) {
byNameAccuracy = 3.0;
} else if (i != terms.size()) {
byNameAccuracy = 2.0;
}
//Calculate the relevance
relevance += byNameRelevance * byNameAccuracy;
}
}
return relevance;
}
}