/* Copyright (c) 2006 Google Inc.
*
* 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 sample.gbase.recipe;
import com.google.api.gbase.client.FeedURLFactory;
import com.google.api.gbase.client.GoogleBaseEntry;
import com.google.api.gbase.client.GoogleBaseFeed;
import com.google.api.gbase.client.GoogleBaseQuery;
import com.google.api.gbase.client.GoogleBaseService;
import com.google.gdata.util.ServiceException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* A recipe search.
*
* There will be such an object in all cases, even
* if no query has been run. This object is created
* by RecipeSearchServlet and displayed by the JSP.
*/
public class RecipeSearch {
private final GoogleBaseService service;
private Set<String> mainIngredient;
private Set<String> cuisine;
private Integer cookingTime;
private String query;
/** The query string, with the unsupported characters replaced with spaces.*/
private String queryClean;
/** The index of the first retrieved recipe. */
private int startIndex = 0;
/** Set to true to perform the search on the user's items. */
private boolean ownItems;
/** Total number of results, -1 means the query hasn't been run yet. */
protected int total = -1;
protected List<Recipe> recipes;
private static final int DEFAULT_MAX_RESULTS = 10;
private int maxResults = DEFAULT_MAX_RESULTS;
private final FeedURLFactory urlFactory;
/**
* Create a new search.
*
* @param service Google data API service
* @param urlFactory feed URL factory to be used when creating a Query
* @param ownItems true to show only the items of the authenticated user
*/
public RecipeSearch(GoogleBaseService service,
FeedURLFactory urlFactory,
boolean ownItems) {
this.service = service;
this.urlFactory = urlFactory;
this.ownItems = ownItems;
mainIngredient = null;
cuisine = null;
cookingTime = null;
query = null;
queryClean = null;
}
/**
* Checks whether the search has been run and there is at least one result.
*
* @return true if the search has been run and there is at least one result
*/
public boolean hasResults() {
return recipes != null && ! recipes.isEmpty();
}
/**
* Gets the result of the search, if it has been run.
*
* @return a list of Recipe which might be empty if the
* search has been run, or null if the search has not
* been run yet
*/
public List<Recipe> getRecipes() {
return recipes;
}
/**
* Checks if the search will be done for the authenticated user's items only.
*
* @return true if the search returns only the items that belong to the
* authenticated user
*/
public boolean isOwnItems() {
return ownItems;
}
/**
* Specifies if we are searching the authenticated user's items.
*
* @param ownItems true to search only the authenticated user's items
*/
public void setOwnItems(boolean ownItems) {
this.ownItems = ownItems;
}
/** Gets the current main ingredients, or null. */
public Set<String> getMainIngredientValues() {
return mainIngredient;
}
/** Sets the main ingredient. */
public void setMainIngredientValues(String[] mainIngredient) {
this.mainIngredient = RecipeUtil.validateValues(mainIngredient);
}
/** Gets the current cuisines, or null. */
public Set<String> getCuisineValues() {
return cuisine;
}
/** Sets the cuisine. */
public void setCuisineValues(String[] cuisine) {
this.cuisine = RecipeUtil.validateValues(cuisine);
}
/** Gets the current (maximum) cooking time, or null. */
public Integer getCookingTime() {
return cookingTime;
}
/** Sets the current maximum cooking time. */
public void setCookingTime(Integer cookingTime) {
this.cookingTime = cookingTime;
}
/** Gets the current page length. */
public int getMaxResults() {
return maxResults;
}
/** Sets the page length. */
public void setMaxResults(int maxResults) {
this.maxResults = maxResults;
}
/**
* Gets the total number of recipes that matched the query, which
* might be larger than the page length.
*
* @return the total, or -1 if the total is unknown, either because
* the query has not been run or because the total was not in
* the result
*/
public int getTotal() {
return total;
}
/**
* Gets the index of the first result to return.
*
* @return a positive value
*/
public int getStartIndex() {
return startIndex;
}
/**
* Sets the index of the first result to return.
* @param startIndex a positive value
*/
public void setStartIndex(int startIndex) {
this.startIndex = startIndex;
}
/** Gets the current page. */
public int getCurrentPage() {
return startIndex / maxResults;
}
/**
* Gets a description of the retrieved (current) interval, showing
* the index of the first item and the index of the last item.
*
* @return short description of the current page interval
*/
public String getCurrentPageInterval() {
return "" + (startIndex + 1) + " - " +
Math.min(startIndex + maxResults, total);
}
/** Gets the number of pages needed to contain all the results. */
public int getTotalPages() {
if (total == 0) {
return 0;
}
/* Using total-1, because if we have 10 results and 10 items per page,
* we still want one page.
*/
return (total - 1) / maxResults;
}
/** Runs the query and fills the result. */
public void runQuery() throws IOException, ServiceException {
GoogleBaseQuery query = createQuery();
System.out.println("Searching: " + query.getUrl());
GoogleBaseFeed feed = service.query(query);
List<Recipe> result = new ArrayList<Recipe>(maxResults);
for (GoogleBaseEntry entry : feed.getEntries()) {
result.add(new Recipe(entry));
}
this.recipes = result;
total = feed.getTotalResults();
}
/**
* Creates a GoogleBaseQuery that searches for recipes, according to
* the various properties of the RecipeSearch.
*
* @return a query to be used for querying with a
* {@link com.google.api.gbase.client.GoogleBaseService
* GoogleBaseService}
* @see com.google.api.gbase.client.GoogleBaseService#query(com.google.gdata.client.Query)
*/
private GoogleBaseQuery createQuery() {
URL queryUrl;
if (ownItems) {
queryUrl = urlFactory.getItemsFeedURL();
} else {
queryUrl = urlFactory.getSnippetsFeedURL();
}
GoogleBaseQuery query = new GoogleBaseQuery(queryUrl);
query.setMaxResults(maxResults);
if (startIndex > 0) {
// the first index is 1
query.setStartIndex(startIndex + 1);
}
query.setGoogleBaseQuery(createQueryString());
return query;
}
/**
* Creates a full text query out of the values of the query, mainIngredient,
* cuisine and cookingTime.
*
* @return a query to be used for setting the full text query of a
* {@link com.google.api.gbase.client.GoogleBaseQuery}
* @see com.google.api.gbase.client.GoogleBaseQuery#setFullTextQuery(String)
*/
private String createQueryString() {
StringBuffer retval = new StringBuffer(RecipeUtil.RECIPE_ITEMTYPE_QUERY);
if (queryClean != null) {
retval.append(queryClean);
}
appendAttributeCondition(retval, "main ingredient", mainIngredient, true);
appendAttributeCondition(retval, "cuisine", cuisine, false);
if (cookingTime != null) {
Collection<String> cookingTimes = new ArrayList<String>();
cookingTimes.add("0.." + cookingTime + " min");
cookingTimes.add("0.." + cookingTime + " minutes");
appendAttributeCondition(retval, "cooking time", cookingTimes, false);
}
return retval.toString();
}
/**
* Appends a filtering condition to a full text query.
* It is composed by simple [name: value] conditions
* joined by and AND or an OR operation.
*
* @param sb a StringBuffer for creating a full text query
* @param name name of the attributes
* @param values values the attributes have to match
* @param isAnd true if the attributes have to match all the values,
* false if the attributes have to match at least one value
*/
private static void appendAttributeCondition(StringBuffer sb,
String name,
Collection<String> values,
boolean isAnd) {
if (values != null && !values.isEmpty()) {
sb.append(" (");
Iterator iter = values.iterator();
while (iter.hasNext()) {
sb.append("[").append(name).append(": ").append(iter.next()).append("]");
if (iter.hasNext()) {
sb.append(isAnd ? " " : "|");
}
}
sb.append(")");
}
}
/** Returns true when the current page is not the first page. */
public boolean hasPreviousPage() {
return getCurrentPage() > 0;
}
/** Returns true when the current page is not the last one. */
public boolean hasNextPage() {
return getTotalPages() > getCurrentPage();
}
/** Gets the expected number of recipes for the next page. */
public int getNextPageSize() {
return Math.min(maxResults, total - (getCurrentPage() + 1) * maxResults);
}
/** Gets a description of the query used in the search. */
public StringBuffer getFilterDescription() {
StringBuffer retval = new StringBuffer();
if (queryClean != null && ! "".equals(queryClean)) {
retval.append("<b>keywords</b> are <b>").
append(queryClean).
append("</b> ");
}
addCollectionDescription(retval, cuisine, "cuisine", false);
addCollectionDescription(retval, mainIngredient, "main ingredient", true);
if (cookingTime != null) {
if (retval.length() > 0) {
retval.append("and ");
}
retval.append("<b>cooking time</b> is under <b>").
append(cookingTime).
append(" ").
append(RecipeUtil.COOKING_TIME_UNIT).
append("</b> ");
}
if (retval.length() > 0) {
retval.insert(0, "where ");
}
return retval;
}
private static void addCollectionDescription(StringBuffer buffer,
Collection<String> collection,
String name,
boolean isAnd) {
if (collection != null && ! collection.isEmpty()) {
if (buffer.length() > 0) {
buffer.append("and ");
}
buffer.append("<b>").append(name).append("</b> is");
Iterator<String> iter = collection.iterator();
while (iter.hasNext()) {
buffer.append(" <b>").append(iter.next()).append("</b> ");
if (iter.hasNext()) {
buffer.append(isAnd ? "and " : "or ");
}
}
}
}
/**
* Sets the query string.
*
* @param query the query string, as provided by user
* @throws NullPointerException if the {@code query} is null.
*/
public void setQuery(String query) {
if (query != null) {
this.query = query;
this.queryClean = RecipeUtil.cleanQueryString(query);
} else {
throw new NullPointerException("Query must not be null.");
}
}
/**
* Returns the original query string, as specified in the
* {@link #setQuery(String)} method.
*/
public String getQuery() {
return query;
}
}