Author: lukaszlenart
Date: Sat Oct 23 20:19:47 2010
New Revision: 1026675

URL: http://svn.apache.org/viewvc?rev=1026675&view=rev
Log:
Solved WW-3494 - many improvments to REST plugin

Added:
    
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
Modified:
    
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
    
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
    
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
    
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
    
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
    
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
    
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
    struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml
    
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
    
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java

Modified: 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
 Sat Oct 23 20:19:47 2010
@@ -61,4 +61,12 @@ public interface ContentTypeHandlerManag
      */
     String handleResult(ActionConfig actionConfig, Object methodResult, Object 
target)
             throws IOException;
+    
+    /**
+     * Finds the extension in the url
+     * 
+     * @param url The url
+     * @return The extension
+     */
+    String findExtension(String url);
 }

Modified: 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
 Sat Oct 23 20:19:47 2010
@@ -102,9 +102,6 @@ public class DefaultContentTypeHandlerMa
         
         if (handler == null) {
             String extension = findExtension(req.getRequestURI());
-            if (extension == null) {
-                extension = defaultExtension;
-            }
             handler = handlersByExtension.get(extension);
         }
         return handler;
@@ -117,9 +114,6 @@ public class DefaultContentTypeHandlerMa
      */
     public ContentTypeHandler getHandlerForResponse(HttpServletRequest req, 
HttpServletResponse res) {
         String extension = findExtension(req.getRequestURI());
-        if (extension == null) {
-            extension = defaultExtension;
-        }
         return handlersByExtension.get(extension);
     }
 
@@ -137,33 +131,7 @@ public class DefaultContentTypeHandlerMa
         String resultCode = null;
         HttpServletRequest req = ServletActionContext.getRequest();
         HttpServletResponse res = ServletActionContext.getResponse();
-        if (target instanceof ModelDriven) {
-            target = ((ModelDriven)target).getModel();
-        }
-
-        boolean statusNotOk = false;
-        if (methodResult instanceof HttpHeaders) {
-            HttpHeaders info = (HttpHeaders) methodResult;
-            resultCode = info.apply(req, res, target);
-            if (info.getStatus() != SC_OK) {
-
-                // Don't return content on a not modified
-                if (info.getStatus() == SC_NOT_MODIFIED) {
-                    target = null;
-                } else {
-                    statusNotOk = true;
-                }
-
-            }
-        } else {
-            resultCode = (String) methodResult;
-        }
-        
-        // Don't return any content for PUT, DELETE, and POST where there are 
no errors
-        if (!statusNotOk && !"get".equalsIgnoreCase(req.getMethod())) {
-            target = null;
-        }
-
+               
         ContentTypeHandler handler = getHandlerForResponse(req, res);
         if (handler != null) {
             String extCode = resultCode+"-"+handler.getExtension();
@@ -192,12 +160,12 @@ public class DefaultContentTypeHandlerMa
      * @param url The url
      * @return The extension
      */
-    protected String findExtension(String url) {
+    public String findExtension(String url) {
         int dotPos = url.lastIndexOf('.');
         int slashPos = url.lastIndexOf('/');
         if (dotPos > slashPos && dotPos > -1) {
             return url.substring(dotPos+1);
         }
-        return null;
+        return defaultExtension;
     }
 }

Modified: 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
 Sat Oct 23 20:19:47 2010
@@ -152,6 +152,14 @@ public class DefaultHttpHeaders implemen
         return status;
     }
     
+    public void setStatus(int s) {
+       status = s;
+    }
+
+       public String getResultCode() {
+               return resultCode;
+       }
+    
     
     
     

Modified: 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
 Sat Oct 23 20:19:47 2010
@@ -43,4 +43,15 @@ public interface HttpHeaders {
      * The HTTP status code
      */
     int getStatus();
+
+    /**
+     * The HTTP status code
+     */
+    void setStatus(int status);
+
+    /**
+     * The result code to process
+     */
+    String getResultCode();
+
 }
\ No newline at end of file

Modified: 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
 Sat Oct 23 20:19:47 2010
@@ -24,17 +24,30 @@ package org.apache.struts2.rest;
 import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.DefaultActionInvocation;
 import com.opensymphony.xwork2.Result;
