Inital commit

This commit is contained in:
Tilman
2011-10-26 09:44:02 +02:00
commit 5971f4b417
1813 changed files with 737837 additions and 0 deletions
@@ -0,0 +1,348 @@
/* 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.GoogleBaseService;
import com.google.gdata.client.http.AuthSubUtil;
import com.google.gdata.util.AuthenticationException;
import java.io.IOException;
import java.net.URL;
import java.net.MalformedURLException;
import java.security.GeneralSecurityException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Provides an authenticated GoogleBaseService
* to the servlet that will process the request.
*/
public class AuthenticationFilter implements Filter {
/**
* The request attribute that contains the authenticated service.
*/
static final String SERVICE_ATTRIBUTE = "googleBaseService";
/**
* The parameter used by AuthSub to specify the authentication token.
* Also used as the name of the http session attribute that contains
* the session token.
*/
static final String TOKEN_PARAMETER = "token";
static final String TOKEN_COOKIE_NAME = "AuthSubSessionToken";
static final String DEFAULT_AUTHSUB_PROTOCOL = "https";
static final String DEFAULT_AUTHSUB_HOSTNAME = "www.google.com";
protected String authsubProtocol;
protected String authsubHostname;
protected FeedURLFactory urlFactory;
protected String applicationName;
/**
* Developer key, used for identification against the Google Base data
* API servers.
*/
protected String key;
private ServletContext servletContext;
public void init(FilterConfig filterConfig) throws ServletException {
servletContext = filterConfig.getServletContext();
key = servletContext.getInitParameter(RecipeUtil.DEVELOPER_KEY_PARAMETER);
if (key == null || "".equals(key.trim())) {
String errorMessage = "No developer key specified.\n Please edit " +
"web.xml and add your developer key in the \"key\" context " +
"parameter. \n You can obtain a developer key at: \n\t" +
"http://code.google.com/api/base/signup.html";
System.err.println(errorMessage);
throw new ServletException(errorMessage);
}
urlFactory = (FeedURLFactory)
servletContext.getAttribute(RecipeListener.FEED_URL_FACTORY_ATTRIBUTE);
authsubProtocol = filterConfig.getInitParameter("authsubProtocol");
if (authsubProtocol == null) {
authsubProtocol = DEFAULT_AUTHSUB_PROTOCOL;
}
authsubHostname = filterConfig.getInitParameter("authsubHostname");
if (authsubHostname == null) {
authsubHostname = DEFAULT_AUTHSUB_HOSTNAME;
}
}
public void destroy() {
servletContext = null;
urlFactory = null;
applicationName = null;
authsubProtocol = null;
authsubHostname = null;
}
/**
* Starts or stops an authenticated session, depending on the value
* of the {@value #TOKEN_PARAMETER} parameter, or provides the servlets
* with an authenticated
* {@link com.google.api.gbase.client.GoogleBaseService GoogleBaseService},
* during an authenticated session.
*/
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String oneTimeToken = httpRequest.getParameter(TOKEN_PARAMETER);
String sessionToken = getSessionTokenCookie(httpRequest);
if (oneTimeToken != null) {
if ("".equals(oneTimeToken)) {
// Revoke the session token
if (sessionToken != null) {
stopAuthenticatedSession(httpRequest, httpResponse, sessionToken);
return;
}
} else {
// Convert the token to a session token and keep it
try {
startAuthenticatedSession(httpRequest, httpResponse, oneTimeToken);
return;
} catch (GeneralSecurityException e) {
throw new ServletException(e);
} catch (AuthenticationException e) {
// Log and then continue as if this token was not there.
// (It was probably bookmarked.)
servletContext.log("Invalid one-time token", e);
}
}
}
// Request a new token if we don't have one at this point
if (sessionToken == null) {
redirectToAuthSub(httpRequest, httpResponse);
return;
}
// Create a service that authenticates using the session token
GoogleBaseService service = new GoogleBaseService(
applicationName, key, authsubProtocol, authsubHostname);
service.setAuthSubToken(sessionToken);
// Make the service available to the servlet
httpRequest.setAttribute(SERVICE_ATTRIBUTE, service);
// Execute the servlet
try {
filterChain.doFilter(request, response);
} catch(ServletException e) {
Throwable cause = e.getRootCause();
if (cause instanceof AuthenticationException &&
!response.isCommitted()) {
// Token has been revoked. Re-run AuthSub.
redirectToAuthSub(httpRequest, httpResponse);
} else {
// Let the exception be handled as usual.
throw e;
}
}
}
/**
* Gets the AuthSub session token from a cookie.
*
* @param httpRequest
* @return session token or null
*/
String getSessionTokenCookie(HttpServletRequest httpRequest) {
Cookie[] cookies = httpRequest.getCookies();
if (cookies == null) {
return null;
}
for (Cookie cookie : cookies) {
if (cookie.getName().equals(TOKEN_COOKIE_NAME)) {
return cookie.getValue();
}
}
return null;
}
/**
* Revokes the specified session token and redirects the browser to
* the context of the request.
*
* @param request
* @param response
* @param sessionToken a session token
* @throws IOException
* @throws ServletException
*/
private void stopAuthenticatedSession(HttpServletRequest request,
HttpServletResponse response,
String sessionToken)
throws IOException, ServletException {
if (sessionToken != null) {
revokeSessionToken(sessionToken);
clearSessionTokenCookie(request, response);
}
String url = request.getContextPath();
response.sendRedirect(response.encodeRedirectURL(url));
}
/**
* Force cookie expiration by sending an expired cookie to
* the browser.
*
* @param request
* @param response
*/
private void clearSessionTokenCookie(HttpServletRequest request,
HttpServletResponse response)
throws ServletException {
response.addCookie(newExpiredSessionTokenCookie(request));
}
private Cookie newExpiredSessionTokenCookie(HttpServletRequest request)
throws ServletException {
Cookie cookie = newSessionTokenCookie(request, "");
cookie.setMaxAge(0);
return cookie;
}
/**
* Explicitely revoke the session token.
*
* @param token
* @throws IOException
* @throws ServletException
*/
protected void revokeSessionToken(String token) throws IOException, ServletException
{
try {
AuthSubUtil.revokeToken(authsubProtocol, authsubHostname, token, null);
} catch (AuthenticationException e) {
throw new ServletException(e);
} catch (GeneralSecurityException e) {
throw new ServletException(e);
}
}
/**
* Exchanges the single use token for a session token and
* redirects the browser to the same address with the
* token specification part removed.
*
* @param request
* @param response
* @param oneTimeToken a single use AuthSub token
* @throws IOException
* @throws GeneralSecurityException
* @throws AuthenticationException if the one-time token is invalid
*/
private void startAuthenticatedSession(HttpServletRequest request,
HttpServletResponse response,
String oneTimeToken)
throws IOException, GeneralSecurityException, AuthenticationException,
ServletException {
String sessionToken = exchangeForSessionToken(oneTimeToken);
// Store the authentication token in a cookie
response.addCookie(newSessionTokenCookie(request, sessionToken));
// Redirect the browser to the same address
// with the "token=value" part removed from the query string
StringBuffer url = request.getRequestURL();
String queryString = request.getQueryString();
if (queryString != null) {
queryString = queryString.replaceFirst("token=[^&]*&?", "");
if (queryString.length() > 0) {
url.append("?").append(queryString);
}
}
response.sendRedirect(response.encodeRedirectURL(url.toString()));
}
protected Cookie newSessionTokenCookie(HttpServletRequest request,
String sessionToken)
throws ServletException {
Cookie cookie = new Cookie(TOKEN_COOKIE_NAME, sessionToken);
// AuthSub session tokens effectively don't expire. Hang on to it.
// If the AuthSub token is revoked, the filter will request
// a new token.
cookie.setMaxAge(365*24*60*60);
cookie.setPath(request.getContextPath() + "/");
try {
cookie.setDomain(new URL(request.getRequestURL().toString()).getHost());
} catch(MalformedURLException e) {
throw new ServletException(e);
}
// Cookie domain set automatically by the server.
return cookie;
}
/**
* Converts a one-time token into a reusable session token.
* @param oneTimeToken
* @throws IOException
*/
protected String exchangeForSessionToken(String oneTimeToken)
throws IOException, GeneralSecurityException, AuthenticationException {
return AuthSubUtil.exchangeForSessionToken(authsubProtocol,
authsubHostname,
oneTimeToken,
null);
}
/**
* Redirects to the AuthSub authentication page.
*
* @param request
* @param response
* @throws IOException
*/
private void redirectToAuthSub(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
StringBuffer next = request.getRequestURL();
String queryString = request.getQueryString();
if (queryString != null && !"".equals(queryString) ) {
next.append("?").append(queryString);
}
String scope = new URL(urlFactory.getBaseURL(), "feeds").toExternalForm();
String url = AuthSubUtil.getRequestUrl(authsubProtocol,
authsubHostname,
next.toString(),
scope,
false,
true);
response.sendRedirect(response.encodeRedirectURL(url));
}
}
@@ -0,0 +1,254 @@
/* 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 java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
/**
* Methods called from the JSP for generating HTML code.
*
* This is the work normally done by a web framework.
*/
public class DisplayUtils {
/**
* Prints checkboxes on two colums, with a text input field at the end.
* The checked values will be displayed along with the values to be showed.
*
* The text input field can be used to input a custom value. It has the
* same name as the checkbox input fields, so when the submitted values
* are processed, you have to remove the empty value, in case this field
* remains empty.
*
* @param out output writer
* @param name name of the input field
* @param values values to be shown
* @param checked checked values
* @throws IOException
*/
public static void printCheckboxes(Writer out,
String name,
String[] values,
Set<String> checked) throws IOException {
Set<String> allValues = new TreeSet<String>();
// The displayed values are the static values plus the checked values
allValues.addAll(Arrays.asList(values));
if (checked == null) {
checked = new HashSet<String>();
}
allValues.addAll(checked);
int middle = (allValues.size() + 2) / 2;
int row = 0;
out.write("<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">");
out.write("<tr><td><ul class=\"inputlist\">");
for (String value : allValues) {
if (row == middle) {
out.write("</ul></td><td><ul class=\"inputlist\">");
}
out.write("<li>");
printCheckbox(out, name, value, checked.contains(value));
out.write("</li>");
row++;
}
out.write("<li><label>Other:<br/><input id=\"other");
out.write(name);
out.write("\" type=\"text\" name=\"");
out.write(name);
out.write("\" value=\"\" size=\"15\" class=\"txt\"></label></li>");
out.write("</ul></td></tr></table>");
}
/**
* Prints a checkbox input field with a label.
*
* @param out output writer
* @param name name of the field
* @param value value of the field
* @param checked true if the field is checked
* @throws IOException
*/
public static void printCheckbox(Writer out, String name, String value,
boolean checked) throws IOException {
value = escape(value);
out.write("<label><input type=\"checkbox\" name=\"");
out.write(name);
out.write("\" value=\"");
out.write(value);
out.write("\"");
if (checked) {
out.write(" checked");
}
out.write("/>&nbsp;");
if (checked) {
out.write("<b>");
}
out.write(value);
if (checked) {
out.write("</b>");
}
out.write("</label>");
}
/**
* Escapes the items of the Collection and returns them in a StringBuffer,
* separated by commas.
*
* @param list
* @return list of the elements of the collection, sepparated by commas
*/
public static StringBuffer printList(Collection<String> list) {
StringBuffer sb = new StringBuffer();
if (list != null && list.size() > 0) {
Iterator<String> iter = list.iterator();
while (true) {
sb.append(escape(iter.next()));
if (iter.hasNext()) {
sb.append(", ");
} else {
break;
}
}
}
return sb;
}
/**
* Prints &lt;option&gt; tags and set the selected option.
*
* @param out output writer
* @param names option names, which are used both for
* values and for labels
* @param current name of the current option
* @throws IOException
*/
public static void printOptions(Writer out,
String[] names,
String current) throws IOException {
for (int i=0; i<names.length; i++) {
String name = names[i];
printOption(out, name, name, name.equals(current));
}
printOption(out, "all", "", current==null);
}
/**
* Prints one &lt;option&gt; tag and select it if necessary.
*
* @param out output writer
* @param label option label
* @param value option value
* @param selected true if it's selected
* @throws IOException
*/
public static void printOption(Writer out,
String label,
String value,
boolean selected) throws IOException {
out.write("<option");
if (selected) {
out.write(" selected");
}
out.write(" value=\"");
out.write(value);
out.write("\">");
out.write(escape(label));
out.write("</option>");
}
/**
* Simple HTML escaping of the String version of the specified Object.
*
* @param obj Object that has a meaningful toString() value
* @return string with some escaped characters
*/
public static String escape(Object obj) {
if (obj == null) {
return "";
}
return escapeAndShorten(obj.toString(), -1);
}
/**
* Simple HTML escaping.
*
* Escapes &lt; &amp; and &gt; and leaves the rest as it is.
*
* @param raw
* @return string with some escaped characters
*/
public static String escape(String raw) {
return escapeAndShorten(raw, -1);
}
/**
* Escape HTML data and shorted the result if necessary.
*
* If the text is escaped, &lt;b&gt;...&lt;/b&gt; will be
* appended.
*
* @param raw raw text to escape
* @param maxLength maximum output length
* @return HTML code
*/
public static String escapeAndShorten(String raw, int maxLength) {
if (raw == null) {
return "";
}
StringBuilder retval = new StringBuilder();
int length = raw.length();
boolean shortened = false;
if (maxLength != -1 && length > maxLength) {
length = maxLength;
shortened = true;
}
for (int i=0; i<length; i++) {
char c = raw.charAt(i);
switch (c) {
case '<':
retval.append("&lt;");
break;
case '>':
retval.append("&gt;");
break;
case '&':
retval.append("&amp;");
break;
case '\'':
retval.append("&#039;");
break;
case '"':
retval.append("&#034;");
break;
default:
retval.append(c);
break;
}
}
if (shortened) {
retval.append("<b>...</b>");
}
return retval.toString();
}
}
@@ -0,0 +1,318 @@
/* 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.AttributeHistogram;
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.api.gbase.client.MetadataEntryExtension;
import com.google.api.gbase.client.AttributeHistogram.UniqueValue;
import com.google.gdata.util.ServiceException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.ServletContext;
/**
* Holds the most used values of attributes in Google Base and
* periodically refreshes them.
*
* A MostUsedValues object is focused on a GoogleBaseService and
* a FeedURLFactory. One object can be used to analyse only the items that
* match a specific query.
*
* The class currently works for TEXT attributes only.
*
* The cache() and clear() calls are synchronized,
* so that the different Maps and Collections remain in sync.
*
* Typically, right after you create a MostUsedValues object, you call the
* {@link #cache(long, int, ServletContext, String[])} method to set it up
* to cache the most used values of your attributes. This creates
* {@link java.util.TimerTask} objects that periodically refresh the cached
* values. After this, you use {@link #getMostUsedValuesForAttribute(String)}
* to get the most used values for the attributes you previously specified.
* In the end, when you no longer need the cache, you can call
* {@link #clear()} to stop the TimerTask objects and clear the cache.
*/
public class MostUsedValues {
protected static final String TEXT_TYPE = "(text)";
/**
* The initial value of the max values limit when making a query;
* the step used to increase that limit if some of the attributes
* are not found.
*/
protected static final int STEP_MAXRESULTS = 25;
/**
* The max of the max values limit when making a query.
*/
protected static final int MAX_MAXRESULTS = 200;
/**
* The cached most used values.
* This is a synchronized map.
*/
private java.util.Map<String, String[]> mostUsedValues;
/**
* The timers that periodically refresh the cache.
* The access to this object has to be synchronized.
*/
private Collection<Timer> timers;
private GoogleBaseService service;
private FeedURLFactory urlFactory;
private String queryString;
/**
* Creates an empty MostUsedValues.
*
* @param service any GoogleBaseService used to retrieve attribute histograms
* @param urlFactory a FeedURLFactory used to create the URLs of
* the attribute histograms
* @param queryString the query string used to filter the analyzed items,
* for example one that focuses only on the items that
* have a specific item type.
*/
public MostUsedValues(GoogleBaseService service,
FeedURLFactory urlFactory,
String queryString) {
this.service = service;
this.urlFactory = urlFactory;
this.queryString = queryString;
mostUsedValues = new Hashtable<String, String[]>();
timers = new ArrayList<Timer>();
}
/**
* Gets the cached most used values of an attribute.
*
* @param attrName the name of the attribute
* @return a cached list of the most used values
*/
public String[] getMostUsedValuesForAttribute(String attrName) {
return mostUsedValues.get(attrName);
}
/**
* Sets up the object to cache a limited number of the most used values of
* each of the specified attributes; the cache is refreshed periodically.
*
* This method does not check if the specified attributes are already cached.
* You have to make sure an attribute is specified only once in your calls.
*
* @param interval the cache refresh period, in millis
* @param maxValues the maximum number of values to cache
* @param servletContext a ServletContext to be used for logging
* @param attrNames the names of the attributes to be cached
*/
synchronized public void cache(long interval,
final int maxValues,
ServletContext servletContext,
final String... attrNames) {
if (attrNames.length == 0) {
return;
}
Timer timer = new Timer(true);
TimerTask task = createRefresher(maxValues, servletContext, attrNames);
task.run();
timer.schedule(task, interval, interval);
timers.add(timer);
}
/**
* Creates a TimerTask that refreshes the cached most used values of
* the specified attributes.
*
* @param maxValues how many values to cache for each attribute
* @param servletContext servlet context for logging error messages
* @param attrNames the names of the attributes
* @return a TimerTask that refreshes the cache
*/
private TimerTask createRefresher(final int maxValues,
final ServletContext servletContext,
final String... attrNames) {
TimerTask task = new TimerTask() {
/**
* Tells the MostUsedValues object that created this TimerTask to
* refresh the cached most used values of some of the attributes.
*/
@Override public void run() {
try {
MostUsedValues.this.retrieveMostUsedValues(maxValues, attrNames);
} catch (IOException e) {
servletContext.log(e.getMessage(), e);
} catch (ServiceException e) {
servletContext.log(e.getMessage() + " " +
e.getHttpErrorCodeOverride() + " " +
e.getResponseContentType() + ": " +
e.getResponseBody(), e);
}
}
};
return task;
}
/**
* Retrieves the most used values for some attributes
* for the items that match the query string
* and stores a limited number of those values for each attribute.
*
* @param numValue maximum number of values to store
* @param attrNames the names of the attributes
*/
protected void retrieveMostUsedValues(int numValue, final String... attrNames)
throws ServiceException, IOException {
URL url = urlFactory.getAttributesFeedURL();
GoogleBaseQuery query = new GoogleBaseQuery(url);
StringBuffer queryString = createQueryString(attrNames);
query.setGoogleBaseQuery(queryString.toString());
query.setMaxValues(numValue);
int numResults = 0;
int lastNumResults = 0;
Collection<String> attrToRetrieve =
new ArrayList<String>(Arrays.asList(attrNames));
do {
// Get the feed
numResults += STEP_MAXRESULTS;
query.setMaxResults(numResults);
GoogleBaseFeed feed = service.query(query);
if (lastNumResults == feed.getTotalResults()) {
// No new entries to process
break;
}
lastNumResults = feed.getTotalResults();
// Extract the values from the entries
Iterator<String> attrIter = attrToRetrieve.iterator();
while (attrIter.hasNext()) {
String attrName = attrIter.next();
// Searching for the entry with the following name
String entryTitle = attrName + TEXT_TYPE;
for (GoogleBaseEntry entry : feed.getEntries()) {
if (entryTitle.equals(entry.getTitle().getPlainText())) {
extractValuesFromEntry(numValue, attrName, entry);
attrIter.remove();
}
}
}
} while (!attrToRetrieve.isEmpty() && numResults <= MAX_MAXRESULTS);
if (!attrToRetrieve.isEmpty()) {
throw new ServiceException("The retrieved histograms do not contain" +
"some of the attributes. The most used values of these attributes " +
"have not been refreshed.");
}
}
/**
* Returns the query string extended so that it filters out the items
* that do not have at least one of the specified attributes of type TEXT.
*
* @param attrNames the attributes we are interested in
* @return an extended query string
*/
protected StringBuffer createQueryString(final String... attrNames) {
StringBuffer queryString = new StringBuffer(this.queryString);
queryString.append("(");
queryString.append("[").append(attrNames[0]).append(TEXT_TYPE).append("]");
for (int i = 1; i < attrNames.length; i++) {
String attrName = attrNames[i];
queryString.append("|[").append(attrName).append(TEXT_TYPE).append("]");
}
queryString.append(")");
return queryString;
}
/**
* Caches a limited number of the values of a GoogleBaseEntry.
*
* @param numValue maximum number of values to cache
* @param attrName the name of the attribute that has the values
* @param entry an entry with a MetadataEntryExtension
*/
private void extractValuesFromEntry(int numValue,
String attrName,
GoogleBaseEntry entry) {
MetadataEntryExtension metadata = entry.getGoogleBaseMetadata();
AttributeHistogram attributeHistogram = metadata.getAttributeHistogram();
List<? extends UniqueValue> values = attributeHistogram.getValues();
int valuesCount = Math.min(numValue, values.size());
String[] usedValues = new String[valuesCount];
for (int i = 0; i < valuesCount; i++) {
usedValues[i] = values.get(i).getValueAsString();
}
updateMostUsedValue(attrName, usedValues);
}
/**
* Cancels all refresh timers and clears the cache.
*/
synchronized public void clear() {
for (Timer timer : timers) {
timer.cancel();
}
timers.clear();
mostUsedValues.clear();
}
/**
* Returns the number of cached attributes.
*/
public int size() {
return mostUsedValues.size();
}
/**
* Returns true if no attribute is cached.
*/
public boolean isEmpty() {
return mostUsedValues.isEmpty();
}
public String getQueryString() {
return queryString;
}
/**
* Caches the most used values of an attribute.
* @param attrName
* @param stringValues
*/
protected void updateMostUsedValue(String attrName, String[] stringValues) {
mostUsedValues.put(attrName, stringValues);
}
}
+338
View File
@@ -0,0 +1,338 @@
/* 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.GoogleBaseEntry;
import com.google.api.gbase.client.NumberUnit;
import com.google.gdata.data.Content;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.OtherContent;
import com.google.gdata.data.Person;
import com.google.gdata.data.TextConstruct;
import com.google.gdata.data.TextContent;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* One recipe, ready to be displayed.
* Just a data holding object.
*/
public class Recipe {
public final static String RECIPE_ITEMTYPE = "recipes";
public final static String MAIN_INGREDIENT_ATTRIBUTE = "main ingredient";
public final static String CUISINE_ATTRIBUTE = "cuisine";
public final static String COOKING_TIME_ATTRIBUTE = "cooking time";
public final static String AUTHOR_UNKNOWN = "";
private final String id;
private final DateTime postedOn;
private final String postedBy;
private final NumberUnit<Integer> cookingTime;
private final String url;
private final String title;
private final String description;
/** A never-null list that contains the main ingredients. */
private final Set<String> mainIngredient;
/** A never-null list that contains the cuisines. */
private final Set<String> cuisine;
/**
* Creates a recipe. The parameters can be null.
*
* @param id id generated by the GoogleBase server
* @param title
* @param url alternate url of the recipe;
* if null, when the recipe is used to insert or to update
* it is generated by the GoogleBase server
* @param description
* @param mainIngredient
* @param cuisine
* @param cookingTime
*/
public Recipe(String id, String title, String url,
String description, Set<String> mainIngredient, Set<String> cuisine,
NumberUnit<Integer> cookingTime) {
if (mainIngredient == null) {
mainIngredient = new HashSet<String>();
}
if (cuisine == null) {
cuisine = new HashSet<String>();
}
this.id = id;
this.title = title;
this.url = url;
this.description = description;
this.mainIngredient = mainIngredient;
this.cuisine = cuisine;
this.cookingTime = cookingTime;
this.postedOn = null;
this.postedBy = null;
}
/**
* Creates a recipe out of a GoogleBaseEntry.
*
* @param entry an entry that represents a recipe
*/
public Recipe(GoogleBaseEntry entry) {
id = extractIdFromUrl(entry.getId());
title = entry.getTitle().getPlainText();
url = entry.getHtmlLink().getHref();
String description = null;
if (entry.getContent() != null) {
Content content = entry.getContent();
if (content instanceof TextContent) {
description = ((TextContent)content).getContent().getPlainText();
} else if (content instanceof OtherContent) {
description = ((OtherContent)content).getText();
}
}
this.description = description;
mainIngredient = new HashSet<String>(entry.getGoogleBaseAttributes().
getTextAttributeValues(MAIN_INGREDIENT_ATTRIBUTE));
cuisine = new HashSet<String>(entry.getGoogleBaseAttributes().
getTextAttributeValues(CUISINE_ATTRIBUTE));
cookingTime = entry.getGoogleBaseAttributes().
getIntUnitAttribute(COOKING_TIME_ATTRIBUTE);
postedOn = entry.getPublished();
// if an entry has no author specified, will set it to empty string
List<Person> authors = entry.getAuthors();
postedBy = (authors.isEmpty() ? AUTHOR_UNKNOWN : authors.get(0).getName());
}
/**
* Creates an empty recipe, the values are null or empty Sets.
*/
public Recipe() {
this(null, null, null, null, null, null, null);
}
public GoogleBaseEntry toGoogleBaseEntry(String idUrl)
{
GoogleBaseEntry entry = new GoogleBaseEntry();
entry.getGoogleBaseAttributes().setItemType(RECIPE_ITEMTYPE);
if (idUrl != null) {
entry.setId(idUrl);
}
entry.setTitle(TextConstruct.create(TextConstruct.Type.TEXT, title, null));
if (url != null) {
entry.addHtmlLink(url, null, null);
}
if (description != null) {
// If the original content was not TEXT, the formatting is lost
entry.setContent(
TextConstruct.create(TextConstruct.Type.TEXT, description, null));
}
for (String ingredient : mainIngredient) {
entry.getGoogleBaseAttributes().addTextAttribute(
MAIN_INGREDIENT_ATTRIBUTE, ingredient);
}
for (String cuisineItem : cuisine) {
entry.getGoogleBaseAttributes().addTextAttribute(
CUISINE_ATTRIBUTE, cuisineItem);
}
if (cookingTime != null) {
entry.getGoogleBaseAttributes().addIntUnitAttribute(
COOKING_TIME_ATTRIBUTE, cookingTime);
}
return entry;
}
/**
* Extracts the id of the item from the given url.
*
* The id found in GoogleBaseEntry is a URL that contains the real, numerical
* id of the item in Google Base. Parsing the URL is unfortunately the
* only way of getting a numerical id given a GoogleBaseEntry.
*
* @param url a URL that ends with "/" [N] number
*/
private static String extractIdFromUrl(String url) {
int lastSlash = url.lastIndexOf('/');
if (lastSlash == -1 || lastSlash == (url.length()-1)) {
throw new IllegalArgumentException("Id is in a strange format. " + url);
}
String oid = url.substring(lastSlash + 1);
return oid;
}
/** Checks whether there is a description for the recipe. */
public boolean hasDescription() {
return description != null;
}
/** Checks whether there is a cooking time for the recipe. */
public boolean hasCookingTime() {
return cookingTime != null;
}
/** Checks whether there are some cuisines for the recipe. */
public boolean hasCuisine() {
return cuisine.size() > 0;
}
/** Checks whether there are some main ingredients for the recipe. */
public boolean hasMainIngredient() {
return mainIngredient.size() > 0;
}
/**
* Gets the date at which the recipe was posted, as a string.
*
* @param detailed set to true to get a full date and time
*/
public String getPostedOnAsString(boolean detailed) {
Date date = new Date(postedOn.getValue());
String template = detailed ? "MMMMM d, yyyy HH:mm z" : "MMM d";
DateFormat format = new SimpleDateFormat(template);
return format.format(date);
}
/** Gets the host and protocol from the recipe URL. */
public String getHostAndProtocol() throws MalformedURLException {
URL urlObject = new URL(getUrl());
return urlObject.getProtocol() + "://" + urlObject.getHost();
}
/**
* Returns true when the title, description, mainIngredient and cuisine
* attributes are not null nor empty.
*/
public boolean isComplete() {
return title != null &&
description != null &&
mainIngredient.size() > 0 &&
cuisine.size() > 0;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("Recipe[");
appendNamedParameter(sb, "title", title);
appendNamedParameter(sb, "id", id);
appendNamedParameter(sb, "url", url);
appendNamedParameter(sb, "description", description);
appendNamedParameter(sb, "cookingTime", cookingTime);
appendNamedParameter(sb, "postedOn", postedOn);
appendNamedParameter(sb, "postedBy", postedBy);
appendNamedCollection(sb, "mainIngredient", mainIngredient);
appendNamedCollection(sb, "cuisine", cuisine);
sb.append("]");
return sb.toString();
}
/**
* Appends the name and the value of an Object to a StringBuffer.
*
* @param sb
* @param name
* @param value
*/
private static void appendNamedParameter(StringBuffer sb,
String name,
Object value) {
if (value != null) {
sb.append(name).append("=\"").append(value).append("\" ");
}
}
/**
* Appends the name and the elements of a Collection to a StringBuffer.
*
* @param sb
* @param name
* @param collection
*/
private static void appendNamedCollection(
StringBuffer sb,
String name,
Collection<String> collection) {
if (collection.size() > 0) {
sb.append(name).append("=(");
for (String value : collection) {
sb.append("\"").append(value).append("\", ");
}
sb.append(") ");
}
}
/** Gets the id generated by the server. */
public String getId() {
return id;
}
/** Gets the user assigned title. */
public String getTitle() {
return title;
}
/**
* Gets the url of the recipe, as set by the customer.
* If the customer sets no url, the server will generate one.
*/
public String getUrl() {
return url;
}
/** Gets the user assigned description. */
public String getDescription() {
return description;
}
/** Returns a never-null Set with the main ingredients of the recipe. */
public Set<String> getMainIngredient() {
return mainIngredient;
}
/** Returns a never-null Set with the cuisines the recipe belongs to. */
public Set<String> getCuisine() {
return cuisine;
}
/** Gets the user assigned cooking time. */
public NumberUnit<Integer> getCookingTime() {
return cookingTime;
}
/** Gets the server generated owner attribute of the recipe. */
public String getPostedBy() {
return postedBy;
}
/** Gets the server generated date at which the recipe was posted. */
public DateTime getPostedOn() {
return postedOn;
}
/** Returns true when the Recipe doesn't have an id. */
public boolean isNew() {
return id == null;
}
}
@@ -0,0 +1,265 @@
/* 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.GoogleBaseService;
import com.google.api.gbase.client.NumberUnit;
import com.google.gdata.util.ServiceException;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Inserts, updates or deletes a recipe,
* depending on the "action" servlet initialization parameter.
*/
@SuppressWarnings("serial")
public class RecipeActionServlet extends HttpServlet {
public static final String DISPLAY_JSP = "/WEB-INF/recipeEdit.jsp";
private static final int ACTION_ADD = 0;
private static final int ACTION_UPDATE = 1;
private static final int ACTION_DELETE = 2;
protected FeedURLFactory urlFactory;
/** The operation this servlet has to perform. */
protected int action = -1;
@Override
public void init(ServletConfig servletConfig) throws ServletException {
super.init(servletConfig);
ServletContext context = servletConfig.getServletContext();
urlFactory = (FeedURLFactory)
context.getAttribute(RecipeListener.FEED_URL_FACTORY_ATTRIBUTE);
String action = servletConfig.getInitParameter("action");
if ("add".equals(action)) {
this.action = ACTION_ADD;
} else if ("update".equals(action)) {
this.action = ACTION_UPDATE;
} else if ("delete".equals(action)) {
this.action = ACTION_DELETE;
} else {
throw new ServletException("Unknown action: " + action);
}
}
private boolean isAdd() { return ACTION_ADD == action; }
private boolean isUpdate() { return ACTION_UPDATE == action; }
private boolean isDelete() { return ACTION_DELETE == action; }
@Override
public void destroy() {
super.destroy();
}
/** Inserts or updates the submitted recipe and redirects to recipeList. */
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
GoogleBaseService service = (GoogleBaseService) request.getAttribute(
AuthenticationFilter.SERVICE_ATTRIBUTE);
Recipe recipe = getPostedRecipe(request);
if (!recipe.isComplete()) {
String message = "<div class='errormessage'>Please fill out " +
"all the mandatory fields.</div>";
editRecipe(request, response, recipe, message);
} else {
try {
if (isAdd()) {
recipeAdd(service, recipe);
} else if (isUpdate()) {
recipeUpdate(service, recipe);
} else {
throw new ServletException("Unknown POST action: " + action);
}
} catch (ServiceException e) {
RecipeUtil.logServiceException(this, e);
RecipeUtil.forwardToErrorPage(request, response, e);
return;
}
listOwnRecipes(response);
}
}
/** Redirect to the page that lists customer's recipes. */
protected void listOwnRecipes(HttpServletResponse response)
throws IOException {
String redirectUrl = "recipeList";
response.sendRedirect(response.encodeRedirectURL(redirectUrl));
}
/**
* Inserts a recipe using the specified authenticated service.
*
* @param service an authenticated GoogleBaseService
* @param recipe recipe to be inserted
* @throws IOException
* @throws ServiceException
*/
protected void recipeAdd(GoogleBaseService service,
Recipe recipe)
throws IOException, ServiceException {
URL feedUrl = urlFactory.getItemsFeedURL();
GoogleBaseEntry entry = recipe.toGoogleBaseEntry(null);
service.insert(feedUrl, entry);
}
/**
* Updates a recipe using the specified authenticated service.
*
* The recipe must have a valid GoogleBase id.
*
* @param service an authenticted GoogleBaseService
* @param recipe recipe to be updated
* @throws ServiceException
* @throws IOException
*/
protected void recipeUpdate(GoogleBaseService service,
Recipe recipe)
throws ServiceException, IOException {
URL feedUrl = urlFactory.getItemsEntryURL(recipe.getId());
GoogleBaseEntry entry = recipe.toGoogleBaseEntry(feedUrl.toString());
service.update(feedUrl, entry);
}
/**
* Uses the specified authenticated service to delete a recipe.
*
* @param service an authenticated service
* @param id the id of the recipe
* @throws ServiceException
* @throws IOException
*/
protected void recipeDelete(GoogleBaseService service,
String id)
throws ServiceException, IOException {
URL feedUrl = urlFactory.getItemsEntryURL(id);
service.delete(feedUrl);
}
/**
* Builds a Recipe from the parameters submitted with the specified request.
*
* @param request http request to be processed
* @return a submitted recipe
*/
static Recipe getPostedRecipe(HttpServletRequest request) {
String id = getNullIfEmpty(request.getParameter(RecipeUtil.ID_PARAMETER));
String title = getNullIfEmpty(request.getParameter(
RecipeUtil.TITLE_PARAMETER));
String url = getNullIfEmpty(request.getParameter(RecipeUtil.URL_PARAMETER));
String description = getNullIfEmpty(request.getParameter(
RecipeUtil.DESCRIPTION_PARAMETER));
Set<String> mainIngredient = RecipeUtil.validateValues(
request.getParameterValues(RecipeUtil.MAIN_INGREDIENT_PARAMETER));
Set<String> cuisine = RecipeUtil.validateValues(
request.getParameterValues(RecipeUtil.CUISINE_PARAMETER));
NumberUnit<Integer> cookingTime;
try {
cookingTime = new NumberUnit<Integer>(
new Integer(request.getParameter(RecipeUtil.COOKING_TIME_PARAMETER)),
RecipeUtil.COOKING_TIME_UNIT);
} catch (Exception e) {
// If anything goes bad, we set cookingTime to null
cookingTime = null;
}
Recipe recipe = new Recipe(id,
title,
url,
description,
mainIngredient,
cuisine,
cookingTime);
return recipe;
}
static private String getNullIfEmpty(String s) {
return s != null && "".equals(s) ? null : s;
}
/** Shows the page for inserting or updating a recipe or deletes a recipe. */
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
GoogleBaseService service = (GoogleBaseService) request.getAttribute(
AuthenticationFilter.SERVICE_ATTRIBUTE);
String id = request.getParameter(RecipeUtil.ID_PARAMETER);
try {
if (isDelete()) {
recipeDelete(service, id);
listOwnRecipes(response);
} else {
// The recipe that will be used on the edit page
// for inserting or updating.
Recipe recipe = null;
if (isAdd()) {
recipe = new Recipe();
} else if (isUpdate()) {
URL entryUrl = urlFactory.getItemsEntryURL(id);
GoogleBaseEntry entry = service.getEntry(entryUrl);
recipe = new Recipe(entry);
}
if (recipe != null) {
// Ready to add or update
editRecipe(request, response, recipe, null);
}
}
} catch (ServiceException e) {
RecipeUtil.logServiceException(this, e);
RecipeUtil.forwardToErrorPage(request, response, e);
return;
}
}
/**
* Sets the {@value RecipeUtil#RECIPE_ATTRIBUTE} attribute of the request to
* contain the specified recipe and forwards the request to the
* {@value #DISPLAY_JSP} page.
*
* @param request
* @param response
* @param recipe the recipe to be passed to the edit jsp page
* @param message HTML code to be displayed at the top of the page, usually an
* error message
*/
private void editRecipe(HttpServletRequest request,
HttpServletResponse response,
Recipe recipe,
String message)
throws ServletException, IOException {
request.setAttribute(RecipeUtil.RECIPE_ATTRIBUTE, recipe);
request.setAttribute(RecipeUtil.MESSAGE_ATTRIBUTE,
message == null ? "" : message);
// Forward to the JSP
request.getRequestDispatcher(DISPLAY_JSP).forward(request, response);
}
}
@@ -0,0 +1,105 @@
/* 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.GoogleBaseService;
import com.google.gdata.util.ServiceException;
import java.io.IOException;
import java.net.URL;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Displays a recipe.
*/
@SuppressWarnings("serial")
public class RecipeDisplayServlet extends HttpServlet {
public static final String DISPLAY_JSP = "/WEB-INF/recipeDisplay.jsp";
protected FeedURLFactory urlFactory;
@Override
public void init(ServletConfig servletConfig) throws ServletException {
super.init(servletConfig);
ServletContext context = servletConfig.getServletContext();
urlFactory = (FeedURLFactory)
context.getAttribute(RecipeListener.FEED_URL_FACTORY_ATTRIBUTE);
}
@Override
public void destroy() {
super.destroy();
}
/**
* Shows the page for displaying a Recipe.
*
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// This is a public page, so we use a simple, nonauthenticated service
GoogleBaseService service = RecipeUtil.getGoogleBaseService(request,
this.getServletContext());
String id = request.getParameter(RecipeUtil.ID_PARAMETER);
recipeDisplay(request, response, service, id);
}
/**
* Retrieves a recipe and forwards the request
* to the {@link #DISPLAY_JSP} jsp page that displays the recipe.
*
* @param request
* @param response
* @param service the service used to retrieve the recipe
* @param id the id of the recipe
*/
private void recipeDisplay(HttpServletRequest request,
HttpServletResponse response,
GoogleBaseService service,
String id)
throws ServletException, IOException {
GoogleBaseEntry entry;
try {
URL feedUrl = urlFactory.getSnippetsEntryURL(id);
entry = service.getEntry(feedUrl, GoogleBaseEntry.class);
} catch (ServiceException e) {
RecipeUtil.logServiceException(this, e);
RecipeUtil.forwardToErrorPage(request, response, e);
return;
}
Recipe recipe = new Recipe(entry);
request.setAttribute(RecipeUtil.RECIPE_ATTRIBUTE, recipe);
RecipeSearch results = new RecipeSearch(service, urlFactory, false);
RecipeUtil.setRecipeSearch(request, results);
// Forward to the JSP
request.getRequestDispatcher(DISPLAY_JSP).forward(request, response);
}
}
@@ -0,0 +1,109 @@
/* 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.GoogleBaseService;
import java.net.MalformedURLException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* Creates objects needed by the servlets and makes them available by
* setting them as attributes of the global servlet context.
*
* Makes sure the required initialization parameters are present.
*
*/
public class RecipeListener implements ServletContextListener {
public static final String MOST_USED_VALUES_ATTRIBUTE = "mostUsedValues";
public static final String FEED_URL_FACTORY_ATTRIBUTE = "feedUrlFactory";
FeedURLFactory urlFactory;
protected MostUsedValues mostUsedValues;
/**
* Creates an initialised MostUsedValues object and a FeedURLFactory
* to be used by the servlets.
* Makes sure the applicationName init parameter is set.
*
* @throws RuntimeException
*/
public void contextInitialized(ServletContextEvent event) {
ServletContext servletContext = event.getServletContext();
String applicationName =
servletContext.getInitParameter(RecipeUtil.APPLICATION_NAME_PARAMETER);
if (applicationName == null) {
RuntimeException re =
new RuntimeException("applicationName context parameter is missing");
servletContext.log(re.getMessage(), re.getCause());
throw re;
}
String baseUrl = servletContext.getInitParameter("baseUrl");
if (baseUrl == null) {
urlFactory = FeedURLFactory.getDefault();
} else {
try {
urlFactory = new FeedURLFactory(baseUrl);
} catch (MalformedURLException e) {
RuntimeException re =
new RuntimeException("Cannot use the baseUrl context parameter", e);
servletContext.log(re.getMessage(), re.getCause());
throw re;
}
}
servletContext.setAttribute(FEED_URL_FACTORY_ATTRIBUTE, urlFactory);
String key = servletContext.getInitParameter(RecipeUtil.DEVELOPER_KEY_PARAMETER);
GoogleBaseService service = new GoogleBaseService(applicationName, key);
mostUsedValues = new MostUsedValues(service,
urlFactory,
RecipeUtil.RECIPE_ITEMTYPE_QUERY);
initMostUsedValues(mostUsedValues, servletContext);
RecipeUtil.setMostUsedValues(servletContext, mostUsedValues);
}
public void contextDestroyed(ServletContextEvent event) {
mostUsedValues.clear();
}
/**
* Initializes a MostUsedValues object to cache the most used values
* of some attributes, suitable to be used in the web pages.
*
* @param mostUsedValues object to initialize
* @param servletContext the servlet context used by mostUsedValues
* to log exceptions
*/
public static void initMostUsedValues(MostUsedValues mostUsedValues,
ServletContext servletContext) {
long interval = 1000L * 60L * 60L; // 1 hour
mostUsedValues.cache(interval, 14, servletContext,
Recipe.CUISINE_ATTRIBUTE);
mostUsedValues.cache(interval, 16, servletContext,
Recipe.MAIN_INGREDIENT_ATTRIBUTE);
}
}
@@ -0,0 +1,395 @@
/* 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;
}
}
@@ -0,0 +1,179 @@
/* 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.GoogleBaseService;
import com.google.gdata.util.ServiceException;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Setup a {@link RecipeSearch} object and optionally fill it.
*/
@SuppressWarnings("serial")
public class RecipeSearchServlet extends HttpServlet {
private static final String START_INDEX_PARAMETER = "startIndex";
private static final String MAX_RESULTS_PARAMETER = "maxResults";
private static final String QUERY_PARAMETER = "query";
public static final String DISPLAY_JSP = "/WEB-INF/recipeSearch.jsp";
protected boolean ownItems;
protected FeedURLFactory urlFactory;
@Override
public void init(ServletConfig servletConfig) throws ServletException {
super.init(servletConfig);
ServletContext context = servletConfig.getServletContext();
urlFactory = (FeedURLFactory)
context.getAttribute(RecipeListener.FEED_URL_FACTORY_ATTRIBUTE);
String scope = servletConfig.getInitParameter("scope");
ownItems = "own".equals(scope);
}
@Override
public void destroy() {
super.destroy();
}
/**
* Runs a recipe search.
*
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
GoogleBaseService service = RecipeUtil.getGoogleBaseService(request,
this.getServletContext());
RecipeSearch recipeSearch;
try {
if (request.getParameter("query") == null) {
recipeSearch = new RecipeSearch(service, urlFactory, ownItems);
} else {
recipeSearch = createRecipeSearch(service, request);
}
recipeSearch.runQuery();
} catch (RecipeValidationException rve) {
// internal exception in the argument handling (possibly incorrect args)
RecipeUtil.forwardToErrorPage(request, response, rve.getMessage());
return;
} catch (ServiceException se) {
// exception comming from Google Base
RecipeUtil.logServiceException(this, se);
RecipeUtil.forwardToErrorPage(request, response, se);
return;
}
RecipeUtil.setRecipeSearch(request, recipeSearch);
// Forward to the JSP
request.getRequestDispatcher(DISPLAY_JSP).forward(request, response);
}
/**
* Creates and fills in a {@link RecipeSearch} object based on the content
* of the {@code request}.
*
* @param service the object used to connect to the Google Base service
* @param request the representation of the Http request
* @throws RecipeValidationException if a parameter from the request has an
* invalid value (cooking time, start index, max results are not valid
* numbers).
*/
private RecipeSearch createRecipeSearch(GoogleBaseService service,
HttpServletRequest request)
throws RecipeValidationException {
RecipeSearch search = new RecipeSearch(service, urlFactory, ownItems);
String query = request.getParameter(QUERY_PARAMETER);
if (isSet(query)) {
search.setQuery(query);
}
String[] mainIngredient = request.getParameterValues(
RecipeUtil.MAIN_INGREDIENT_PARAMETER);
if (isSet(mainIngredient)) {
search.setMainIngredientValues(mainIngredient);
}
String[] cuisine = request.getParameterValues(
RecipeUtil.CUISINE_PARAMETER);
if (isSet(cuisine)) {
search.setCuisineValues(cuisine);
}
String cookingTime = request.getParameter(
RecipeUtil.COOKING_TIME_PARAMETER);
if (isSet(cookingTime)) {
try {
search.setCookingTime(new Integer(cookingTime));
} catch (NumberFormatException e) {
throw new RecipeValidationException(String.format(
"Cooking time is not a number (%s).", cookingTime));
}
}
String startIndex = request.getParameter(START_INDEX_PARAMETER);
if (isSet(startIndex)) {
try {
search.setStartIndex(Integer.parseInt(startIndex));
} catch (NumberFormatException e) {
throw new RecipeValidationException(String.format(
"Start index is not a number (%s).", startIndex));
}
}
String maxResults = request.getParameter(MAX_RESULTS_PARAMETER);
if (isSet(maxResults)) {
try {
search.setMaxResults(Integer.parseInt(maxResults));
} catch (NumberFormatException e) {
throw new RecipeValidationException(String.format(
"Max results is not a number (%s).", maxResults));
}
}
search.setOwnItems(ownItems);
return search;
}
private boolean isSet(String parameter) {
return parameter != null && !"".equals(parameter);
}
private boolean isSet(String[] parameter) {
return parameter != null && parameter.length > 0;
}
}
@@ -0,0 +1,269 @@
/* 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.GoogleBaseService;
import com.google.api.gbase.client.ServiceErrors;
import com.google.gdata.util.ServiceException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Contains the names of the HTML input fields used to edit a recipe.
* Has methods that are generally useful, for example for logging
* or validating values.
* Has methods to extract values from servlet context init parameters.
* Has methods used for passing objects from a servlet to a JSP.
*/
public class RecipeUtil {
public static final String ID_PARAMETER = "oid";
public static final String APPLICATION_NAME_PARAMETER = "applicationName";
public static final String TITLE_PARAMETER = "title";
public static final String URL_PARAMETER = "url";
public static final String DESCRIPTION_PARAMETER = "description";
public static final String MAIN_INGREDIENT_PARAMETER = "mainIngredient";
public static final String CUISINE_PARAMETER = "cuisine";
public static final String COOKING_TIME_PARAMETER = "cookingTime";
public static final String DEVELOPER_KEY_PARAMETER = "key";
public static final String COOKING_TIME_UNIT = "minutes";
public static final String RECIPE_ATTRIBUTE = "recipe";
public static final String RECIPESEARCH_ATTRIBUTE = "recipeSearch";
public static final String RECIPESEARCH_ERROR = "recipeSearchError";
public static final String RECIPESEARCH_ERROR_DESCRIPTION =
"recipeSearchErrorDescription";
public static final String RECIPESEARCH_ERROR_OBJECT =
"recipeSearchErrorService";
public static final String MESSAGE_ATTRIBUTE = "message";
public static final String RECIPE_ITEMTYPE_QUERY =
"[item type : recipe | recipes]";
/** Pattern used for finding the unsupported characters in the query string.*/
private static final Pattern QUERY_REPLACE_PATTERN =
Pattern.compile("\\p{Punct}");
public static final String ERROR_JSP = "/WEB-INF/recipeError.jsp";
/**
* Builds a HashSet containing the specified values,
* filtering the null and empty ones.
*
* @param values usually an array returned by request.getParameterValues()
* @return a HashSet containing the nonempty values
*/
public static Set<String> validateValues(String[] values) {
Set<String> valuesList = new HashSet<String>();
if (values != null) {
for (String value : values) {
value = cleanQueryString(value);
if (value != null && ! "".equals(value)) {
valuesList.add(value);
}
}
}
return valuesList;
}
/**
* Cleans the {@code searchString} set by the user, by removing the special
* punctuation characters not allowed directly in a query.
*
* @param searchString the string set by the user
* @return the String to be used for executing the query to Google Base.
*/
public static String cleanQueryString(String searchString) {
Matcher matcher = QUERY_REPLACE_PATTERN.matcher(searchString);
return matcher.replaceAll(" ").trim();
}
/**
* Logs an exception in a convenient format,
* using the log method of a servlet context.
*
* @param servlet the servlet used to log the exception
* @param e exception to be logged
*/
public static void logServiceException(HttpServlet servlet,
ServiceException e) {
if (e.getResponseBody() != null) {
// Log the full error message and response code.
servlet.log(e.getMessage() + " " +
e.getHttpErrorCodeOverride() + " " +
e.getResponseContentType() + ": " +
e.getResponseBody(), e);
}
}
/**
* Sets a RecipeSearch as an attribute of a HttpServletRequest.
*
* @param request a request that will be passed to a JSP
* @param results the RecipeSearch, executed or not
*/
public static void setRecipeSearch(HttpServletRequest request,
RecipeSearch results) {
request.setAttribute(RECIPESEARCH_ATTRIBUTE, results);
}
/**
* Gets from a HttpServletRequest a RecipeSearch that was previously set
* using {@link #setRecipeSearch}.
* If it is missing, a NullPointerException is thrown.
*
* @param request a request passed from a Servlet
* @return a non-null RecipeSearch
*/
public static RecipeSearch getRecipeSearch(HttpServletRequest request) {
RecipeSearch results = (RecipeSearch)request.getAttribute(
RECIPESEARCH_ATTRIBUTE);
if (null == results) {
throw new NullPointerException("recipe search results are missing");
}
return results;
}
/**
* Gets the error message, or {@code null} if no error message was set in the
* request.
*
* @param request the request in which the method will locate the error
* @return the error message, or {@code null} if no message was set
*/
public static String getRecipeError(HttpServletRequest request) {
return (String)request.getAttribute(RECIPESEARCH_ERROR);
}
/**
* Gets the error message, or {@code null} if no error message was set in the
* request.
*
* @param request the request in which the method will locate the error
* @return the error message, or {@code null} if no message was set
*/
public static String getRecipeErrorDescription(HttpServletRequest request) {
return (String)request.getAttribute(RECIPESEARCH_ERROR_DESCRIPTION);
}
/**
* Gets the errors obtained from Google Base, or {@code null} if no service
* error was set in the request.
*
* @param request the request in which the method will locate the error
* @return the service errors, or {@code null} if no service errors were set
*/
public static ServiceErrors getRecipeErrorObject(HttpServletRequest request) {
return (ServiceErrors)request.getAttribute(RECIPESEARCH_ERROR_OBJECT);
}
/**
* Forwards the request to the error page, for displaying the specified
* {@code errorMessage} and the {@code description}. No service errors will
* be displayed.
*
* @param request the request object
* @param response the response object
* @param errorMessage the error message to be displayed
* @param description the description of the error message, {@code null} if
* no description should be displayed.
* @throws ServletException
* @throws IOException
*/
public static void forwardToErrorPage(HttpServletRequest request,
HttpServletResponse response, String errorMessage)
throws ServletException, IOException {
request.setAttribute(RECIPESEARCH_ERROR, errorMessage);
request.getRequestDispatcher(ERROR_JSP).forward(request, response);
}
/**
* Forwards the request to the error page, for displaying the information
* contained by the {@code se} parameter. This method registers the service
* errors too, using a {@link ServiceErrors} object.
*
* @param request the request object
* @param response the response object
* @param se the service error containing the information for the error page
* @throws ServletException
* @throws IOException
*/
public static void forwardToErrorPage(HttpServletRequest request,
HttpServletResponse response, ServiceException se)
throws ServletException, IOException {
request.setAttribute(RECIPESEARCH_ERROR, se.getMessage());
request.setAttribute(RECIPESEARCH_ERROR_DESCRIPTION, se.getResponseBody());
request.setAttribute(RECIPESEARCH_ERROR_OBJECT, new ServiceErrors(se));
request.getRequestDispatcher(ERROR_JSP).forward(request, response);
}
/**
* Gets the GoogleBaseService object created by {@link AuthenticationFilter}
* or creates a new one if <code>AuthenticationFilter</code> has not been
* applied yet.
*
* @param req
* @param servletContext
* @return a GoogleBaseService object
*/
public static GoogleBaseService getGoogleBaseService(HttpServletRequest req,
ServletContext servletContext) {
GoogleBaseService service;
service = (GoogleBaseService) req.getAttribute(
AuthenticationFilter.SERVICE_ATTRIBUTE);
if (service == null) {
service = new GoogleBaseService(
servletContext.getInitParameter(APPLICATION_NAME_PARAMETER),
servletContext.getInitParameter(DEVELOPER_KEY_PARAMETER));
req.setAttribute(AuthenticationFilter.SERVICE_ATTRIBUTE, service);
}
return service;
}
/**
* Gets a MostUsedValues that was previously set using
* {@link #setMostUsedValues}.
*
* @param servletContext
* @return a non-null initialized MostUsedValues
*/
public static MostUsedValues getMostUsedValues(ServletContext servletContext)
throws ServletException {
MostUsedValues mostUsedValues = (MostUsedValues)
servletContext.getAttribute(RecipeListener.MOST_USED_VALUES_ATTRIBUTE);
if (null == mostUsedValues) {
throw new ServletException("Most used values cache is missing");
}
return mostUsedValues;
}
public static void setMostUsedValues(ServletContext servletContext,
MostUsedValues mostUsedValues) {
servletContext.setAttribute(RecipeListener.MOST_USED_VALUES_ATTRIBUTE,
mostUsedValues);
}
}
@@ -0,0 +1,27 @@
/* Copyright (c) 2007 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;
/**
* Exception thrown when an invalid parameter is received by the Recipe Demo
* Application.
*/
public class RecipeValidationException extends Exception {
public RecipeValidationException(String message) {
super(message);
}
}
@@ -0,0 +1,3 @@
<div class="demolabel">
This recipe book is a demo of the Google Base Data API. <br/><a href="http://code.google.com/apis/base">Learn more and download API tools</a>.
</div>
@@ -0,0 +1,7 @@
<div id="footer">
<a href="http://code.google.com/apis/base">Google Base Data API</a> -
<a href="http://code.google.com/apis/base/terms.html">Terms and Conditions</a> -
<a href="http://base.google.com">Google Base</a> -
<a href="http://www.google.com">Google home</a>
<p id="copyright">&copy; Google 2006</p>
</div>
@@ -0,0 +1,68 @@
<%--
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.
--%>
<%@ page import="sample.gbase.recipe.DisplayUtils"%>
<%@ page import="sample.gbase.recipe.Recipe"%>
<%@ page import="sample.gbase.recipe.RecipeSearch"%>
<%@ page import="sample.gbase.recipe.RecipeUtil"%>
<%RecipeSearch searchResults = RecipeUtil.getRecipeSearch(request);%>
<div id="leftnav">
<div id="search">
<form name="searchForm" action="recipe<%= searchResults.isOwnItems() ? "List" : "Search" %>" method="GET"><h2>Find a recipe</h2>
<table border="0" cellpadding="0" cellspacing="0" id="advancedsearch">
<tr><th colspan="2">With the word or phrase</th></tr>
<tr>
<td><input type="text" name="query" size="25" class="txt"
value='<%=DisplayUtils.escape(searchResults.getQuery()) %>'>
</td>
</tr>
<tr>
<th>Cuisine type</th>
</tr>
<tr>
<td><%DisplayUtils.printCheckboxes(out,
RecipeUtil.CUISINE_PARAMETER,
RecipeUtil.getMostUsedValues(pageContext.getServletContext()).getMostUsedValuesForAttribute(Recipe.CUISINE_ATTRIBUTE),
searchResults.getCuisineValues()); %></td>
</tr>
<tr>
<th>Main ingredient</th>
</tr>
<tr>
<td><%DisplayUtils.printCheckboxes(out,
RecipeUtil.MAIN_INGREDIENT_PARAMETER,
RecipeUtil.getMostUsedValues(pageContext.getServletContext()).getMostUsedValuesForAttribute(Recipe.MAIN_INGREDIENT_ATTRIBUTE),
searchResults.getMainIngredientValues()); %></td>
</tr>
<tr>
<th>Cooking time</th>
</tr>
<tr>
<td><input name="cookingTime" type="text" size="4"
value='<%=DisplayUtils.escape(searchResults.getCookingTime()) %>'
> <%=RecipeUtil.COOKING_TIME_UNIT %></td>
</tr>
</table>
<br/>
<button onclick="document.searchForm.startIndex.value='0'; document.searchForm.submit();"
style="font-weight:bold; -moz-border-radius:3px;border: 2px outset #bbbbbb;font-size:124%; padding:2px 1em;">Search</button>
<input type="hidden" name="startIndex" value="0"></form>
</div>
<p id="poweredby">
&nbsp;Powered by<br>
<a href="http://base.google.com/"><img src="googleBase.gif" border="0" alt="Google Base" vspace="2"></a>
</p>
</div>
@@ -0,0 +1,71 @@
<%--
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.
--%>
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page import="sample.gbase.recipe.DisplayUtils"%>
<%@ page import="sample.gbase.recipe.Recipe"%>
<%@ page import="sample.gbase.recipe.RecipeUtil"%>
<html>
<head>
<title>Google Base API Demo: Recipe Book</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div id="content">
<%@ include file="demoLabel.inc" %>
<%Recipe recipe = (Recipe) request.getAttribute(RecipeUtil.RECIPE_ATTRIBUTE); %>
<h1><a href="recipeSearch" class="on">All Recipes</a> &gt; <%=DisplayUtils.escape(recipe.getTitle()) %></h1>
<%@ include file="leftNav.jsp" %>
<div id="body" class="main">
<div id="singleitem">
<h3 class="itemtitle"><%=DisplayUtils.escape(recipe.getTitle()) %></h3>
<h4 class="postdate"><%=recipe.getPostedOnAsString(true) %></h4>
<%if (recipe.hasCuisine()) { %>
<h5>Cuisine: <%=DisplayUtils.printList(recipe.getCuisine()) %></h5><br/>
<%} %>
<%if (recipe.hasCookingTime()) { %>
<h5>Cooking time: <%=recipe.getCookingTime().getValue() %> <%=DisplayUtils.escape(recipe.getCookingTime().getUnit()) %></h5>
<%} %>
<%if (recipe.hasMainIngredient()) { %>
<div class="ingredients">
<ul>
<%for (String ingredient : recipe.getMainIngredient()) { %>
<li><%=DisplayUtils.escape(ingredient) %></li>
<%} %>
</ul>
</div>
<%} %>
<br/>
<div class="preparation">
<%=DisplayUtils.escape(recipe.getDescription()) %>
</div>
</div>
</div>
<br clear="all">
<%@ include file="demoLabel.inc" %>
</div>
<%@ include file="footer.inc" %>
</body>
</html>
@@ -0,0 +1,112 @@
<%--
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.
--%>
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page import="sample.gbase.recipe.DisplayUtils"%>
<%@ page import="sample.gbase.recipe.Recipe"%>
<%@ page import="sample.gbase.recipe.RecipeUtil"%>
<html>
<head>
<title>Google Base API Demo: Recipe Book</title>
<link rel="stylesheet" href="style.css">
<style type="text/css">
.main {
margin-left: 0;
}
</style>
</head>
<body>
<div id="content">
<%@ include file="demoLabel.inc" %>
<%Recipe recipe = (Recipe) request.getAttribute(RecipeUtil.RECIPE_ATTRIBUTE); %>
<%String message = (String) request.getAttribute(RecipeUtil.MESSAGE_ATTRIBUTE); %>
<h1><a href="recipeList" class="on">My Recipes</a> &gt; <%=recipe.isNew() ? "Add" : "Update" %> a recipe</h1>
<div id="body" class="main">
<div id="newitem">
<%=message %>
<form name="newitem" method="POST" action="recipe<%=recipe.isNew() ? "Add" : "Update?" + RecipeUtil.ID_PARAMETER + "=" + DisplayUtils.escape(recipe.getId()) %>"/>
<table cellpadding="0" cellspacing="0" border="0" id="createrecipe">
<tr>
<th>Recipe title *</th>
<td><input type="text" name="<%=RecipeUtil.TITLE_PARAMETER %>"
value="<%=DisplayUtils.escape(recipe.getTitle()) %>"
class="txt" size="35"></td>
</tr>
<tr>
<th>Cuisine type *</th>
<td>
<%DisplayUtils.printCheckboxes(out,
RecipeUtil.CUISINE_PARAMETER,
RecipeUtil.getMostUsedValues(pageContext.getServletContext()).getMostUsedValuesForAttribute(Recipe.CUISINE_ATTRIBUTE),
recipe.getCuisine()); %>
</td>
</tr>
<tr>
<th>Instructions *</th>
<td><textarea name="<%=RecipeUtil.DESCRIPTION_PARAMETER %>"
cols="40" rows="8"
><%=DisplayUtils.escape(recipe.getDescription()) %></textarea>
</td>
</tr>
<tr>
<th>Main ingredients *</th>
<td>
<%DisplayUtils.printCheckboxes(out,
RecipeUtil.MAIN_INGREDIENT_PARAMETER,
RecipeUtil.getMostUsedValues(pageContext.getServletContext()).getMostUsedValuesForAttribute(Recipe.MAIN_INGREDIENT_ATTRIBUTE),
recipe.getMainIngredient()); %>
</td>
</tr>
<tr>
<th>URL</th>
<td><input type="text" name="<%=RecipeUtil.URL_PARAMETER %>"
value="<%=DisplayUtils.escape(recipe.getUrl()) %>"
class="txt" size="40"></td>
</tr>
<tr>
<th>Cooking time</th>
<td><input type="text" name="<%=RecipeUtil.COOKING_TIME_PARAMETER %>"
value="<%=recipe.hasCookingTime() ? recipe.getCookingTime().getValue() : "" %>"
class="txt" size="4"> <%=RecipeUtil.COOKING_TIME_UNIT %></td>
</tr>
</table>
<p>
This recipe will be publicly viewable on the internet once you click "Publish". <br> <br>
<input type="submit" value="Publish this recipe" style="font-weight:bold;"> &nbsp;
<button onclick="history.go(-1); return false;">Cancel</button>
</p>
<p>* Mandatory attributes</p>
</form>
</div>
</div>
<br clear="all">
<%@ include file="demoLabel.inc" %>
</div>
<%@ include file="footer.inc" %>
</body>
</html>
@@ -0,0 +1,78 @@
<%--
Copyright (c) 2007 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.
--%>
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page import="com.google.api.gbase.client.ServiceErrors"%>
<%@ page import="sample.gbase.recipe.RecipeUtil"%>
<%@page import="sample.gbase.recipe.DisplayUtils"%>
<%@page import="com.google.api.gbase.client.ServiceError"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Google Base API demo: Recipe Book</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="content">
<%@ include file="demoLabel.inc" %>
<%
String error = RecipeUtil.getRecipeError(request);
String description = RecipeUtil.getRecipeErrorDescription(request);
ServiceErrors serviceErrors = RecipeUtil.getRecipeErrorObject(request);
%>
<div style="height:70%; padding-top: 20px;">
An error has occured. Hit the browser's Back button for continuing to use
the Recipe Demo Application. <br/>
<%if (error != null) { %>
<div class="errordiv">
<span class="errormessage"><%= DisplayUtils.escape(error) %></span>
<br/>
</div>
<%} // has error %>
<%if (description != null) { %>
<div class="errordiv">
Detailed information about the error: <br/>
<pre><%= DisplayUtils.escape(description) %></pre>
</div>
<%} // has description %>
<%if (serviceErrors != null) { %>
<div class="errordiv">
Detailed information about the Google Base service error: <br/>
<ul>
<%for (ServiceError serviceError : serviceErrors.getAllErrors()) { %>
<li> <%= DisplayUtils.escape(serviceError.getType()) %> : <%= DisplayUtils.escape(serviceError.getReason()) %>
<%} // for each service error %>
</ul>
<%if (serviceErrors.getAllErrors().size() == 0) { %>
<i>No service errors could be parsed</i>
<%} // if no service errors registered %>
</div>
<%} // has errors object %>
</div>
<br clear="all">
<%@ include file="demoLabel.inc" %>
</div>
<%@ include file="footer.inc" %>
</body>
</html>
@@ -0,0 +1,138 @@
<%--
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.
--%>
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page import="sample.gbase.recipe.DisplayUtils"%>
<%@ page import="sample.gbase.recipe.Recipe"%>
<%@ page import="sample.gbase.recipe.RecipeSearch"%>
<%@ page import="sample.gbase.recipe.RecipeUtil"%>
<%@ page import="java.util.Iterator"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Google Base API demo: Recipe Book</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="content">
<%@ include file="demoLabel.inc" %>
<%RecipeSearch recipeSearch = RecipeUtil.getRecipeSearch(request); %>
<%if (recipeSearch.isOwnItems()) { %>
<a href="recipeSearch" class="toplink">All recipes</a>
<%} else { %>
<a href="recipeList" class="toplink">My recipes</a>
<%} %>
<%if (recipeSearch.isOwnItems()) { %>
<a href="recipeAdd" class="toplink">Add a recipe</a>
<%} %>
<h1>
<%if (recipeSearch.isOwnItems()) { %>
<a href="recipeList" class="on">My Recipes</a>
<%} else { %>
<a href="recipeSearch" class="on">All Recipes</a>
<%} %>
&gt; Search
<%if (recipeSearch.getTotal() > 0) { %>
results
<%} %>
</h1>
<%@ include file="leftNav.jsp" %>
<div id="body" class="main">
<div id="searchresults">
<script language="JavaScript">
function gotoPage(index) {
document.searchForm.reset();
document.searchForm.startIndex.value =
index*<%=recipeSearch.getMaxResults()%>;
document.searchForm.submit();
}
function narrowSearchByIngredient(ingredient) {
document.searchForm.reset();
document.searchForm.othermainIngredient.value = ingredient;
document.searchForm.submit();
}
function confirmDelete(oid) {
if (confirm("Do you really want to delete this recipe?")) {
window.location = "recipeDelete?oid=" + oid;
}
}
</script>
<%if (recipeSearch.getTotal() >= 0) {%>
<%if (recipeSearch.getTotal() == 0) {%>
<h4>Your search for recipes
<%=recipeSearch.getFilterDescription() %>
did not match any recipes.</h4>
<%} %>
<%if (recipeSearch.hasResults()) {%>
<h4><%=recipeSearch.getTotal()%> recipes
<%=recipeSearch.getFilterDescription() %>
- showing <%=recipeSearch.getCurrentPageInterval()%></h4>
<%for (Iterator iter = recipeSearch.getRecipes().iterator(); iter.hasNext(); ) {%>
<%Recipe recipe = (Recipe)iter.next();%>
<p>
<a href="recipeDisplay?oid=<%=recipe.getId()%>" class="m"><%=DisplayUtils.escape(recipe.getTitle()).toUpperCase() %></a>
Main ingredient:
<%for (Iterator<String> ingredientIter = recipe.getMainIngredient().iterator(); ingredientIter.hasNext(); ) { %>
<%String ingredient = ingredientIter.next(); %>
<a href="javascript:narrowSearchByIngredient('<%=ingredient%>')" class="r"><%=ingredient%></a><%=ingredientIter.hasNext() ? ", " : "" %>
<%} %>
<br>
posted on <%=recipe.getPostedOnAsString(false)%>
by <%=DisplayUtils.escape(recipe.getPostedBy())%><br>
<%if (recipeSearch.isOwnItems()) { %>
<a href="recipeUpdate?oid=<%=recipe.getId() %>">Update</a> |
<a href="javascript:confirmDelete('<%=recipe.getId() %>')">Delete</a>
<%} %>
</p>
<%} // for recipe in recipeSearch.getRecipes()%>
<%} // if recipeSearch.hasResults%>
<%if (recipeSearch.getTotal() > recipeSearch.getMaxResults()) { //results don't fit on one page?%>
<p class="pagination">
Results <%=recipeSearch.getCurrentPageInterval() %> of <%=recipeSearch.getTotal() %><br/>
<%if (recipeSearch.hasPreviousPage()) {%>
<a href="javascript:gotoPage(<%=recipeSearch.getCurrentPage() - 1 %>)"
style="margin-right:1em;"
class="next">&laquo; Previous <%=recipeSearch.getMaxResults() %></a>
<%} %>
<%if (recipeSearch.hasNextPage()) {%>
<a href="javascript:gotoPage(<%=recipeSearch.getCurrentPage() + 1 %>)"
class="next">Next <%=recipeSearch.getNextPageSize() %> &raquo;</a>
<%} %>
</p>
<%} // pages? %>
<%} //total >= 0 %>
</div>
</div>
<br clear="all">
<%@ include file="demoLabel.inc" %>
</div>
<%@ include file="footer.inc" %>
</body>
</html>
@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Google Base Recipe Search</display-name>
<description>Search Recipes on Google Base</description>
<context-param>
<param-name>baseUrl</param-name>
<param-value>http://www.google.com/base/</param-value>
<description>URL of the google base server to connect to.</description>
</context-param>
<context-param>
<param-name>applicationName</param-name>
<param-value>Google-RecipeDemo-1.0</param-value>
<description>Application name used when authenticating.</description>
</context-param>
<context-param>
<param-name>key</param-name>
<param-value></param-value>
<description>Developer key used to authenticate against the Google Base data API servers.</description>
</context-param>
<filter>
<filter-name>AuthenticationFilter</filter-name>
<filter-class>sample.gbase.recipe.AuthenticationFilter</filter-class>
<init-param>
<param-name>authsubProtocol</param-name>
<param-value>https</param-value>
<description>Protocol to be used when connecting to AuthSub.</description>
</init-param>
<init-param>
<param-name>authsubHostname</param-name>
<param-value>www.google.com</param-value>
<description>Hostname of the authentication server.</description>
</init-param>
</filter>
<listener>
<listener-class>sample.gbase.recipe.RecipeListener</listener-class>
</listener>
<filter-mapping>
<filter-name>AuthenticationFilter</filter-name>
<servlet-name>OwnRecipeSearchServlet</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>AuthenticationFilter</filter-name>
<servlet-name>RecipeAddServlet</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>AuthenticationFilter</filter-name>
<servlet-name>RecipeUpdateServlet</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>AuthenticationFilter</filter-name>
<servlet-name>RecipeDeleteServlet</servlet-name>
</filter-mapping>
<servlet>
<servlet-name>AllRecipeSearchServlet</servlet-name>
<servlet-class>sample.gbase.recipe.RecipeSearchServlet</servlet-class>
<init-param>
<param-name>scope</param-name>
<param-value>all</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>OwnRecipeSearchServlet</servlet-name>
<servlet-class>sample.gbase.recipe.RecipeSearchServlet</servlet-class>
<init-param>
<param-name>scope</param-name>
<param-value>own</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>RecipeAddServlet</servlet-name>
<servlet-class>sample.gbase.recipe.RecipeActionServlet</servlet-class>
<init-param>
<param-name>action</param-name>
<param-value>add</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>RecipeUpdateServlet</servlet-name>
<servlet-class>sample.gbase.recipe.RecipeActionServlet</servlet-class>
<init-param>
<param-name>action</param-name>
<param-value>update</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>RecipeDeleteServlet</servlet-name>
<servlet-class>sample.gbase.recipe.RecipeActionServlet</servlet-class>
<init-param>
<param-name>action</param-name>
<param-value>delete</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>RecipeDisplayServlet</servlet-name>
<servlet-class>sample.gbase.recipe.RecipeDisplayServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AllRecipeSearchServlet</servlet-name>
<url-pattern>/recipeSearch</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>OwnRecipeSearchServlet</servlet-name>
<url-pattern>/recipeList</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RecipeAddServlet</servlet-name>
<url-pattern>/recipeAdd</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RecipeUpdateServlet</servlet-name>
<url-pattern>/recipeUpdate</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RecipeDeleteServlet</servlet-name>
<url-pattern>/recipeDelete</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RecipeDisplayServlet</servlet-name>
<url-pattern>/recipeDisplay</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

