/* ===================================================================
 * RecipeLucenePlugin.java
 * 
 * Created May 25, 2006 9:18:03 PM
 * 
 * Copyright (c) 2006 Matt Magoffin.
 * 
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation; either version 2 of 
 * the License, or (at your option) any later version.
 * 
 * This program 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 
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License 
 * along with this program; if not, write to the Free Software 
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 
 * 02111-1307 USA
 * ===================================================================
 * $Id: RecipeLucenePlugin.java 72 2009-05-14 02:11:56Z msqr $
 * ===================================================================
 */

package magoffin.matt.ieat.biz.impl.lucene;

import java.io.IOException;
import java.io.Serializable;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import magoffin.matt.ieat.biz.BizContext;
import magoffin.matt.ieat.biz.BizContextHolder;
import magoffin.matt.ieat.biz.RecipeSearchBiz;
import magoffin.matt.ieat.dao.RecipeDao;
import magoffin.matt.ieat.dao.UserDao;
import magoffin.matt.ieat.domain.AdvancedSearchCriteria;
import magoffin.matt.ieat.domain.Recipe;
import magoffin.matt.ieat.domain.RecipeIngredient;
import magoffin.matt.ieat.domain.RecipeRating;
import magoffin.matt.ieat.domain.RecipeSearchCriteria;
import magoffin.matt.ieat.domain.RecipeStep;
import magoffin.matt.ieat.domain.System;
import magoffin.matt.ieat.domain.UiRecipeMatch;
import magoffin.matt.ieat.domain.UiSearchResults;
import magoffin.matt.ieat.domain.User;
import magoffin.matt.lucene.IndexEvent;
import magoffin.matt.lucene.IndexResults;
import magoffin.matt.lucene.LucenePlugin;
import magoffin.matt.lucene.LuceneServiceUtils;
import magoffin.matt.lucene.SearchCriteria;
import magoffin.matt.lucene.SearchMatch;
import magoffin.matt.lucene.SearchResults;
import magoffin.matt.lucene.IndexEvent.EventType;
import magoffin.matt.lucene.LuceneService.IndexWriterOp;
import magoffin.matt.util.DelegatingInvocationHandler;
import magoffin.matt.util.StringUtil;

import org.apache.log4j.Logger;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.RangeQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.BooleanClause.Occur;
import org.springframework.util.StringUtils;

/**
 * Lucene search plugin implementation for User objects.
 * 
 * @author matt.magoffin
 * @version $Revision: 72 $ $Date: 2009-05-14 14:11:56 +1200 (Thu, 14 May 2009) $
 */
public class RecipeLucenePlugin extends AbstractLucenePlugin implements LucenePlugin {

	private RecipeDao recipeDao = null;
	private UserDao userDao = null;
	private int maxRating = 5;
	private int reindexPageSize = 100;
	private NumberFormat averageRatingNumberFormat = null;
	private final Logger log = Logger.getLogger(RecipeLucenePlugin.class);

	/**
	 * Default constructor.
	 */
	public RecipeLucenePlugin() {
		super();
		setIndexType(IndexType.RECIPE.toString());
	}

	/* (non-Javadoc)
	 * @see magoffin.matt.lucene.LucenePlugin#getIdForObject(java.lang.Object)
	 */
	public Object getIdForObject(Object object) {
		if ( object instanceof Recipe ) {
			return ((Recipe)object).getRecipeId();
		}
		return null;
	}

	public void index(Object objectId, IndexWriter writer) {
		Recipe r = recipeDao.get((Long)objectId);
		indexRecipe(r, writer);
	}

	public void indexObject(Object object, IndexWriter writer) {
		indexRecipe((Recipe)object, writer);
	}
	