-import com.opensymphony.xwork2.UnknownHandlerManager;
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ModelDriven;
+import com.opensymphony.xwork2.ValidationAware;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.entities.ResultConfig;
 import com.opensymphony.xwork2.config.entities.ActionConfig;
 import com.opensymphony.xwork2.inject.Inject;
 import com.opensymphony.xwork2.util.logging.Logger;
 import com.opensymphony.xwork2.util.logging.LoggerFactory;
 import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
 
-import java.io.IOException;
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Map;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.dispatcher.HttpHeaderResult;
+import org.apache.struts2.rest.handler.ContentTypeHandler;
+import org.apache.struts2.rest.handler.HtmlHandler;
 
 
 /**
@@ -49,11 +62,28 @@ public class RestActionInvocation extend
     private static final Logger LOG = 
LoggerFactory.getLogger(RestActionInvocation.class);
     
     private ContentTypeHandlerManager handlerSelector;
+    private boolean logger;
+    private String defaultErrorResultName;
 
+    protected HttpHeaders httpHeaders;
+    protected Object target;
+    protected boolean isFirstInterceptor = true;
+    protected boolean hasErrors;
+    
     protected RestActionInvocation(Map extraContext, boolean pushAction) {
         super(extraContext, pushAction);
     }
 
+    @Inject("struts.rest.logger")
+    public void setLogger(String value) {
+        logger = new Boolean(value); 
+    }
+    
+    @Inject("struts.rest.defaultErrorResultName")
+    public void setDefaultErrorResultName(String value) {
+        defaultErrorResultName = value; 
+    }
+    
     @Inject
     public void setMimeTypeHandlerSelector(ContentTypeHandlerManager sel) {
         this.handlerSelector = sel;
@@ -100,7 +130,7 @@ public class RestActionInvocation extend
                 methodResult = method.invoke(action, new Object[0]);
             }
             
-            return processResult(actionConfig, methodResult);
+            return saveResult(actionConfig, methodResult);
         } catch (NoSuchMethodException e) {
             throw new IllegalArgumentException("The " + methodName + "() is 
not defined in action " + getAction().getClass() + "");
         } catch (InvocationTargetException e) {
@@ -123,16 +153,301 @@ public class RestActionInvocation extend
         }
     }
 
-    protected String processResult(ActionConfig actionConfig, Object 
methodResult) throws IOException {
-        if (methodResult instanceof Result) {
-            this.explicitResult = (Result) methodResult;
+    /**
+     * Save the result to be used later.
+     * @param actionConfig
+     * @param methodResult the result of the action.
+     * @return the result code to process.
+     *
+     * @throws ConfigurationException If it is an incorrect result.
+     */
+    protected String saveResult(ActionConfig actionConfig, Object 
methodResult) {
+       if (methodResult instanceof Result) {
+            explicitResult = (Result) methodResult;
             // Wire the result automatically
             container.inject(explicitResult);
-            return null;
+        } else if (methodResult instanceof HttpHeaders) {
+            httpHeaders = (HttpHeaders) methodResult;
+            resultCode = httpHeaders.getResultCode();
+        } else if (methodResult instanceof String) {
+            resultCode = (String) methodResult;
         } else if (methodResult != null) {
-            resultCode = handlerSelector.handleResult(actionConfig, 
methodResult, action);
+               throw new ConfigurationException("The result type " + 
methodResult.getClass()
+                               + " is not allowed. Use the type String, 
HttpHeaders or Result.");
         }
         return resultCode;
     }
 
+       @Override
+       public String invoke() throws Exception {
+               long startTime = 0;
+               
+               boolean executeResult = false;
+               if (isFirstInterceptor) {
+                       startTime = System.currentTimeMillis();         
+                       executeResult = true;
+                       isFirstInterceptor = false;
+               }
+               
+               // Normal invoke without execute the result
+               proxy.setExecuteResult(false);
+               resultCode = super.invoke();
+               
+               // Execute the result when the last interceptor has finished
+               if (executeResult) {
+                       long middleTime = System.currentTimeMillis();
+            
+                       try {
+                               processResult();
+               
+                       } catch (ConfigurationException e) {
+                               throw e;
+                               
+               } catch (Exception e) {
+                       
+                       // Error proccesing the result
+                       LOG.error("Exception processing the result.", e);
+                       
+                       if (!ServletActionContext.getResponse().isCommitted()) {
+                               ServletActionContext.getResponse()
+                                               
.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                                       stack.set("exception", e);
+                                       result = null;
+                                       resultCode = null;
+                               processResult();
+                       }
+               }
+                       
+                       // Log execution + result time
+            logger(startTime, middleTime);
+        }
+               
+               return resultCode;
+       }
+       
+       protected void processResult() throws Exception {
+               String timerKey = "processResult: " + getResultCode();
+        try {
+            UtilTimerStack.push(timerKey);
+
+            HttpServletRequest request = ServletActionContext.getRequest();
+            HttpServletResponse response = ServletActionContext.getResponse();
+
+               // Select the target
+            selectTarget();
+            
+               // Get the httpHeaders
+            if (httpHeaders == null) {
+               httpHeaders = new DefaultHttpHeaders(resultCode);
+            }
+
+            // Apply headers
+            if (!hasErrors) {
+               httpHeaders.apply(request, response, target);
+            } else {
+               disableCatching(response);
+            }
+            
+            // Don't return content on a not modified
+            if (httpHeaders.getStatus() != HttpServletResponse.SC_NOT_MODIFIED 
) {
+                       executeResult();
+            } else {
+               if (LOG.isDebugEnabled()) {
+                       LOG.debug("Result not processed because the status code 
is not modified.");
+                   }
+            }
+        
+        } finally {
+            UtilTimerStack.pop(timerKey);
+        }
+    }
+
+       /**
+     * Execute the current result. If it is an error and no result is selected 
load 
+     * the default error result (default-error).
+     */
+       private void executeResult() throws Exception {
+               
+       // Get handler by representation
+       ContentTypeHandler handler = handlerSelector.getHandlerForResponse(
+                       ServletActionContext.getRequest(), 
ServletActionContext.getResponse());
+       
+               // get the result
+               this.result = createResult();
+        
+               if (this.result instanceof HttpHeaderResult) {
+                       
+                       // execute the result to apply headers and status in 
every representations
+               this.result.execute(this);
+               updateStatusFromResult();
+               }
+               
+               if (handler != null && !(handler instanceof HtmlHandler)) {
+                       
+                       // Specific representation (json, xml...)
+                       resultCode = handlerSelector.handleResult(
+                                       this.getProxy().getConfig(), 
httpHeaders, target);
+
+               } else {
+                       
+                       // Normal struts execution (html o other struts result)
+                       findResult();
+               if (result != null) {
+               this.result.execute(this);
+               
+               } else {
+                   if (LOG.isDebugEnabled()) {
+                       LOG.debug("No result returned for action " + 
getAction().getClass().getName() 
+                                       + " at " + 
proxy.getConfig().getLocation());
+                   }
+               }
+               }
+       }
+       
+       /**
+        * Get the status code from HttpHeaderResult 
+        * and it is saved in the HttpHeaders object.
+        * @throws Exception
+        */
+       private void updateStatusFromResult() {
+               
+               if (this.result instanceof HttpHeaderResult) {
+                       try {
+                       Field field = 
result.getClass().getDeclaredField("status");
+                       if (field != null) {
+                               field.setAccessible(true);
+                               int status = (Integer)field.get(result);
+                               if (status != -1) {
+                               this.httpHeaders.setStatus(status);
+                               }
+                       }
+                       } catch (Exception e) {
+                               if (LOG.isDebugEnabled()) {
+                                       LOG.debug(e.getMessage(), e);
+                               }
+                       }
+               }               
+       }
+       
+       /**
+     * Find the most appropriate result:
+     * - Find by result code.
+     * - If it is an error, find the default error result.
+     *
+     * @throws ConfigurationException If not result can be found
+     */
+       private void findResult() throws Exception {
+               
+               boolean isHttpHeaderResult = false;
+               if (result != null && this.result instanceof HttpHeaderResult) {
+                       result = null;
+                       isHttpHeaderResult = true;
+               }
+               
+               if (result == null && resultCode != null && 
!Action.NONE.equals(resultCode)
+                               && unknownHandlerManager.hasUnknownHandlers()) {
+               
+                       // Find result by resultCode
+               this.result = unknownHandlerManager.handleUnknownResult(
+                               invocationContext, proxy.getActionName(), 
proxy.getConfig(), resultCode);
+       }
+               
+               if (this.result == null && this.hasErrors && 
defaultErrorResultName != null) {
+               
+                       // Get default error result
+               ResultConfig resultConfig = this.proxy.getConfig().getResults()
+                       .get(defaultErrorResultName);
+               if (resultConfig != null) {
+                       this.result = objectFactory.buildResult(resultConfig, 
+                               invocationContext.getContextMap());
+                       if (LOG.isDebugEnabled()) {
+                       LOG.debug("Found default error result.");
+                   }
+               }
+        }
+               
+        if (result == null && resultCode != null && 
+                       !Action.NONE.equals(resultCode) && !isHttpHeaderResult) 
{
+            throw new ConfigurationException("No result defined for action " 
+                       + getAction().getClass().getName()
+                    + " and result " + getResultCode(), proxy.getConfig());
+        }
+       }
+       
+    @SuppressWarnings("unchecked")
+       protected void selectTarget() {
+       
+       // Select target (content to return)
+       Throwable e = (Throwable)stack.findValue("exception");
+        if (e != null) {
+               
+               // Exception
+               target = e;
+               hasErrors = true;
+        
+        } else if (action instanceof ValidationAware && 
((ValidationAware)action).hasErrors()) {
+               
+               // Error messages
+               ValidationAware validationAwareAction = 
((ValidationAware)action);
+               
+               Map errors = new HashMap();
+               if (validationAwareAction.getActionErrors().size() > 0) {
+               errors.put("actionErrors", 
validationAwareAction.getActionErrors());
+               }
+               if (validationAwareAction.getFieldErrors().size() > 0) {
+               errors.put("fieldErrors", 
validationAwareAction.getFieldErrors());
+               }
+               target = errors;
+               hasErrors = true;
+        
+        } else if (action instanceof ModelDriven) {
+               
+               // Model
+            target = ((ModelDriven)action).getModel();
+        
+        } else {
+               target = action;
+        }
+       
+        // don't return any content for PUT, DELETE, and POST where there are 
no errors
+               if (!hasErrors && 
!"get".equalsIgnoreCase(ServletActionContext.getRequest().getMethod())) {
+                       target = null;
+               }
+    }
+    
+    private void disableCatching(HttpServletResponse response) {
+       // No cache
+        response.setHeader("Cache-Control", "no-cache");
+        response.setDateHeader("Last-Modified", 0);
+        response.setHeader("ETag", "-1");
+    }
+    
+    private void logger(long startTime, long middleTime) {
+            if (logger && LOG.isInfoEnabled()) {
+                long endTime = System.currentTimeMillis();
+                        long executionTime = middleTime - startTime;
+                        long processResult = endTime - middleTime;
+                        long total = endTime - startTime;
+
+                    String message = "Executed action [/";
+                    String namespace = getProxy().getNamespace();
+                    if ((namespace != null) && (namespace.trim().length() > 
1)) {
+                        message += namespace + "/";
+                    }
+                    message += getProxy().getActionName() + "!" + 
getProxy().getMethod();
+                    String extension = handlerSelector.findExtension(
+                                
ServletActionContext.getRequest().getRequestURI());
+                    if (extension != null) {
+                        message += "!" + extension;
+                    }
+                    if (httpHeaders != null) {
+                        message += "!" + httpHeaders.getStatus();
+                    }
+                    message += "] took " + total + " ms (execution: " + 
executionTime 
+                       + " ms, result: " + processResult + " ms)";
+                    
+                    LOG.info(message);
+            }
+       }
+       
 }

Modified: 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
 Sat Oct 23 20:19:47 2010
@@ -106,6 +106,9 @@ public class RestActionMapper extends De
     private String newMethodName = "editNew";
     private String deleteMethodName = "destroy";
     private String putMethodName = "update";
+    private String optionsMethodName = "options";
+    private String postContinueMethodName = "createContinue";
+    private String putContinueMethodName = "updateContinue";
     
     public RestActionMapper() {
     }
@@ -154,6 +157,21 @@ public class RestActionMapper extends De
         this.putMethodName = putMethodName;
     }
 
