Extract RequestFactoryServlet from ExpensesDataServlet

Review at http://gwt-code-reviews.appspot.com/293803


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7872 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java b/bikeshed/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
new file mode 100644
index 0000000..b78b6fc
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2010 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 com.google.gwt.requestfactory.server;
+
+import com.google.gwt.requestfactory.shared.RequestFactory;
+import com.google.gwt.requestfactory.shared.RequestFactory.RequestDefinition;
+import com.google.gwt.requestfactory.shared.impl.RequestDataManager;
+import com.google.gwt.valuestore.shared.Property;
+import com.google.gwt.valuestore.shared.ValuesKey;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Handles GWT RequestFactory requests. Configured via servlet context param
+ * servlet.serverOperation, which must be set to the fully qualified name of an
+ * enum implementing {@link RequestDefinition}.
+ * <p>
+ * e.g.
+ * 
+ * <pre>  &lt;context-param>
+    &lt;param-name>servlet.serverOperation&lt;/param-name>
+    &lt;param-value>com.google.gwt.sample.expenses.shared.ExpenseRequestFactory$ServerSideOperation&lt;/param-value>
+  &lt;/context-param>
+
+ * </pre>
+ */
+public class RequestFactoryServlet extends HttpServlet {
+
+  private static final String SERVER_OPERATION_CONTEXT_PARAM = "servlet.serverOperation";
+
+  // TODO: Remove this hack
+  private static final Set<String> PROPERTY_SET = new HashSet<String>();
+  static {
+    for (String str : new String[] {
+        "id", "version", "displayName", "userName", "purpose", "created"}) {
+      PROPERTY_SET.add(str);
+    }
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+
+    RequestDefinition operation = null;
+    try {
+      response.setStatus(HttpServletResponse.SC_OK);
+      PrintWriter writer = response.getWriter();
+      JSONObject topLevelJsonObject = new JSONObject(getContent(request));
+      String operationName = topLevelJsonObject.getString(RequestDataManager.OPERATION_TOKEN);
+      if (operationName.equals(RequestFactory.UPDATE_STRING)) {
+        sync(topLevelJsonObject.getString(RequestDataManager.CONTENT_TOKEN),
+            writer);
+      } else {
+
+        operation = getOperationFromName(operationName, getConfigClass());
+        Class<?> domainClass = Class.forName(operation.getDomainClassName());
+        Method domainMethod = domainClass.getMethod(
+            operation.getDomainMethodName(), operation.getParameterTypes());
+        if (!Modifier.isStatic(domainMethod.getModifiers())) {
+          throw new IllegalArgumentException("the " + domainMethod.getName()
+              + " is not static");
+        }
+        Object args[] = RequestDataManager.getObjectsFromParameterMap(
+            getParameterMap(topLevelJsonObject),
+            domainMethod.getParameterTypes());
+        Object resultList = domainMethod.invoke(null, args);
+        if (!(resultList instanceof List<?>)) {
+          throw new IllegalArgumentException("return value not a list "
+              + resultList);
+        }
+        JSONArray jsonArray = getJsonArray((List<?>) resultList,
+            operation.getReturnType());
+        writer.print(jsonArray.toString());
+      }
+      writer.flush();
+      // TODO: clean exception handling code below.
+    } catch (ClassNotFoundException e) {
+      throw new IllegalArgumentException(e);
+    } catch (IllegalAccessException e) {
+      throw new IllegalArgumentException(e);
+    } catch (InvocationTargetException e) {
+      throw new IllegalArgumentException(e);
+    } catch (SecurityException e) {
+      throw new IllegalArgumentException(e);
+    } catch (JSONException e) {
+      throw new IllegalArgumentException(e);
+    } catch (NoSuchMethodException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  /**
+   * Allows subclass to provide hack implementation. TODO real reflection based
+   * implsementation.
+   */
+  @SuppressWarnings("unused")
+  protected void sync(String content, PrintWriter writer) {
+    return;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Class<? extends RequestDefinition> getConfigClass() {
+    try {
+      final String serverOperation = getServletContext().getInitParameter(
+          SERVER_OPERATION_CONTEXT_PARAM);
+      if (null == serverOperation) {
+        throw new IllegalStateException(String.format(
+            "Context parameter \"%s\" must name an enum implementing %s",
+            SERVER_OPERATION_CONTEXT_PARAM, RequestDefinition.class.getName()));
+      }
+      Class<?> clazz = Class.forName(serverOperation);
+      if (!RequestDefinition.class.isAssignableFrom(clazz)) {
+        throw new RuntimeException(serverOperation + " must implement "
+            + RequestDefinition.class.getName());
+      }
+      return (Class<? extends RequestDefinition>) clazz;
+    } catch (ClassNotFoundException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private String getContent(HttpServletRequest request) throws IOException {
+    int contentLength = request.getContentLength();
+    byte contentBytes[] = new byte[contentLength];
+    BufferedInputStream bis = new BufferedInputStream(request.getInputStream());
+    int readBytes = 0;
+    while (bis.read(contentBytes, readBytes, contentLength - readBytes) > 0) {
+      // read the contents
+    }
+    // TODO: encoding issues?
+    return new String(contentBytes);
+  }
+
+  /**
+   * Converts the returnValue of a 'get' method to a JSONArray.
+   * 
+   * @param resultObject object returned by a 'get' method, must be of type
+   *          List<?>
+   * @return the JSONArray
+   */
+  private JSONArray getJsonArray(List<?> resultList,
+      Class<? extends ValuesKey<?>> entityKeyClass) throws JSONException,
+      NoSuchMethodException, IllegalAccessException, InvocationTargetException {
+    JSONArray jsonArray = new JSONArray();
+    if (resultList.size() == 0) {
+      return jsonArray;
+    }
+    ValuesKey<?> key = (ValuesKey<?>) entityKeyClass.getMethod("get").invoke(
+        null);
+    for (Object entityElement : resultList) {
+      JSONObject jsonObject = new JSONObject();
+      for (Property<?, ?> p : key.all()) {
+
+        if (requestedProperty(p)) {
+          String propertyName = p.getName();
+          jsonObject.put(propertyName, getPropertyValue(entityElement,
+              propertyName));
+        }
+      }
+      jsonArray.put(jsonObject);
+    }
+    return jsonArray;
+  }
+
+  /**
+   * Returns methodName corresponding to the propertyName that can be invoked on
+   * an {@link Entity} object.
+   * 
+   * Example: "userName" returns "getUserName". "version" returns "getVersion"
+   */
+  private String getMethodNameFromPropertyName(String propertyName) {
+    if (propertyName == null) {
+      throw new NullPointerException("propertyName must not be null");
+    }
+
+    StringBuffer methodName = new StringBuffer("get");
+    methodName.append(propertyName.substring(0, 1).toUpperCase());
+    methodName.append(propertyName.substring(1));
+    return methodName.toString();
+  }
+
+  private RequestDefinition getOperationFromName(String operationName,
+      Class<? extends RequestDefinition> enumClass) throws SecurityException,
+      IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+    for (RequestDefinition operation : ((RequestDefinition[]) enumClass.getMethod(
+        "values").invoke(null))) {
+      if (operation.name().equals(operationName)) {
+        return operation;
+      }
+    }
+    throw new IllegalArgumentException("Unknown operation " + operationName);
+  }
+
+  /**
+   * @param jsonObject
+   * @return
+   * @throws JSONException
+   */
+  private Map<String, String> getParameterMap(JSONObject jsonObject)
+      throws JSONException {
+    Map<String, String> parameterMap = new HashMap<String, String>();
+    Iterator<?> keys = jsonObject.keys();
+    while (keys.hasNext()) {
+      String key = keys.next().toString();
+      if (key.startsWith(RequestDataManager.PARAM_TOKEN)) {
+        parameterMap.put(key, jsonObject.getString(key));
+      }
+    }
+    return parameterMap;
+  }
+
+  /**
+   * @param entityElement
+   * @param property
+   * @return
+   */
+  private Object getPropertyValue(Object entityElement, String propertyName)
+      throws SecurityException, NoSuchMethodException, IllegalAccessException,
+      InvocationTargetException {
+    String methodName = getMethodNameFromPropertyName(propertyName);
+    Method method = entityElement.getClass().getMethod(methodName);
+    Object returnValue = method.invoke(entityElement);
+    /*
+     * TODO: make these conventions more prominent. 1. encoding long as String
+     * 2. encoding Date as Double
+     */
+    if (returnValue instanceof java.lang.Long) {
+      return returnValue.toString();
+    }
+    if (returnValue instanceof java.util.Date) {
+      return new Double(((java.util.Date) returnValue).getTime());
+    }
+    return returnValue;
+  }
+
+  /**
+   * returns true if the property has been requested. TODO: fix this hack.
+   * 
+   * @param p the field of entity ref
+   * @return has the property value been requested
+   */
+  private boolean requestedProperty(Property<?, ?> p) {
+    return PROPERTY_SET.contains(p.getName());
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java b/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java
index 3fb9d33..9a4b1af 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/server/ExpensesDataServlet.java
@@ -15,232 +15,25 @@
  */
 package com.google.gwt.sample.expenses.server;
 
-import com.google.gwt.requestfactory.shared.RequestFactory;
-import com.google.gwt.requestfactory.shared.RequestFactory.RequestDefinition;
-import com.google.gwt.requestfactory.shared.impl.RequestDataManager;
+import com.google.gwt.requestfactory.server.RequestFactoryServlet;
 import com.google.gwt.sample.expenses.server.domain.Report;
 import com.google.gwt.sample.expenses.server.domain.Storage;
 import com.google.gwt.sample.expenses.shared.ReportKey;
-import com.google.gwt.valuestore.shared.Property;
-import com.google.gwt.valuestore.shared.ValuesKey;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.io.BufferedInputStream;
-import java.io.IOException;
 import java.io.PrintWriter;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 
 /**
- *
+ * Dwindling interim servlet that calls our mock storage backend directly
+ * instead of reflectively. Should soon vanish completely. 
  */
-public class ExpensesDataServlet extends HttpServlet {
-
-  // TODO: Remove this hack
-  private static final Set<String> PROPERTY_SET = new HashSet<String>();
-  static {
-    for (String str : new String[] {
-        "id", "version", "displayName", "userName", "purpose", "created"}) {
-      PROPERTY_SET.add(str);
-    }
-  }
+public class ExpensesDataServlet extends RequestFactoryServlet {
 
   @Override
-  protected void doPost(HttpServletRequest request, HttpServletResponse response)
-      throws IOException {
-
-    RequestDefinition operation = null;
-    try {
-      response.setStatus(HttpServletResponse.SC_OK);
-      PrintWriter writer = response.getWriter();
-      JSONObject topLevelJsonObject = new JSONObject(getContent(request));
-      String operationName = topLevelJsonObject.getString(RequestDataManager.OPERATION_TOKEN);
-      if (operationName.equals(RequestFactory.UPDATE_STRING)) {
-        sync(topLevelJsonObject.getString(RequestDataManager.CONTENT_TOKEN),
-            writer);
-      } else {
-        operation = getOperationFromName(
-            operationName,
-            (Class<RequestDefinition>) Class.forName(getServletContext().getInitParameter(
-                "servlet.serverOperation")));
-        Class<?> domainClass = Class.forName(operation.getDomainClassName());
-        Method domainMethod = domainClass.getMethod(
-            operation.getDomainMethodName(), operation.getParameterTypes());
-        if (!Modifier.isStatic(domainMethod.getModifiers())) {
-          throw new IllegalArgumentException("the " + domainMethod.getName()
-              + " is not static");
-        }
-        Object args[] = RequestDataManager.getObjectsFromParameterMap(
-            getParameterMap(topLevelJsonObject),
-            domainMethod.getParameterTypes());
-        Object resultList = domainMethod.invoke(null, args);
-        if (!(resultList instanceof List)) {
-          throw new IllegalArgumentException("return value not a list "
-              + resultList);
-        }
-        JSONArray jsonArray = getJsonArray((List<?>) resultList,
-            operation.getReturnType());
-        writer.print(jsonArray.toString());
-      }
-      writer.flush();
-      // TODO: clean exception handling code below.
-    } catch (ClassNotFoundException e) {
-      throw new IllegalArgumentException(e);
-    } catch (IllegalAccessException e) {
-      throw new IllegalArgumentException(e);
-    } catch (InvocationTargetException e) {
-      throw new IllegalArgumentException(e);
-    } catch (SecurityException e) {
-      throw new IllegalArgumentException(e);
-    } catch (JSONException e) {
-      throw new IllegalArgumentException(e);
-    } catch (NoSuchMethodException e) {
-      throw new IllegalArgumentException(e);
-    }
-  }
-
-  private String getContent(HttpServletRequest request) throws IOException {
-    int contentLength = request.getContentLength();
-    byte contentBytes[] = new byte[contentLength];
-    BufferedInputStream bis = new BufferedInputStream(request.getInputStream());
-    int readBytes = 0;
-    while (bis.read(contentBytes, readBytes, contentLength - readBytes) > 0) {
-      // read the contents
-    }
-    // TODO: encoding issues?
-    return new String(contentBytes);
-  }
-
-  /**
-   * Converts the returnValue of a 'get' method to a JSONArray.
-   * 
-   * @param resultObject object returned by a 'get' method, must be of type
-   *          List<?>
-   * @return the JSONArray
-   */
-  private JSONArray getJsonArray(List<?> resultList,
-      Class<? extends ValuesKey<?>> entityKeyClass) throws JSONException,
-      NoSuchMethodException, IllegalAccessException, InvocationTargetException {
-    JSONArray jsonArray = new JSONArray();
-    if (resultList.size() == 0) {
-      return jsonArray;
-    }
-    ValuesKey<?> key = (ValuesKey<?>) entityKeyClass.getMethod("get").invoke(
-        null);
-    for (Object entityElement : resultList) {
-      JSONObject jsonObject = new JSONObject();
-      for (Property<?, ?> p : key.all()) {
-
-        if (requestedProperty(p)) {
-          String propertyName = p.getName();
-          jsonObject.put(propertyName, getPropertyValue(entityElement,
-              propertyName));
-        }
-      }
-      jsonArray.put(jsonObject);
-    }
-    return jsonArray;
-  }
-
-  /**
-   * Returns methodName corresponding to the propertyName that can be invoked on
-   * an {@link Entity} object.
-   * 
-   * Example: "userName" returns "getUserName". "version" returns "getVersion"
-   */
-  private String getMethodNameFromPropertyName(String propertyName) {
-    if (propertyName == null) {
-      throw new NullPointerException("propertyName must not be null");
-    }
-
-    StringBuffer methodName = new StringBuffer("get");
-    methodName.append(propertyName.substring(0, 1).toUpperCase());
-    methodName.append(propertyName.substring(1));
-    return methodName.toString();
-  }
-
-  private RequestDefinition getOperationFromName(String operationName,
-      Class<RequestDefinition> enumClass) throws SecurityException,
-      IllegalAccessException, InvocationTargetException, NoSuchMethodException {
-    for (RequestDefinition operation : ((RequestDefinition[]) enumClass.getMethod(
-        "values").invoke(null))) {
-      if (operation.name().equals(operationName)) {
-        return operation;
-      }
-    }
-    throw new IllegalArgumentException("Unknown operation " + operationName);
-  }
-
-  /**
-   * @param jsonObject
-   * @return
-   * @throws JSONException
-   */
-  private Map<String, String> getParameterMap(JSONObject jsonObject)
-      throws JSONException {
-    Map<String, String> parameterMap = new HashMap<String, String>();
-    Iterator<?> keys = jsonObject.keys();
-    while (keys.hasNext()) {
-      String key = keys.next().toString();
-      if (key.startsWith(RequestDataManager.PARAM_TOKEN)) {
-        parameterMap.put(key, jsonObject.getString(key));
-      }
-    }
-    return parameterMap;
-  }
-
-  /**
-   * @param entityElement
-   * @param property
-   * @return
-   */
-  private Object getPropertyValue(Object entityElement, String propertyName)
-      throws SecurityException, NoSuchMethodException, IllegalAccessException,
-      InvocationTargetException {
-    String methodName = getMethodNameFromPropertyName(propertyName);
-    Method method = entityElement.getClass().getMethod(methodName);
-    Object returnValue = method.invoke(entityElement);
-    /*
-     * TODO: make these conventions more prominent. 1. encoding long as String
-     * 2. encoding Date as Double
-     */
-    if (returnValue instanceof java.lang.Long) {
-      return returnValue.toString();
-    }
-    if (returnValue instanceof java.util.Date) {
-      return new Double(((java.util.Date) returnValue).getTime());
-    }
-    return returnValue;
-  }
-
-  /**
-   * returns true if the property has been requested. TODO: fix this hack.
-   * 
-   * @param p the field of entity ref
-   * @return has the property value been requested
-   */
-  private boolean requestedProperty(Property<?, ?> p) {
-    return PROPERTY_SET.contains(p.getName());
-  }
-
-  /**
-   * @throws IOException
-   */
-  private void sync(String content, PrintWriter writer) throws IOException {
+  protected void sync(String content, PrintWriter writer) {
 
     try {
       JSONArray reportArray = new JSONArray(content);
diff --git a/bikeshed/war/WEB-INF/web.xml b/bikeshed/war/WEB-INF/web.xml
index 9a43bd7..f962f53 100644
--- a/bikeshed/war/WEB-INF/web.xml
+++ b/bikeshed/war/WEB-INF/web.xml
@@ -5,7 +5,8 @@
 
 <web-app>
 
- <context-param>
+  <!--  Configures expensesData servlet -->
+  <context-param>
     <param-name>servlet.serverOperation</param-name>
     <param-value>com.google.gwt.sample.expenses.shared.ExpenseRequestFactory$ServerSideOperation</param-value>
   </context-param>
@@ -46,7 +47,7 @@
     <url-pattern>/tree/tree</url-pattern>
   </servlet-mapping>
 
-  <!-- Default page to serve --> 
+  <!-- Default page to serve -->
   <welcome-file-list>
     <welcome-file>Tree.html</welcome-file>
   </welcome-file-list>
@@ -54,6 +55,7 @@
   <!-- Require login. -->
   <security-constraint>
     <web-resource-collection>
+      <web-resource-name>Access</web-resource-name>
       <url-pattern>/*</url-pattern>
     </web-resource-collection>
     <auth-constraint>