	@SuppressWarnings("unchecked")
	private void doReindexAll(IndexWriter writer, RecipeIndexResults indexResults) {
		int currPage = 0;
		UiSearchResults results = null;
		do {
			results = recipeDao.getAllRecipes(reindexPageSize, currPage);
			for ( Recipe recipe : (List<Recipe>)results.getRecipe() ) {
				List<Object> errors = indexRecipe(recipe, writer);
				if ( errors != null && errors.size() > 0 ) {
					indexResults.errors.put(recipe.getRecipeId(), errors.get(0).toString());
				}
			}
			currPage++;
		} while ( results.getRecipe().size() > 0 );
		/* OLD VERSION:
		recipeDao.index(
				new RecipeDao.RecipeIndexCallback() {
					private Recipe currRecipe = null;
					private Set ingredients = new HashSet();
					private Set steps = new HashSet();
					private Map ratingMap = new HashMap();
					public void handle(RecipeDao.RecipeIndexCallbackData data) {
						if ( currRecipe == null || !(currRecipe.getRecipeId().equals(data.getRecipeId())) ) {
							// starting new recipe, so index prev recipe if prev not null and start new
							
							if ( currRecipe != null ) {
								// need to compile all ingredients, steps, and ratings etc into 
								// recipe so they get indexed
								for ( Iterator itr = ingredients.iterator(); itr.hasNext(); ) {
									RecipeIngredient ri = domainObjectFactory.getRecipeIngredientInstance();
									Ingredient ingred = domainObjectFactory.getIngredientInstance();
									ingred.setIngredientId((Integer)itr.next());
									ri.setIngredient(ingred);
									currRecipe.getIngredient().add(ri);
								}
								for ( Iterator itr = steps.iterator(); itr.hasNext(); ) {
									RecipeStep step = domainObjectFactory.getRecipeStepInstance();
									step.setValue((String)itr.next());
									currRecipe.getStep().add(step);
								}
								for ( Iterator itr = ratingMap.entrySet().iterator(); itr.hasNext(); ) {
									Map.Entry me = (Map.Entry)itr.next();
									Short rating = (Short)me.getValue();
									Integer userId = (Integer)me.getKey();
									RecipeRating recipeRating = domainObjectFactory.getRecipeRatingInstance();
									recipeRating.setRating(rating.shortValue());
									recipeRating.setUserId(userId);
									currRecipe.getRating().add(recipeRating);
								}
							
								indexRecipe(currRecipe,writer,false);
							}
							currRecipe = domainObjectFactory.getRecipeInstance();
							ingredients.clear();
							steps.clear();
							ratingMap.clear();

							currRecipe.setRecipeId(data.getRecipeId());
							currRecipe.setName(data.getName());
							currRecipe.setExcerpt(data.getExcerpt());
							currRecipe.setDirections(data.getDirections());
							
							User owner = domainObjectFactory.getUserInstance();
							owner.setUserId(data.getOwnerId());
							currRecipe.setOwner(owner);
						}
						
						// add recipe step
						if ( data.getStep() != null ) {
							steps.add(data.getStep());
						}
						
						// add recipe ingredients
						if ( data.getIngredientId() != null ) {
							ingredients.add(data.getIngredientId());
						}
						
						// add user recipe rating
						if ( data.getRating() != null && data.getRatingUserId() != null 
								&& !ratingMap.containsKey(data.getRatingUserId()) ) {
							ratingMap.put(data.getRatingUserId(),data.getRating());
						}
					}
					public void finish() {
						if ( currRecipe != null ) {
							indexRecipe(currRecipe,writer,false);
						}
					}
				}
		);
		*/
	}
	
	public IndexResults reindex() {
		final RecipeIndexResults results = new RecipeIndexResults();
		if ( isSingleThreaded() ) {
			try {
				getLucene().doIndexWriterOp(getIndexType(), true, false, true, new IndexWriterOp() {
					public void doWriterOp(String type, IndexWriter writer) {
						doReindexAll(writer, results);
					}
				});
			} finally {
				results.finished = true;
			}
		} else {
			new Thread(new Runnable() {
				public void run() {
					try {
						getLucene().doIndexWriterOp(getIndexType(), true, false, true, new IndexWriterOp() {
							public void doWriterOp(String type, IndexWriter writer) {
								doReindexAll(writer, results);
							}
						});
					} finally {
						results.finished = true;
					}
				}
			}, "RecipesReindex").start();
		}
		return results;
	}

	public SearchResults find(SearchCriteria criteria) {
		Query q = getLuceneQueryForSearchCriteria(criteria);
		return handleStandardSearch(q);
	}
	