+    @Inject(required=false,value="struts.mapper.optionsMethodName")
+    public void setOptionsMethodName(String optionsMethodName) {
+        this.optionsMethodName = optionsMethodName;
+    }
+
+    @Inject(required=false,value="struts.mapper.postContinueMethodName")
+    public void setPostContinueMethodName(String postContinueMethodName) {
+        this.postContinueMethodName = postContinueMethodName;
+    }
+
+    @Inject(required=false,value="struts.mapper.putContinueMethodName")
+    public void setPutContinueMethodName(String putContinueMethodName) {
+        this.putContinueMethodName = putContinueMethodName;
+    }
+
     public ActionMapping getMapping(HttpServletRequest request,
             ConfigurationManager configManager) {
         ActionMapping mapping = new ActionMapping();
@@ -209,8 +227,11 @@ public class RestActionMapper extends De
             // If a method hasn't been explicitly named, try to guess using 
ReST-style patterns
             if (mapping.getMethod() == null) {
 
-                // Handle uris with no id, possibly ending in '/'
-                if (lastSlashPos == -1 || lastSlashPos == fullName.length() 
-1) {
+               if (isOptions(request)) {
+                       mapping.setMethod(optionsMethodName);
+                
+               // Handle uris with no id, possibly ending in '/'
+               } else if (lastSlashPos == -1 || lastSlashPos == 
fullName.length() -1) {
 
                     // Index e.g. foo
                     if (isGet(request)) {
@@ -218,7 +239,11 @@ public class RestActionMapper extends De
                         
                     // Creating a new entry on POST e.g. foo
                     } else if (isPost(request)) {
-                        mapping.setMethod(postMethodName);
+                       if (isExpectContinue(request)) {
+                            mapping.setMethod(postContinueMethodName);
+                       } else {
+                            mapping.setMethod(postMethodName);
+                       }
                     }
 
                 // Handle uris with an id at the end
@@ -243,7 +268,11 @@ public class RestActionMapper extends De
                     
                     // Updating an item e.g. foo/1    
                     }  else if (isPut(request)) {
-                        mapping.setMethod(putMethodName);
+                       if (isExpectContinue(request)) {
+                            mapping.setMethod(putContinueMethodName);
+                       } else {
+                            mapping.setMethod(putMethodName);
+                       }
                     }
                 }
             }
@@ -334,4 +363,13 @@ public class RestActionMapper extends De
         }
     }
 
