package org.limewire.ui.swing.search; import java.util.HashMap; import java.util.List; import java.util.Map; import org.limewire.collection.SortedList; import org.limewire.core.api.FilePropertyKey; import org.limewire.core.api.search.SearchCategory; import org.limewire.i18n.I18nMarker; import org.limewire.ui.swing.util.FilePropertyKeyUtils; import org.limewire.ui.swing.util.Translator; import com.google.inject.Inject; import com.google.inject.Singleton; /** * Used to generate {@link SearchInfo} objects for a search based on an advanced search defined by * a map of key/value or an encoded search string. */ @Singleton public class KeywordAssistedSearchBuilder { private static final String UNTRANSLATED_SEPARATOR = I18nMarker.marktr(":"); /** * Turns on and off format checking for advanced queries. * * <p> NOTE: The algorithm with format checking off can do a good job * of getting the maximum information from a desirable query but * can lead to false positives since it assumes almost all queries * are intended to be advanced. */ private static final boolean CHECK_FORMAT = true; private final Translator translator; @Inject KeywordAssistedSearchBuilder(Translator translator) { this.translator = translator; } /** * @return a full composite query String from a map of the desired properties and * their values to search on. */ String createCompositeQuery(Map<FilePropertyKey, String> advancedSearch, SearchCategory category) { String keySeparator = getTranslatedKeySeprator(); StringBuilder sb = new StringBuilder(); for(FilePropertyKey key : advancedSearch.keySet()) { String value = advancedSearch.get(key); if (value != null && value.trim().length() > 0) { sb.append(translator.translate( FilePropertyKeyUtils.getUntraslatedDisplayName(key, category)) .toLowerCase()); sb.append(keySeparator); sb.append(value); sb.append(' '); } } int len = sb.length(); if (len > 0) { sb.deleteCharAt(len-1); } return sb.toString(); } /** * Returns the string used to separate key/value pairs in a compound advanced search query. */ String getTranslatedKeySeprator() { return translator.translateWithComment("content separator ie. \"name:limewire\"", UNTRANSLATED_SEPARATOR); } /** * @return a new {@link SearchInfo} based on the advanced search map and category. */ public SearchInfo createAdvancedSearch(Map<FilePropertyKey,String> advancedSearch, SearchCategory searchCategory) { return DefaultSearchInfo.createAdvancedSearch(createCompositeQuery(advancedSearch, searchCategory), advancedSearch, searchCategory); } /** * Attempts to generate an advanced search based on {@link SearchInfo} * from a {@link SearchCategory} and encoded composite query string. * * @return null if the query could not be parsed otherwise the corresponding {@link SearchInfo}. */ public SearchInfo attemptToCreateAdvancedSearch(String query, SearchCategory searchCategory) { // Advanced search in the all or other category is impossible if (searchCategory == SearchCategory.ALL || searchCategory == SearchCategory.OTHER) { return null; } String translatedKeySeparator = getTranslatedKeySeprator(); String untranslatedKeySeparator = UNTRANSLATED_SEPARATOR; // Only attempt to parse an advanced search if the query has at least one special // key separator sequence if (query.indexOf(translatedKeySeparator) > 0 || query.indexOf(untranslatedKeySeparator) > 0) { String lowerCaseUntranslatedQuery = translator.toLowerCaseEnglish(query); String lowerCaseTranslatedQuery = translator.toLowerCaseCurrentLocale(query); Map<FilePropertyKey,String> map = new HashMap<FilePropertyKey,String>(); List<KeyPacket> foundKeys = new SortedList<KeyPacket>(); // Check the query and record possible key locations in English and current language. // NOTE: English is preferred over the current language in all cases for consistency. for (FilePropertyKey candidateKey : FilePropertyKey.values()) { String untranslatedKeyName = FilePropertyKeyUtils.getUntraslatedDisplayName(candidateKey, searchCategory); // Check for English key KeyPacket keyPacket = attemptToFindKey(candidateKey, translator.toLowerCaseEnglish(untranslatedKeyName), untranslatedKeySeparator, lowerCaseUntranslatedQuery); // If the key was found then add it to the list of found keys if (keyPacket != null) { foundKeys.add(keyPacket); continue; } if (translator.isCurrentLanguageEnglish()) { // If we are already in English then we don't need to try and translate the key continue; } // If the key in English was not found check if the translated key was keyPacket = attemptToFindKey(candidateKey, translator.toLowerCaseCurrentLocale( translator.translate(untranslatedKeyName)), translatedKeySeparator, lowerCaseTranslatedQuery); // If the translated key was found then record its location and contents if (keyPacket != null) { foundKeys.add(keyPacket); } } // If no values were successfully parsed then do not attempt to create an // advanced search (should fall back to regular search) if (foundKeys.size() == 0) { return null; } // Check the format, if there is leading non key data before the first key // this probably means this is not actually an intended advanced search if (CHECK_FORMAT) { if (query.substring(0, foundKeys.get(0).getStartIndex()).trim().length() > 0) { return null; } } // Find the value for each found key and insert it in the map // (iterate to one before the last because the last key is a special case) for ( int i=0 ; i<foundKeys.size()-1 ; i++ ) { attemptToParseValue(query, map, foundKeys.get(i), foundKeys.get(i+1).startIndex); } // Make sure the last key corresponds to a valid key/value pair // and if not merge it with the value of the second last key KeyPacket currentPacket = foundKeys.get(foundKeys.size()-1); if (currentPacket.getEndIndex() != query.length()) { attemptToParseValue(query, map, currentPacket, query.length()); } else { if(foundKeys.size() < 2) { return null; } else { KeyPacket secondLastPacket = foundKeys.get(foundKeys.size()-2); map.remove(secondLastPacket); attemptToParseValue(query, map, secondLastPacket, query.length()); } } // If no values were successfully parsed then do not attempt to create an // advanced search (should fall back to regular search) if (map.size() == 0) { return null; } return createAdvancedSearch(map, searchCategory); } return null; } /** * Attempts to parse the value within the bounds and puts it in the map * if possible. */ private static void attemptToParseValue(String query, Map<FilePropertyKey,String> map, KeyPacket currentPacket, int nextIndex) { // Parse the value between the end of the current key and start of the last String value = query.substring(currentPacket.getEndIndex(), nextIndex).trim(); // Only insert into the map if there is data if (value.length() > 0) { map.put(currentPacket.getAssociatedKey(), value); } } private static KeyPacket attemptToFindKey(FilePropertyKey key, String lowerCaseKeyText, String keySeparator, String lowerCaseQuery) { return attemptToFindKey(key, lowerCaseKeyText, keySeparator, lowerCaseQuery, 0); } private static KeyPacket attemptToFindKey(FilePropertyKey key, String lowerCaseKeyText, String keySeparator, String lowerCaseQuery, int startSearchFrom) { int startIndex = lowerCaseQuery.indexOf(lowerCaseKeyText+keySeparator, startSearchFrom); if (startIndex > -1) { // Make sure there is at least a white space before the key so we don't mush things together if (startIndex > 0) { if (lowerCaseQuery.substring(startIndex-1, startIndex).trim().length() > 0) { // This is probably part of another value and not an actual key return attemptToFindKey(key, lowerCaseKeyText, keySeparator, lowerCaseQuery, startIndex+lowerCaseKeyText.length()); } } return new KeyPacket(key, startIndex, startIndex+lowerCaseKeyText.length()+keySeparator.length()); } return null; } public CategoryOverride parseCategoryOverride(String query) { String translatedKeySeparator = getTranslatedKeySeprator(); String untranslatedKeySeparator = UNTRANSLATED_SEPARATOR; // Only attempt to parse an advanced search if the query has at least one special // key separator sequence boolean firstSeparatorIsUntranslated = true; int firstSeparatorPosition = query.indexOf(untranslatedKeySeparator); if (firstSeparatorPosition < 0) { firstSeparatorPosition = query.indexOf(translatedKeySeparator); firstSeparatorIsUntranslated = false; } SearchCategory querySelectedSearchCategory = null; // There should be at least one key separator and it must be forward of the first character if (firstSeparatorPosition > 0) { // Check if the first token is a SearchCategory override querySelectedSearchCategory = attemptToParseSearchCategory( query.substring(0, firstSeparatorPosition).trim(), firstSeparatorIsUntranslated); // If the users language uses the same "colon" as English check again for an override in the // other language if (querySelectedSearchCategory == null && !translator.isCurrentLanguageEnglish() && translatedKeySeparator == untranslatedKeySeparator) { querySelectedSearchCategory = attemptToParseSearchCategory( query.substring(0, firstSeparatorPosition).trim(), !firstSeparatorIsUntranslated); } } if (querySelectedSearchCategory == null) { return null; } else { return new CategoryOverride(querySelectedSearchCategory, query.substring((firstSeparatorPosition+1))); } } private SearchCategory attemptToParseSearchCategory(String firstTerm, boolean firstSeparatorIsUntranslated) { if (firstSeparatorIsUntranslated) { String candidateTerm = translator.toLowerCaseEnglish(firstTerm); for ( SearchCategory category : SearchCategory.values() ) { if (translator.toLowerCaseEnglish( SearchCategoryUtils.getUntranslatedName(category)).equals(candidateTerm)) { return category; } } } else { String candidateTerm = translator.toLowerCaseCurrentLocale(firstTerm); for ( SearchCategory category : SearchCategory.values() ) { if (translator.toLowerCaseCurrentLocale(translator.translate( SearchCategoryUtils.getUntranslatedName(category))).equals(candidateTerm)) { return category; } } } return null; } public static class CategoryOverride { private final SearchCategory category; private final String cutQuery; public CategoryOverride(SearchCategory category, String cutQuery) { this.category = category; this.cutQuery = cutQuery; } public SearchCategory getCategory() { return category; } public String getCutQuery() { return cutQuery; } } /** * Helper class for storing triples matching found key to its location and * and size within a query. */ private static class KeyPacket implements Comparable<KeyPacket> { private final FilePropertyKey associatedKey; private final int startIndex; private final int endIndex; public KeyPacket(FilePropertyKey associatedKey, int startIndex, int endIndex) { this.associatedKey = associatedKey; this.startIndex = startIndex; this.endIndex = endIndex; } public FilePropertyKey getAssociatedKey() { return associatedKey; } public int getStartIndex() { return startIndex; } public int getEndIndex() { return endIndex; } @Override public int compareTo(KeyPacket o) { if (startIndex > o.startIndex) { return 1; } else if (startIndex < o.startIndex) { return -1; } else { return 0; } } } }