	private Query getLuceneQueryForSearchCriteria(SearchCriteria criteria) {
		Query q = null;
		if ( criteria instanceof RecipeSearchCriteria ) {
			BizContext context = null;
			if ( criteria instanceof BizContextHolder ) {
				context = ((BizContextHolder)criteria).getBizContext();
			}
			q = getQuery((RecipeSearchCriteria)criteria, context);
		}
		return q;
	}

	public Object getNativeQuery(SearchCriteria criteria) {
		return getLuceneQueryForSearchCriteria(criteria);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.lucene.LucenePlugin#build(org.apache.lucene.document.Document)
	 */
	public SearchMatch build(Document doc) {
		UiRecipeMatch match = getDomainObjectFactory().getRecipeMatchInstance();
		match.setRecipeId(Long.valueOf(doc.get(IndexField.ITEM_ID.getFieldName())));
		match.setName(doc.get(IndexField.ITEM_NAME.getFieldName()));
		match.setExcerpt(doc.get(IndexField.RECIPE_EXCERPT.getFieldName()));
		match.setDirections(doc.get(IndexField.RECIPE_DIRECTIONS.getFieldName()));
		
		System sys = getDomainObjectFactory().getSystemInstance();
		sys.setValue(doc.get(IndexField.RECIPE_SYSTEM.getFieldName()));
		
		if ( SearchMatch.class.isAssignableFrom(match.getClass()) ) {
			return (SearchMatch)match;
		}
		SearchMatch searchMatch = (SearchMatch)DelegatingInvocationHandler.wrapObject(
				match, SearchMatch.class);
		return searchMatch;
	}

	@SuppressWarnings("unchecked")
	private List<Object> indexRecipe(Recipe recipe, IndexWriter writer) {
		List<Object> errors = new LinkedList<Object>();
		
		if ( recipe == null || recipe.getRecipeId() == null ) {
			// don't bother trying to index null or empty recipe
			String msg = "Null Recipe passed to indexRecipe()... perhaps not available in transaction?";
			log.debug(msg);
			errors.add(msg);
			return errors;
		}
		
		if ( log.isDebugEnabled() ) {
			log.debug("Indexing Recipe " +recipe.getRecipeId()
					+" (" +recipe.getName() +")");
		}
		
		// index general text for searches
		StringBuilder generalText = new StringBuilder();
		
		Document doc = new Document();
		doc.add(new Field(IndexField.ITEM_ID.getFieldName(), recipe.getRecipeId().toString(), 
				Field.Store.YES, Field.Index.UN_TOKENIZED));

		if ( recipe.getName() != null ) {
			doc.add(new Field(IndexField.ITEM_NAME.getFieldName(), recipe.getName(), 
					Field.Store.YES, Field.Index.TOKENIZED));
			generalText.append(recipe.getName()).append(" ");

			// note we tokenize the following index key assuming a tokenizer that 
			// outputs  a single Term is configured since sorting only works on 
			// fields with a single term
			doc.add(new Field(IndexField.ITEM_INDEX_KEY.getFieldName(), recipe.getName(),
					Field.Store.NO, Field.Index.TOKENIZED));
		}
		
		if ( recipe.getExcerpt() != null ) {
			doc.add(new Field(IndexField.RECIPE_EXCERPT.getFieldName(), recipe.getExcerpt(), 
					Field.Store.YES, Field.Index.TOKENIZED));
			generalText.append(recipe.getExcerpt()).append(" ");
		}
		if ( recipe.getDirections() != null ) {
			doc.add(new Field(IndexField.RECIPE_DIRECTIONS.getFieldName(), recipe.getDirections(), 
					Field.Store.YES, Field.Index.TOKENIZED));
			generalText.append(recipe.getDirections()).append(" ");
		}
		
		if ( recipe.getSystem() != null ) {
			doc.add(new Field(IndexField.RECIPE_SYSTEM.getFieldName(), 
					recipe.getSystem().getValue(), 
					Field.Store.YES, Field.Index.UN_TOKENIZED));
		}
		
		if ( recipe.getStep().size() > 0 ) {
			for ( Iterator<RecipeStep> itr = recipe.getStep().iterator(); itr.hasNext(); ) {
				RecipeStep step = itr.next();
				if ( step.getValue() != null ) {
					doc.add(new Field(IndexField.RECIPE_STEP.getFieldName(), 
							step.getValue(), Field.Store.YES, Field.Index.TOKENIZED));
					generalText.append(step.getValue()).append(" ");
				}
			}
		}

		// index recipe ingredient IDs and names so can perform searches for
		// recipes containing specific ingredients
		if ( recipe.getIngredient().size() > 0 ) {
			for ( Iterator<RecipeIngredient> itr = recipe.getIngredient().iterator(); itr.hasNext(); ) {
				RecipeIngredient ri = itr.next();
				if ( ri != null && ri.getIngredient() != null 
						&& ri.getIngredient().getIngredientId() != null ) {
					doc.add(new Field(IndexField.RECIPE_INGREDIENT_ID.getFieldName(), 
							ri.getIngredient().getIngredientId().toString(), 
							Field.Store.YES, Field.Index.UN_TOKENIZED));
					doc.add(new Field(IndexField.RECIPE_INGREDIENT_NAME.getFieldName(), 
							ri.getIngredient().getName(), Field.Store.NO, Field.Index.TOKENIZED));
					generalText.append(ri.getIngredient().getName()).append(" ");
					if ( StringUtils.hasText(ri.getIngredient().getDescription()) ) {
						doc.add(new Field(IndexField.RECIPE_INGREDIENT_NAME.getFieldName(), 
							ri.getIngredient().getDescription(), Field.Store.NO, Field.Index.TOKENIZED));
						generalText.append(ri.getIngredient().getDescription()).append(" ");
					}
				}
			}
		}
		
		// index recipe ratings so can perform searches for recipes
		// by rating. we also create an "average" rating to store in index
		// to help facility grouping recipes by average user rating
		double averageRating = 0;
		if ( recipe.getRating() != null ) {
			int totalRating = 0;
			int ratingCount = 0;
			for ( Iterator<RecipeRating> itr = recipe.getRating().iterator(); itr.hasNext(); ) {
				RecipeRating rr = itr.next();
				if ( rr != null && rr.getUserId() != null && rr.getRating() > 0 ) {
					ratingCount++;
					totalRating += rr.getRating();
					
					// we index as userId|rating
					String ratingStr = rr.getUserId()+"|"+rr.getRating();
					doc.add(new Field(IndexField.RECIPE_RATING.getFieldName(), 
							ratingStr, Field.Store.YES, Field.Index.UN_TOKENIZED));
				}
			}
			if ( ratingCount > 0 ) {
				averageRating = (double)totalRating / (double)ratingCount;
			}
		}
		String averageRatingStr = averageRatingNumberFormat.format(averageRating * 10.0);
		doc.add(new Field(IndexField.RECIPE_RATING_AVERAGE.getFieldName(), 
				averageRatingStr, Field.Store.YES, Field.Index.UN_TOKENIZED));

		// index the owner of the recipe
		if ( recipe.getOwner() != null && recipe.getOwner().getUserId() != null ) {
			doc.add(new Field(IndexField.ITEM_OWNER.getFieldName(), 
					recipe.getOwner().getUserId().toString(), 
					Field.Store.YES, Field.Index.UN_TOKENIZED));
		}
		
		// add the general text
		if ( generalText.length() > 0 ) {
			doc.add(new Field(IndexField.GENERAL_TEXT.getFieldName(),
					generalText.toString(), Field.Store.NO, Field.Index.TOKENIZED));
		}
		
		try {
			writer.addDocument(doc);
			LuceneServiceUtils.publishIndexEvent(
					new IndexEvent(recipe,EventType.UPDATE, getIndexType()), 
					getIndexEventListeners());
		} catch ( IOException e ) {
			throw new RuntimeException("IOException adding recipe to index",e);
		}
		
		return errors;
	}
	
	
	/**
	 * Generate a Lucene Query object for a recipe search criteria.
	 * @param recipeCriteria the criteria
	 * @param context the current context
	 * @return a Lucene Query object
	 */
	@SuppressWarnings("unchecked")
	private Query getQuery(RecipeSearchCriteria criteria, BizContext context) {
		if ( criteria == null ) {
			throw new IllegalArgumentException("Null criteria passed to getQuery()");
		}
		Query result = null;
		if ( RecipeSearchBiz.RECIPE_SEARCH_MODE_SIMPLE.equals(criteria.getMode()) ) {
			if ( StringUtils.hasText(criteria.getSimpleQuery()) ) {
				String query = StringUtil.normalizeWhitespace(criteria.getSimpleQuery()).toLowerCase();
				result = getSimpleRecipeSearchQuery(query);
			}
		} else if ( RecipeSearchBiz.RECIPE_SEARCH_MODE_BY_INGREDIENT.equals(criteria.getMode()) ) {
			if ( criteria.getIngredient().size() > 0 ) {
				BooleanQuery rootQuery = new BooleanQuery();
				for ( Iterator<RecipeIngredient> itr = criteria.getIngredient().iterator(); itr.hasNext(); ) {
					RecipeIngredient ingredientCriteria = itr.next();
					Query ingredientQuery = null;
					if ( ingredientCriteria.getIngredient().getIngredientId() != null
							&& ingredientCriteria.getIngredient().getIngredientId() > 0 ) {
						ingredientQuery = new TermQuery(new Term(
							IndexField.RECIPE_INGREDIENT_ID.getFieldName(),
							ingredientCriteria.getIngredient().getIngredientId().toString()));
					} else if ( StringUtils.hasText(ingredientCriteria.getIngredient().getName()) ) {
						ingredientQuery = new FuzzyQuery(new Term(
								IndexField.RECIPE_INGREDIENT_NAME.getFieldName(),
								ingredientCriteria.getIngredient().getName()));
					}
					
					// note that the RecipeIngredient.optional field's boolean meaning 
					// is reversed here, as it is assumed the search form uses a "required"
					// meaning for this value
					if ( ingredientQuery != null ) {
						rootQuery.add(ingredientQuery,ingredientCriteria.isOptional()
							? Occur.MUST : Occur.SHOULD);
					}
				}
				result = rootQuery;
			}
		} else if ( RecipeSearchBiz.RECIPE_SEARCH_MODE_ADVANCED.equals(criteria.getMode()) ) {
			result = getAdvancedRecipeSearchQuery(criteria, context);
		} else {
			if ( !StringUtils.hasText(criteria.getName()) ) {
				throw new IllegalArgumentException(
						"Illegal criteria passed to getQuery()");
			}
			result = getLucene().parseQuery(getIndexType(), '"'+criteria.getName()+'"');
		}
		return result;
	}
	
	@SuppressWarnings("unchecked")
	private Query getAdvancedRecipeSearchQuery(RecipeSearchCriteria criteria, BizContext context) {
		BooleanQuery rootQuery = new BooleanQuery();
		for ( Iterator<AdvancedSearchCriteria> itr = criteria.getAdvanced().iterator(); itr.hasNext(); ) {
			AdvancedSearchCriteria asc = itr.next();
			if ( asc.getType() == null || asc.getType().length() < 1 ) {
				throw new IllegalArgumentException("No advanced search type specified.");
			}
			char type = asc.getType().charAt(0);
			boolean prohibited = false;
			switch ( asc.getOp() ) {
				case RecipeSearchBiz.ADVANCED_SEARCH_OP_IS_NOT:
				case RecipeSearchBiz.ADVANCED_SEARCH_OP_DOES_NOT_CONTAIN:
					prohibited = true;
					break;
			}
			Query ascQuery = null;
			switch ( type ) {
				case RecipeSearchBiz.ADVANCED_SEARCH_AUTHOR_TYPE:
					ascQuery = getOwnerQuery(asc.getQuery());
					break;
				
				case RecipeSearchBiz.ADVANCED_SEARCH_RATING_TYPE:
					ascQuery = getAdvancedRecipeSearchAverageRatingQuery(asc);
					break;
				
				case RecipeSearchBiz.ADVANCED_SEARCH_TEXT_TYPE:
					if ( StringUtils.hasText(asc.getQuery()) ) {
						ascQuery = getSimpleRecipeSearchQuery(
								StringUtil.normalizeWhitespace(asc.getQuery()).toLowerCase());
					}
					break;
				
				case RecipeSearchBiz.ADVANCED_SEARCH_USER_RATING_TYPE:
					ascQuery = getAdvancedRecipeSearchUserRatingQuery(asc,
							context.getActingUser());
					break;
				
				default:
					throw new IllegalArgumentException("Unknown search type: " +asc.getType());
			}
			if ( ascQuery != null ) {
				if ( prohibited && !asc.isRequired() ) {
					ascQuery.setBoost(0.01f);
				} else {
					ascQuery.setBoost(1.0f);
				}
				rootQuery.add(ascQuery, 
						asc.isRequired() && prohibited ? Occur.MUST_NOT 
						: asc.isRequired() && !prohibited 
						? Occur.MUST : Occur.SHOULD);
			}
		}
		return rootQuery;
	}

	/**
	 * Get a Query for finding recipes that match some text.
	 * 
	 * <p>This method will return a fuzzy Query that will match against the "general text"
	 * field of a Recipe object, which contains terms from the <code>name</code>,
	 * <code>description</code>, and <code>excerpt</code>, and ingredient properties.</p>
	 * 
	 * @param query the text to search for
	 * @return a Query
	 */
	private Query getSimpleRecipeSearchQuery(String query) {
		BooleanQuery rootQuery = new BooleanQuery();
		getLucene().addTokenizedFuzzyQuery(rootQuery,query,
				IndexField.GENERAL_TEXT.getFieldName(),getIndexType());
		
		if ( log.isDebugEnabled() ) {
			log.debug("Generated simple recipe Lucene query: " +rootQuery);
		}
		
		return rootQuery;
	}
	
	/**
	 * Add a Query to find recipes that match a given average user rating.
	 * 
	 * <p>The average user rating for a recipe is stored in the index, multiplied 
	 * by <code>10</code> and rounded to an integer. This method will treat the 
	 * {@link AdvancedSearchCriteria#getChoice()} value as an integer representing 
	 * the desired rating to search for (as a normal rating scale, i.e. not multipled
	 * by 10). For the {@link RecipeSearchBiz#ADVANCED_SEARCH_OP_IS} search operation
	 * the search query will look for a range of ratings that round to the specified 
	 * integer rating. For example specifing a rating of <code>3</code> will result 
	 * in a query that matches an average rating between <code>2.5</code> and 
	 * <code>3.4</code> as that range rounds to the integer value of <code>3</code>.</p>
	 * 
	 * @param asc the advanced search criteria
	 * @param rootQuery the root query to add the average rating query to
	 */
	private Query getAdvancedRecipeSearchAverageRatingQuery(AdvancedSearchCriteria asc) {
		int choice = 0;
		try {
			choice = Integer.parseInt(asc.getChoice());
		} catch ( Exception e ) {
			// ignore
		}
		Query ascQuery = null;
		switch ( asc.getOp() ) {
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_IS:
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_IS_NOT:
				String low = averageRatingNumberFormat.format((choice - 0.5) * 10.0);
				String high = averageRatingNumberFormat.format((choice + 0.4) * 10.0);
				ascQuery = new RangeQuery(
							new Term(IndexField.RECIPE_RATING_AVERAGE.getFieldName(),low),
							new Term(IndexField.RECIPE_RATING_AVERAGE.getFieldName(),high), 
							false);
				break;
				
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_LESS_THAN:
				ascQuery = new RangeQuery(
						null,
						new Term(IndexField.RECIPE_RATING_AVERAGE.getFieldName(),
								String.valueOf(choice * 10)),
						false);
				break;
				
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_GREATER_THAN:
				ascQuery = new RangeQuery(
						new Term(IndexField.RECIPE_RATING_AVERAGE.getFieldName(),
								String.valueOf(choice * 10)),
						null,
						false);
				break;
				
			default:
				throw new IllegalArgumentException("The advanced search operation " +asc.getOp()
						+" is not supported in average rating searches.");
		}
		return ascQuery;
	}
	
	/**
	 * Add a Query to find recipes with a specific user's rating.
	 * 
	 * <p>The <code>asc</code> parameter must have a logical <code>Integer</code>
	 * value returned by the {@link AdvancedSearchCriteria#getChoice()} method.
	 * This value will be treated as the <em>rating</em> to use for the search 
	 * query, along with the <code>userId</code> supplied by the <code>user</code>
	 * parameter.</p>
	 * 
	 * @param asc the advanced search criteria
	 * @param rootQuery the root Query to add the user rating query to
	 * @param user the user to find the recipe ratings for
	 * @throws IllegalArgumentException if the <code>user</code> or <code>userId</code>
	 * of that object are <em>null</em>
	 */
	private Query getAdvancedRecipeSearchUserRatingQuery(AdvancedSearchCriteria asc, User user) {
		int rating = Integer.parseInt(asc.getChoice());
		if ( user == null || user.getUserId() == null ) {
			throw new IllegalArgumentException("Valid acting user not available in context.");
		}

		// Since we can't perform range queries because of the <user>|<rating> notation
		// we "fake" ranges by generating boolean clauses for each possible rating 
		// in the range. This isn't too bad since there are so few possibilities.
		
		int low = rating;
		int high = rating + 1;
		switch ( asc.getOp() ) {
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_GREATER_THAN:
				low = rating + 1;
				high = maxRating + 1;
				break;
				
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_LESS_THAN:
				low = 1;
				high = rating;
				break;
				
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_IS:
			case RecipeSearchBiz.ADVANCED_SEARCH_OP_IS_NOT:
				// nothing to do
				break;
			
			default:
				throw new IllegalArgumentException("The advanced search operation " +asc.getOp()
						+" is not supported in user rating searches.");
		}
		
		BooleanQuery myRootQuery = new BooleanQuery();
		for ( int i = low; i < high; i++ ) {
			String key = user.getUserId()+"|"+i;
			myRootQuery.add(new TermQuery(new Term(IndexField.RECIPE_RATING.getFieldName(),
					key)), Occur.SHOULD);
		}
		return myRootQuery;
	}
	
	private Query getOwnerQuery(String login) {
		User u = userDao.getUser(login);
		if ( u == null ) {
			return new TermQuery(new Term(IndexField.ITEM_OWNER.getFieldName(),""));
		}
		return new TermQuery(new Term(IndexField.ITEM_OWNER.getFieldName(),
				u.getUserId().toString()));
	}

	private final class RecipeIndexResults implements IndexResults {

		private int count = 0;
		private Map<Long, String> errors = new LinkedHashMap<Long, String>();
		private boolean finished = false;

		public Map<? extends Serializable, String> getErrors() {
			return errors;
		}

		public int getNumIndexed() {
			return count - errors.size();
		}

		public int getNumProcessed() {
			return count;
		}

		public boolean isFinished() {
			return finished;
		}

	}

	/**
	 * @return the tidbitDao
	 */
	public RecipeDao getRecipeDao() {
		return recipeDao;
	}
	
	/**
	 * @param tidbitDao the tidbitDao to set
	 */
	public void setRecipeDao(RecipeDao tidbitDao) {
		this.recipeDao = tidbitDao;
	}
	
	/**
	 * @return the averageRatingNumberFormat
	 */
	public NumberFormat getAverageRatingNumberFormat() {
		return averageRatingNumberFormat;
	}
	
	/**
	 * @param averageRatingNumberFormat the averageRatingNumberFormat to set
	 */
	public void setAverageRatingNumberFormat(NumberFormat averageRatingNumberFormat) {
		this.averageRatingNumberFormat = averageRatingNumberFormat;
	}

	/**
	 * @return the maxRating
	 */
	public int getMaxRating() {
		return maxRating;
	}
	
	/**
	 * @param maxRating the maxRating to set
	 */
	public void setMaxRating(int maxRating) {
		this.maxRating = maxRating;
	}
	
	/**
	 * @return the userDao
	 */
	public UserDao getUserDao() {
		return userDao;
	}
	
	/**
	 * @param userDao the userDao to set
	 */
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	
	/**
	 * @return the reindexPageSize
	 */
	public int getReindexPageSize() {
		return reindexPageSize;
	}
	
	/**
	 * @param reindexPageSize the reindexPageSize to set
	 */
	public void setReindexPageSize(int reindexPageSize) {
		this.reindexPageSize = reindexPageSize;
	}

}