+    protected boolean isOptions(HttpServletRequest request) {
+        return "options".equalsIgnoreCase(request.getMethod());
+    }
+    
+    protected boolean isExpectContinue(HttpServletRequest request) {
+       String expect = request.getHeader("Expect");
+       return (expect != null && 
expect.toLowerCase().contains("100-continue")); 
+    }
+
 }

Modified: 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
 Sat Oct 23 20:19:47 2010
@@ -21,6 +21,13 @@
 
 package org.apache.struts2.rest;
 
+import java.lang.reflect.Method;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.struts2.ServletActionContext;
+
 import com.opensymphony.xwork2.ActionSupport;
 
 /**
@@ -29,7 +36,83 @@ import com.opensymphony.xwork2.ActionSup
  */
 public class RestActionSupport extends ActionSupport {
 
-    public String index() throws Exception {
+       private static final long serialVersionUID = -889518620073576882L;
+       
+       private static final String DELETE = "DELETE";
+       private static final String PUT = "PUT";
+       private static final String POST = "POST";
+       private static final String GET = "GET";
+       private static final String OPTIONS = "OPTIONS";
+       private static final String DIVIDER = ", ";
+
+       /**
+        * Default execution.
+        * @return object because it can return string, result or httpHeader.
+        * @throws Exception
+        */
+    public Object index() throws Exception {
         return execute();
     }
+    
+       /**
+        * Inspect the implemented methods to know the allowed http methods.
+        * 
+        * @return Include the header "Allow" with the allowed http methods. 
+        */
+    public HttpHeaders options() {
+       
+       String methods = OPTIONS;
+       
+       Method[] meths = this.getClass().getDeclaredMethods();
+       for (Method m : meths) {
+               String methodName = m.getName();
+               if (!methods.contains(GET) &&
+                               (methodName.equals("index")
+                               || methodName.equals("show")
+                               || methodName.equals("edit")
+                               || methodName.equals("editNew"))) {
+                       methods += DIVIDER + GET;
+               } else if (methodName.equals("create")) {
+                       methods += DIVIDER + POST;
+               } else if (methodName.equals("update")) {
+                       methods += DIVIDER + PUT;
+               }else if (methodName.equals("destroy")) {
+                       methods += DIVIDER + DELETE;
+               }
+       }
+       
+       HttpServletRequest request = ServletActionContext.getRequest();
+       HttpServletResponse response = ServletActionContext.getResponse();
+       response.addHeader("Allow", methods);
+       
+       DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
+       httpHeaders.apply(request, response, this);
+       httpHeaders.disableCaching().withStatus(HttpServletResponse.SC_OK);
+       
+       return httpHeaders;
+       }
+    
+    /**
+     * By default, return continue.
+     * Is possible override the method to return expectation failed.
+     * 
+     * @return continue
+     */
+    public HttpHeaders createContinue() {
+               return new DefaultHttpHeaders()
+                   .disableCaching()
+                   .withStatus(HttpServletResponse.SC_CONTINUE);
+       }
+    
+    /**
+     * By default, return continue.
+     * Is possible override the method to return expectation failed.
+     * 
+     * @return continue
+     */
+    public HttpHeaders updateContinue() {
+               return new DefaultHttpHeaders()
+                   .disableCaching()
+                   .withStatus(HttpServletResponse.SC_CONTINUE);
+       }
 }

Modified: struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml 
(original)
+++ struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml Sat 
Oct 23 20:19:47 2010
@@ -40,6 +40,8 @@
 
     <constant name="struts.actionProxyFactory" value="rest" />
     <constant name="struts.rest.defaultExtension" value="xhtml" />
+    <constant name="struts.rest.logger" value="true" />
+    <constant name="struts.rest.defaultErrorResultName" value="default-error" 
/>
     <constant name="struts.mapper.class" value="rest" />
     <constant name="struts.mapper.idParameterName" value="id" />
     <constant name="struts.action.extension" value="xhtml,,xml,json" />

Modified: 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
 Sat Oct 23 20:19:47 2010
@@ -180,4 +180,20 @@ public class DefaultHttpHeadersTest exte
 
         assertEquals(SC_OK, mockResponse.getStatus());
     }
