package org.limewire.promotion; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutorService; import org.limewire.concurrent.ExecutorsHelper; import org.limewire.geocode.GeocodeInformation; import org.limewire.promotion.SearcherDatabase.QueryResult; import org.limewire.promotion.containers.PromotionMessageContainer; import org.limewire.promotion.containers.PromotionMessageContainer.GeoRestriction; import org.limewire.promotion.impressions.ImpressionsCollector; import com.google.inject.Inject; import com.google.inject.Singleton; @Singleton public class PromotionSearcherImpl implements PromotionSearcher { private final KeywordUtil keywordUtil; private final SearcherDatabase searcherDatabase; private final ImpressionsCollector impressionsCollector; private final PromotionBinderRepository promotionBinderRepository; private final PromotionServices promotionServices; private final ExecutorService exec; private volatile int maxNumberOfResults = 5; @Inject public PromotionSearcherImpl(final KeywordUtilImpl keywordUtil, final SearcherDatabase searcherDatabase, final ImpressionsCollector impressionsCollector, final PromotionBinderRepository promotionBinderRepository, final PromotionServices promotionServices) { this.keywordUtil = keywordUtil; this.searcherDatabase = searcherDatabase; this.impressionsCollector = impressionsCollector; this.promotionBinderRepository = promotionBinderRepository; this.promotionServices = promotionServices; this.exec = ExecutorsHelper.newThreadPool("SearcherThread"); } /** * Order of operations: * * <ol> * <li> normalize the query using {@link KeywordUtilImpl} * <li> expire any db results that have passed * <li> request the {@link PromotionBinder} for this query from the * {@link PromotionBinderRepository} (may be cached) * <li> insert all valid {@link PromotionMessageContainer} entries into db * <li> run the db search and callback results, but using maxNumberOfResults * as a limit, and using the probability field to decide if a return result * should REALLY be shown. * </ol> * * @param query the searched terms * @param callback the recipient of the results * @param userLocation this can be <code>null</code> */ public void search(final String query, final PromotionSearchResultsCallback callback, final GeocodeInformation userLocation) { if (isEnabled()) { exec.execute(new Searcher(query, callback, userLocation)); } } public void init(final int maxNumberOfResults) throws InitializeException { this.maxNumberOfResults = maxNumberOfResults; searcherDatabase.init(); } /** * This thread handles the real work of the {@link PromotionSearcher}, * conducting the search and invoking the callback passed to the search * method. When the search has completed, this thread dies. */ private class Searcher implements Runnable { private final String query; private final PromotionSearchResultsCallback callback; private final String normalizedQuery; /** The latitude/longitude of the user, or null if not known. */ private final LatitudeLongitude userLatLon; /** Two character territory of user ('US') or null if not known. */ private final String userTerritory; Searcher(String query, PromotionSearchResultsCallback callback, GeocodeInformation userLocation) { this.query = query; this.callback = callback; this.normalizedQuery = keywordUtil.normalizeQuery(query); // Now calculate our latitude/longitude from the Geocode, or null if (userLocation == null) { this.userLatLon = null; this.userTerritory = null; } else { this.userLatLon = getLatitudeLongitude(userLocation); this.userTerritory = userLocation .getProperty(GeocodeInformation.Property.CountryCode); } } /** * Pulls out the latitude and longitude properties and puts them into a * {@link LatitudeLongitude} instance, or returns null if there is a * problem parsing or missing data. * * @param geocodeInformation this could be <code>null</code> */ private LatitudeLongitude getLatitudeLongitude(GeocodeInformation geocodeInformation) { final String lat = geocodeInformation.getProperty(GeocodeInformation.Property.Latitude); final String lon = geocodeInformation .getProperty(GeocodeInformation.Property.Longitude); if (lat == null || lon == null) return null; try { return new LatitudeLongitude(Double.parseDouble(lat), Double.parseDouble(lon)); } catch (NumberFormatException ex) { return null; } } public void run() { // OK, start the meat of the query! final Date timeQueried = new Date(); // Get the binder (maybe cached), ingest it, and search... PromotionBinder binder = null; List<QueryResult> results = null; binder = promotionBinderRepository.getBinderForBucket(keywordUtil.getHashValue(normalizedQuery)); try { if (binder != null) { searcherDatabase.ingest(binder); } searcherDatabase.expungeExpired(); results = searcherDatabase.query(normalizedQuery); } catch (DatabaseExecutionException e) { promotionServices.stop(); } if (results == null) { // // This will happen if an error occured in a database operation // return; } removeInvalidResults(results, timeQueried); List<QueryResult> visibleResults = new ArrayList<QueryResult>(results.size()); for (QueryResult result : results) { if (result.getPromotionMessageContainer().isImpressionOnly()) { impressionsCollector.recordImpression(query, timeQueried, new Date(), result .getPromotionMessageContainer(), result.getBinderUniqueName()); } else { visibleResults.add(result); } } int shownResults = 0; int idx = 0; for(QueryResult result : visibleResults) { final float probability = result.getPromotionMessageContainer().getProbability(); int remainingToIterateThrough = visibleResults.size() - idx; int remainingMaxToShow = maxNumberOfResults - shownResults; if (remainingToIterateThrough <= remainingMaxToShow || Math.random() <= probability) { shownResults++; callback.process(result.getPromotionMessageContainer()); // record we just showed this result. impressionsCollector .recordImpression(query, timeQueried, new Date(), result .getPromotionMessageContainer(), result .getBinderUniqueName()); } idx++; // Exit early if we're done. Assumes impression-only are sorted // at top. if (shownResults >= maxNumberOfResults) break; } } private boolean isMessageValid(final PromotionMessageContainer promotionMessageContainer, final String binderUniqueName, long currentTimeMillis) { final PromotionBinder binder = searcherDatabase.getBinder(binderUniqueName); if (binder == null) return false; return binder.isValidMember(promotionMessageContainer, true, currentTimeMillis); } /** * Strips out results that don't seem to be valid due to territory, * radius, or other restriction. */ private void removeInvalidResults(List<QueryResult> results, Date timeQueried) { for (QueryResult result : new ArrayList<QueryResult>(results)) { PromotionMessageContainer promo = result.getPromotionMessageContainer(); List<GeoRestriction> restrictions = promo.getGeoRestrictions(); // Check that the user is within any geo restrictions if (restrictions.size() > 0) { if (userLatLon == null) { // promo is restricted but we don't know the user's // location so don't show it. results.remove(result); continue; } else { boolean matchedAtLeastOneRestriction = false; for (GeoRestriction restriction : restrictions) { if (restriction.contains(userLatLon)) { matchedAtLeastOneRestriction = true; break; } } if (!matchedAtLeastOneRestriction) { // The user is not within any of the restrictions, // so // remove the result results.remove(result); continue; } } } // Check that the user is within any territories Locale[] territories = promo.getTerritories(); if (territories.length > 0) { if (userTerritory == null) { // promo is restricted but we don't know the user's // location so don't show it. results.remove(result); continue; } else { boolean matchAtLeastOneTerritory = false; for (Locale territory : territories) { if (userTerritory.equalsIgnoreCase(territory.getCountry())) { matchAtLeastOneTerritory = true; break; } } if (!matchAtLeastOneTerritory) { // User isn't in any valid territories results.remove(result); continue; } } } if (!isMessageValid(result.getPromotionMessageContainer(), result .getBinderUniqueName(), timeQueried.getTime())) { results.remove(result); continue; } } } } public void shutDown() { searcherDatabase.shutDown(); } @Override public boolean isEnabled() { return promotionServices.isRunning(); } }