You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2010/10/23 22:19:48 UTC

svn commit: r1026675 - in /struts/struts2/trunk/plugins/rest/src: main/java/org/apache/struts2/rest/ main/resources/ test/java/org/apache/struts2/rest/

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]);
+    }
 
 }