+    
+    public void testApplyOptions() {
+       
+       String methods = "OPTIONS, GET, POST, PUT";
+       String allow = "Allow";
+       
+       mockResponse.addHeader(allow, methods);
+       
+       DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
+       httpHeaders.apply(mockRequest, mockResponse, this);
+       httpHeaders.disableCaching().withStatus(SC_OK);
+       
+        assertEquals(methods, mockResponse.getHeader(allow));
+        assertEquals(SC_OK, mockResponse.getStatus());
+
+    }
 }

Added: 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java?rev=1026675&view=auto
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
 (added)
+++ 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
 Sat Oct 23 20:19:47 2010
@@ -0,0 +1,270 @@
+package org.apache.struts2.rest;
+
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.dispatcher.HttpHeaderResult;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.DefaultUnknownHandlerManager;
+import com.opensymphony.xwork2.ModelDriven;
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+import com.opensymphony.xwork2.config.entities.InterceptorMapping;
+import com.opensymphony.xwork2.config.entities.ResultConfig;
+import com.opensymphony.xwork2.mock.MockActionProxy;
+import com.opensymphony.xwork2.mock.MockInterceptor;
+import com.opensymphony.xwork2.util.XWorkTestCaseHelper;
+
+public class RestActionInvocationTest extends TestCase {
+
+       RestActionInvocation restActionInvocation;
+       MockHttpServletRequest request;
+       MockHttpServletResponse response;
+
+       @Override
+       protected void setUp() throws Exception {
+               super.setUp();
+               
+               restActionInvocation = new RestActionInvocationTester();
+               request = new MockHttpServletRequest();
+               response = new MockHttpServletResponse();
+               ServletActionContext.setRequest(request);
+               ServletActionContext.setResponse(response);
+
+       }
+       
+       /**
+        * Test the correct action results: null, String, HttpHeaders, Result
+        * @throws Exception
+        */
+       public void testSaveResult() throws Exception {
+
+               Object methodResult = "index";
+               ActionConfig actionConfig = 
restActionInvocation.getProxy().getConfig();
+               assertEquals("index", 
restActionInvocation.saveResult(actionConfig, methodResult));
+       
+               setUp();
+       methodResult = new DefaultHttpHeaders("show");
+       assertEquals("show", restActionInvocation.saveResult(actionConfig, 
methodResult));
+       assertEquals(methodResult, restActionInvocation.httpHeaders);
+       
+               setUp();
+       methodResult = new HttpHeaderResult(HttpServletResponse.SC_ACCEPTED);
+       assertEquals(null, restActionInvocation.saveResult(actionConfig, 
methodResult));
+       assertEquals(methodResult, restActionInvocation.createResult());
+
+               setUp();
+       try {
+               methodResult = new Object();
+               restActionInvocation.saveResult(actionConfig, methodResult);
+
+               // ko
+               assertFalse(true);
+               
+       } catch (ConfigurationException c) {
+               // ok, object not allowed
+       }
+       }
+       
+       /**
+        * Test the target selection: exception, error messages, model and null
+        * @throws Exception
+        */
+       public void testSelectTarget() throws Exception {
+               
+               // Exception
+               Exception e = new Exception();
+               restActionInvocation.getStack().set("exception", e);
+               restActionInvocation.selectTarget();
+               assertEquals(e, restActionInvocation.target);
+
+               // Error messages
+               setUp();
+               String actionMessage = "Error!";
+               RestActionSupport action = 
(RestActionSupport)restActionInvocation.getAction();
+               action.addActionError(actionMessage);
+               Map errors = new HashMap();
+               List<String> list = new ArrayList<String>();
+               list.add(actionMessage);
+       errors.put("actionErrors", list);
+       restActionInvocation.selectTarget();
+               assertEquals(errors, restActionInvocation.target);
+               
+       // Model with get and no content in post, put, delete
+       setUp();
+               RestAction restAction = 
(RestAction)restActionInvocation.getAction();
+               List<String> model = new ArrayList<String>();
+               model.add("Item");
+               restAction.model = model;
+               request.setMethod("GET");
+               restActionInvocation.selectTarget();
+               assertEquals(model, restActionInvocation.target);
+               request.setMethod("POST");
+               restActionInvocation.selectTarget();
+               assertEquals(null, restActionInvocation.target);
+               request.setMethod("PUT");
+               restActionInvocation.selectTarget();
+               assertEquals(null, restActionInvocation.target);
+               request.setMethod("DELETE");
+               restActionInvocation.selectTarget();
+               assertEquals(null, restActionInvocation.target);
+               
+       }
+
+       /**
+        * Test the not modified status code.
+        * @throws Exception
+        */
+       public void testResultNotModified() throws Exception {
+
+               request.addHeader("If-None-Match", "123");
+               request.setMethod("GET");
+
+               RestAction restAction = 
(RestAction)restActionInvocation.getAction();
+               List<String> model = new ArrayList<String>() {
+                       @Override
+                       public int hashCode() {
+                               return 123;
+                       }
+               };
+               model.add("Item");
+               restAction.model = model;
+               
+               restActionInvocation.processResult();
+               assertEquals(SC_NOT_MODIFIED, response.getStatus());
+        
+    }
+       
+       /**
+        * Test the default error result.
+        * @throws Exception
+        */
+       public void testDefaultErrorResult() throws Exception {
+               
+               // Exception
+               Exception e = new Exception();
+               restActionInvocation.getStack().set("exception", e);
+               request.setMethod("GET");
+
+               RestAction restAction = 
(RestAction)restActionInvocation.getAction();
+               List<String> model = new ArrayList<String>();
+               model.add("Item");
+               restAction.model = model;
+               
+               restActionInvocation.setDefaultErrorResultName("default-error");
+               ResultConfig resultConfig = new 
ResultConfig.Builder("default-error", 
+                       "org.apache.struts2.dispatcher.HttpHeaderResult")
+               .addParam("status", "123").build();
+           ActionConfig actionConfig = new 
ActionConfig.Builder("org.apache.rest", 
+                               "RestAction", "org.apache.rest.RestAction")
+               .addResultConfig(resultConfig)
+               .build();
+           
((MockActionProxy)restActionInvocation.getProxy()).setConfig(actionConfig);
+               
+               restActionInvocation.processResult();
+               assertEquals(123, response.getStatus());
+               
+       }
+       
+       public void testNoResult() throws Exception {
+               
+               RestAction restAction = 
(RestAction)restActionInvocation.getAction();
+               List<String> model = new ArrayList<String>();
+               model.add("Item");
+               restAction.model = model;
+               request.setMethod("GET");
+               restActionInvocation.setResultCode("index");
+               
+               try {
+                       restActionInvocation.processResult();
+
+               // ko
+               assertFalse(true);
+               
+       } catch (ConfigurationException c) {
+               // ok, no result
+       }
+
+       }
+       
+       /**
+        * Test the global execution
+        * @throws Exception
+        */
+       public void testInvoke() throws Exception {
+        
+           // Default index method return 'success'
+           
((MockActionProxy)restActionInvocation.getProxy()).setMethod("index");
+
+           // Define result 'success'
+               ResultConfig resultConfig = new ResultConfig.Builder("success", 
+                       "org.apache.struts2.dispatcher.HttpHeaderResult")
+               .addParam("status", "123").build();
+           ActionConfig actionConfig = new 
ActionConfig.Builder("org.apache.rest", 
+                               "RestAction", "org.apache.rest.RestAction")
+               .addResultConfig(resultConfig)
+               .build();
+           
((MockActionProxy)restActionInvocation.getProxy()).setConfig(actionConfig);
+
+               request.setMethod("GET");
+               
+        restActionInvocation.invoke();
+
+        assertEquals(123, response.getStatus());
+    }
+
+
+    class RestActionInvocationTester extends RestActionInvocation {
+       RestActionInvocationTester() {
+            super(new HashMap<String,String>(), true);
+            List<InterceptorMapping> interceptorMappings = new 
ArrayList<InterceptorMapping>();
+            MockInterceptor mockInterceptor = new MockInterceptor();
+            mockInterceptor.setFoo("interceptor");
+            mockInterceptor.setExpectedFoo("interceptor");
+            interceptorMappings.add(new InterceptorMapping("interceptor", 
mockInterceptor));
+            interceptors = interceptorMappings.iterator();
+            MockActionProxy actionProxy = new MockActionProxy();
+            ActionConfig actionConfig = new 
ActionConfig.Builder("org.apache.rest", 
+                               "RestAction", 
"org.apache.rest.RestAction").build();
+            actionProxy.setConfig(actionConfig);
+            proxy = actionProxy;
+            action = new RestAction();
+            setMimeTypeHandlerSelector(new DefaultContentTypeHandlerManager());
+            unknownHandlerManager = new DefaultUnknownHandlerManager();
+                       try {
+                               XWorkTestCaseHelper.setUp();
+                       } catch (Exception e) {
+                               throw new RuntimeException(e);
+                       }
+                       invocationContext = ActionContext.getContext();
+                       container = ActionContext.getContext().getContainer();
+                       stack = ActionContext.getContext().getValueStack();
+                       objectFactory = 
container.getInstance(ObjectFactory.class);
+                       
+        }
+       
+    }
+
+    class RestAction extends RestActionSupport implements 
ModelDriven<List<String>> {
+
+       List<String> model;
+               
+       public List<String> getModel() {
+                       return model;
+               }
+       
+    }
+}

