/* 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; } }