+1
View File
@@ -0,0 +1 @@
<%response.sendRedirect(request.getContextPath() + "/recipeSearch");%>
+89
View File
@@ -0,0 +1,89 @@
body {
font-family: geneva,tahoma,arial,sans-serif; font-size: 83%;
background: #FFFFCC;
text-align:center;
vertical-align:top;
}
td {
font-size: 78%;
vertical-align:top;
}
th {
font-size: 78%;
text-align: left;
vertical-align:top;
}
a:link { color: #0000CC; }
a:visited { color:#0000CC; }
a.on:visited { color: #0000CC; }
.toplink:visited { color: #0000CC; }
.demolabel a:link { color: #0000E0!important; font-weight:bold; }
a.m { display:block; }
a.r { color: #6666cc; text-decoration:none;}
#search th { padding: 1em 0 2px 0; }
#newitem th { padding: .8ex 1em 2px 0px; }
#newitem td { padding: 2px 1em 2px 0px; }
.demolabel { background: #ffffff; font-size: 124%; text-align:center; padding: .5em; -moz-border-radius:4px; border: 1px solid #EEEE66;}
input.txt, textarea { border: 1px inset #999999; -moz-border-radius: 3px; padding:2px;}
textarea { font-family: geneva, tahoma, arial, sans-serif; font-size: 105%; }
label { cursor: pointer; }
ul.inputlist {
padding-left:0;
list-style-type:none;
}
.inputlist li { padding-right: 1em; }
#content { text-align:left; margin: 0 15% 0 15%;}
#leftnav {
width: 30%;
float:left;
}
#search {
border:1px solid #EEEE66;
-moz-border-radius:12px;
background: #FFFFe0;
padding:.5em;
}
#rpp { float:right; }
.next { font-weight: bold; font-size: 124%; }
.main {
margin-left: 33%;
border: 1px solid #EEEE66;
-moz-border-radius:8px;
background: #FFFFe0;
padding: 1em;
}
#footer { margin-top: 2em; }
#footer p { margin:0; }
h2 { margin:0; }
h3 { margin:0; }
h4 { margin:0; font-size: 100%; font-weight: normal;}
h5 { margin:0; font-weight: normal; display:inline; font-size: 100%;}
.toplink {
float:right;
font-weight:bold;
margin-left:1.6em;
padding-top:1.6em;
font-size: 124%;
}
#poweredby {
font-size: 82%;
margin-left:3%;
}
#poweredby img { height: 27px; width: 75px; }
.instructions { font-size: 78%; color: #000000;}
.instructions i { font-style: normal; color:#008000; }
.errormessage { font-weight: bold; color: #CC1100; margin-bottom: 0.5em; }
.errordiv { padding-top: 35px; }