Modified: 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
 (original)
+++ 
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
 Sat Oct 23 20:19:47 2010
@@ -191,5 +191,44 @@ public class RestActionMapperTest extend
         assertEquals(expectedName, mapping.getName());
         assertEquals(expectedNamespace, mapping.getNamespace());
     }
+    
+    public void testOptionsMapping() throws Exception {
+        req.setRequestURI("/myapp/animals/dog");
+        req.setServletPath("/animals/dog");
+        req.setMethod("OPTIONS");
+
+        ActionMapping mapping = mapper.getMapping(req, configManager);
+
+        assertEquals("/animals", mapping.getNamespace());
+        assertEquals("dog", mapping.getName());
+        assertEquals("options", mapping.getMethod());
+    }
+    
+    public void testPostContinueMapping() throws Exception {
+        req.setRequestURI("/myapp/animals/dog");
+        req.setServletPath("/animals/dog");
+        req.setMethod("POST");
+        req.addHeader("Expect", "100-continue");
+
+        ActionMapping mapping = mapper.getMapping(req, configManager);
+
+        assertEquals("/animals", mapping.getNamespace());
+        assertEquals("dog", mapping.getName());
+        assertEquals("createContinue", mapping.getMethod());
+    }
+    
+    public void testPutContinueMapping() throws Exception {
+        req.setRequestURI("/myapp/animals/dog/fido");
+        req.setServletPath("/animals/dog/fido");
+        req.setMethod("PUT");
+        req.addHeader("Expect", "100-continue");
+
+        ActionMapping mapping = mapper.getMapping(req, configManager);
+
+        assertEquals("/animals", mapping.getNamespace());
+        assertEquals("dog", mapping.getName());
+        assertEquals("updateContinue", mapping.getMethod());
+        assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
+    }
 
 }


Reply via email to