Author: mrdon Date: Sat Oct 27 03:47:25 2007 New Revision: 589071 URL: http://svn.apache.org/viewvc?rev=589071&view=rev Log: Adding automatic support for conditional gets
Added: struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java Modified: struts/sandbox/trunk/struts2-rest-plugin/pom.xml struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java Modified: struts/sandbox/trunk/struts2-rest-plugin/pom.xml URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/pom.xml?rev=589071&r1=589070&r2=589071&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/pom.xml (original) +++ struts/sandbox/trunk/struts2-rest-plugin/pom.xml Sat Oct 27 03:47:25 2007 @@ -53,6 +53,13 @@ <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-mock</artifactId> + <version>1.2.8</version> + <optional>true</optional> + </dependency> + </dependencies> </project> Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java?rev=589071&r1=589070&r2=589071&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java Sat Oct 27 03:47:25 2007 @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; import static javax.servlet.http.HttpServletResponse.SC_OK; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -101,13 +102,20 @@ 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) { - statusNotOk = true; + + // Don't return content on a not modified + if (info.getStatus() == SC_NOT_MODIFIED) { + target = null; + } else { + statusNotOk = true; + } + } } else { resultCode = (String) methodResult; @@ -117,20 +125,22 @@ if (!statusNotOk && !"get".equalsIgnoreCase(req.getMethod())) { target = null; } - + ContentTypeHandler handler = getHandlerForRequest(req); - String extCode = resultCode+"-"+handler.getExtension(); - if (actionConfig.getResults().get(extCode) != null) { - resultCode = extCode; - } else { - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - - resultCode = handler.fromObject(target, resultCode, bout); - if (bout.size() > 0) { - res.setContentLength(bout.size()); - res.setContentType(handler.getContentType()); - res.getOutputStream().write(bout.toByteArray()); - res.getOutputStream().close(); + if (handler != null) { + String extCode = resultCode+"-"+handler.getExtension(); + if (actionConfig.getResults().get(extCode) != null) { + resultCode = extCode; + } else { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + resultCode = handler.fromObject(target, resultCode, bout); + if (bout.size() > 0) { + res.setContentLength(bout.size()); + res.setContentType(handler.getContentType()); + res.getOutputStream().write(bout.toByteArray()); + res.getOutputStream().close(); + } } } return resultCode; Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java?rev=589071&r1=589070&r2=589071&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java Sat Oct 27 03:47:25 2007 @@ -20,12 +20,11 @@ */ package org.apache.struts2.rest; -import static javax.servlet.http.HttpServletResponse.*; - -import java.util.Date; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import java.util.Date; /** * Default implementation of rest info that uses fluent-style construction @@ -37,6 +36,7 @@ Object locationId; String location; boolean disableCaching; + boolean noETag = false; Date lastModified; public DefaultHttpHeaders renderResult(String code) { @@ -53,6 +53,11 @@ this.etag = etag; return this; } + + public DefaultHttpHeaders withNoETag() { + this.noETag = true; + return this; + } public DefaultHttpHeaders setLocationId(Object id) { this.locationId = id; @@ -78,30 +83,60 @@ * @see org.apache.struts2.rest.HttpHeaders#apply(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object) */ public String apply(HttpServletRequest request, HttpServletResponse response, Object target) { - response.setStatus(status); + if (disableCaching) { response.setHeader("Cache-Control", "no-cache"); - } else if (lastModified != null) { - response.setDateHeader("LastModified", lastModified.getTime()); - } else { - if (etag == null) { - etag = String.valueOf(target.hashCode()); - } + } + if (lastModified != null) { + response.setDateHeader("Last-Modified", lastModified.getTime()); + } + if (etag == null && !noETag) { + etag = String.valueOf(target.hashCode()); + } + if (etag != null) { response.setHeader("ETag", etag.toString()); } + if (locationId != null) { String url = request.getRequestURL().toString(); int lastSlash = url.lastIndexOf("/"); int lastDot = url.lastIndexOf("."); if (lastDot > lastSlash && lastDot > -1) { - url = url.substring(0, lastDot)+locationId+url.substring(lastDot); + url = url.substring(0, lastDot)+"/"+locationId+url.substring(lastDot); } else { - url += locationId; + url += "/"+locationId; } response.setHeader("Location", url); } else if (location != null) { response.setHeader("Location", location); } + + if (status == SC_OK) { + boolean etagNotChanged = false; + boolean lastModifiedNotChanged = false; + String reqETag = request.getHeader("If-None-Match"); + if (etag != null) { + if (etag.equals(reqETag)) { + etagNotChanged = true; + } + } + + String reqLastModified = request.getHeader("If-Modified-Since"); + if (lastModified != null) { + if (String.valueOf(lastModified.getTime()).equals(reqLastModified)) { + lastModifiedNotChanged = true; + } + + } + + if ((etagNotChanged && lastModifiedNotChanged) || + (etagNotChanged && reqLastModified == null) || + (lastModifiedNotChanged && reqETag == null)) { + status = SC_NOT_MODIFIED; + } + } + + response.setStatus(status); return resultCode; } Modified: struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java?rev=589071&r1=589070&r2=589071&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java Sat Oct 27 03:47:25 2007 @@ -22,15 +22,77 @@ import com.mockobjects.dynamic.C; import com.mockobjects.dynamic.Mock; +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.config.entities.ActionConfig; import com.opensymphony.xwork2.inject.Container; import junit.framework.TestCase; +import org.apache.struts2.ServletActionContext; import org.apache.struts2.rest.handler.ContentTypeHandler; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; public class ContentTypeHandlerManagerTest extends TestCase { + + private ContentTypeHandlerManager mgr; + private MockHttpServletResponse mockResponse; + private MockHttpServletRequest mockRequest; + + @Override + public void setUp() { + mgr = new ContentTypeHandlerManager(); + mockResponse = new MockHttpServletResponse(); + mockRequest = new MockHttpServletRequest(); + mockRequest.setMethod("GET"); + ActionContext.setContext(new ActionContext(new HashMap())); + ServletActionContext.setRequest(mockRequest); + ServletActionContext.setResponse(mockResponse); + } + + @Override + public void tearDown() { + mockRequest = null; + mockRequest = null; + mgr = null; + } + + public void testHandleResultOK() throws IOException { + + String obj = "mystring"; + ContentTypeHandler handler = new ContentTypeHandler() { + public void toObject(InputStream in, Object target) {} + public String fromObject(Object obj, String resultCode, OutputStream stream) throws IOException { + stream.write(obj.toString().getBytes()); + return resultCode; + } + public String getContentType() { return "foo"; } + public String getExtension() { return "foo"; } + }; + mgr.handlers.put("xml", handler); + mgr.defaultHandlerName = "xml"; + mgr.handleResult(new ActionConfig(), new DefaultHttpHeaders().withStatus(SC_OK), obj); + + assertEquals(obj.getBytes().length, mockResponse.getContentLength()); + } + + public void testHandleResultNotModified() throws IOException { + + Mock mockHandlerXml = new Mock(ContentTypeHandler.class); + mockHandlerXml.matchAndReturn("getExtension", "xml"); + mgr.handlers.put("xml", (ContentTypeHandler) mockHandlerXml.proxy()); + mgr.handleResult(null, new DefaultHttpHeaders().withStatus(SC_NOT_MODIFIED), new Object()); + + assertEquals(0, mockResponse.getContentLength()); + } public void testHandlerOverride() { Mock mockHandlerXml = new Mock(ContentTypeHandler.class); Added: struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java?rev=589071&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java Sat Oct 27 03:47:25 2007 @@ -0,0 +1,175 @@ +/* + * $Id: Restful2ActionMapper.java 540819 2007-05-23 02:48:36Z mrdon $ + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.struts2.rest; + +import junit.framework.TestCase; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static javax.servlet.http.HttpServletResponse.*; +import java.util.Date; + +/** + * Created by IntelliJ IDEA. + * User: mrdon + * Date: 27/10/2007 + * Time: 18:14:48 + * To change this template use File | Settings | File Templates. + */ +public class DefaultHttpHeadersTest extends TestCase { + private MockHttpServletResponse mockResponse; + private MockHttpServletRequest mockRequest; + + @Override + public void setUp() { + mockResponse = new MockHttpServletResponse(); + mockRequest = new MockHttpServletRequest(); + } + + @Override + public void tearDown() { + mockRequest = null; + mockRequest = null; + } + + public void testApply() { + Date now = new Date(); + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .lastModified(now) + .withStatus(SC_OK) + .setLocationId("44") + .withETag("asdf"); + mockRequest.setRequestURI("/foo/bar.xhtml"); + + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals(SC_OK, mockResponse.getStatus()); + assertEquals("http://localhost:80/foo/bar/44.xhtml", mockResponse.getHeader("Location")); + assertEquals("asdf", mockResponse.getHeader("ETag")); + assertEquals(now.getTime(), mockResponse.getHeader("Last-Modified")); + + } + + public void testApplyNoLocationExtension() { + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .setLocationId("44"); + mockRequest.setRequestURI("/foo/bar"); + + headers.apply(mockRequest, mockResponse, new Object()); + assertEquals("http://localhost:80/foo/bar/44", mockResponse.getHeader("Location")); + + } + + public void testApplyFullLocation() { + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .setLocation("http://localhost/bar/44"); + mockRequest.setRequestURI("/foo/bar"); + + headers.apply(mockRequest, mockResponse, new Object()); + assertEquals("http://localhost/bar/44", mockResponse.getHeader("Location")); + + } + + public void testAutoETag() { + DefaultHttpHeaders headers = new DefaultHttpHeaders(); + headers.apply(mockRequest, mockResponse, new Object() { + @Override + public int hashCode() { + return 123; + } + }); + + assertEquals("123", mockResponse.getHeader("ETag")); + } + + public void testNoCache() { + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .disableCaching(); + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals("no-cache", mockResponse.getHeader("Cache-Control")); + } + + public void testConditionalGetForJustETag() { + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .withETag("asdf"); + mockRequest.addHeader("If-None-Match", "asdf"); + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus()); + assertEquals("asdf", mockResponse.getHeader("ETag")); + } + + public void testConditionalGetForJustETagNotOK() { + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .withETag("asdf") + .withStatus(SC_BAD_REQUEST); + mockRequest.addHeader("If-None-Match", "asdf"); + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals(SC_BAD_REQUEST, mockResponse.getStatus()); + assertEquals("asdf", mockResponse.getHeader("ETag")); + } + + public void testConditionalGetForJustLastModified() { + Date now = new Date(); + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .lastModified(now); + mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime())); + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus()); + } + + public void testConditionalGetForJustLastModifiedDifferent() { + Date now = new Date(); + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .lastModified(now); + mockRequest.addHeader("If-Modified-Since", String.valueOf(new Date(2323L).getTime())); + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals(SC_OK, mockResponse.getStatus()); + } + + public void testConditionalGetForLastModifiedAndETag() { + Date now = new Date(); + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .lastModified(now) + .withETag("asdf"); + mockRequest.addHeader("If-None-Match", "asdf"); + mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime())); + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus()); + } + + public void testConditionalGetForLastModifiedAndETagWithBadETag() { + Date now = new Date(); + DefaultHttpHeaders headers = new DefaultHttpHeaders() + .lastModified(now) + .withETag("fdsa"); + mockRequest.addHeader("If-None-Match", "asdfds"); + mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime())); + headers.apply(mockRequest, mockResponse, new Object()); + + assertEquals(SC_OK, mockResponse.getStatus()); + } +}