/* * 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 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.regex.Matcher; import java.util.regex.Pattern; /** * A helper class with useful methods for deal with search results. */ public final class SearchHelper { private static final String REGEXP_WILCARD = "*"; //$NON-NLS-1$ private static final String REGEXP_WILCARD_JAVA = ".*"; //$NON-NLS-1$ /** * Constructor of <code>SearchHelper</code>. */ 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 toIgnoreCaseRegExp(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); } //Convert the string to lower and upper final String lowerCase = q.toLowerCase(); final String upperCase = q.toUpperCase(); //Create the regular expression filter StringBuffer sb = new StringBuffer(); int cc = lowerCase.length(); for (int i = 0; i < cc; i++) { char lower = lowerCase.charAt(i); char upper = upperCase.charAt(i); if (lower != upper) { //Convert to expression sb.append(String.format("[%s%s]", lower, upper)); //$NON-NLS-1$ } else { //Not need to convert sb.append(lower); } } return String.format( "%s%s%s", //$NON-NLS-1$; javaRegExp ? REGEXP_WILCARD_JAVA : REGEXP_WILCARD, sb.toString(), 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<String> 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 = Pattern.compile(query, Pattern.CASE_INSENSITIVE); 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<SearchResult> The files converted */ public static List<SearchResult> convertToResults(List<FileSystemObject> files, Query queries) { //Converts the list of files in a list of search results List<SearchResult> results = new ArrayList<SearchResult>(files.size()); int cc = files.size(); for (int i = 0; i < cc; i++) { FileSystemObject fso = files.get(i); double relevance = calculateRelevance(fso, queries); SearchResult result = new SearchResult(relevance, fso); results.add(result); } return results; } /** * Method that calculates the relevance of a file system object for the terms * of a query.<br/> * <br/> * The algorithm is described as:<br/> * <br/> * By Name:<br/> * <ul> * <li>3 points if the term matches the name</li> * <li>2 points if the term starts or ends in the name</li> * <li>1 point if the term has other matches in the name</li> * </ul> * <br/> * By Accuracy:<br/> * <ul> * <li>3 points if the term is the more accuracy (1st term)</li> * <li>1 point if the term is the less accuracy (last term)</li> * <li>2 points in other cases</li> * <li></li> * </ul> * <br/> * <code>Relevance = By Name * By Accuracy</code> * * @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<String> 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 = Pattern.compile(query, Pattern.CASE_INSENSITIVE); 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; } }