Moving rest plugin into trunk

WW-2321


git-svn-id: https://svn.apache.org/repos/asf/struts/struts2/trunk@594765 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Donald J. Brown
2007-11-14 04:57:40 +00:00
parent 0f06910a73
commit 6229ad7a47
33 changed files with 2857 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
+6
View File
@@ -0,0 +1,6 @@
Apache Struts
Copyright 2006 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>2.1.1-SNAPSHOT</version>
</parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-rest-plugin</artifactId>
<version>2.1.1-SNAPSHOT</version>
<name>Struts 2 Plugin</name>
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-codebehind-plugin</artifactId>
<version>${pom.version}</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<classifier>jdk15</classifier>
<version>2.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mockobjects</groupId>
<artifactId>mockobjects-core</artifactId>
<version>0.09</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-mock</artifactId>
<version>1.2.8</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>1.2.8</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>2.1.1-SNAPSHOT</version>
</parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>2.1.1-SNAPSHOT</version>
<name>Struts 2 Rest Showcase Example</name>
<description>Struts 2 Rest Showcase Example</description>
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-rest-plugin</artifactId>
<version>2.1.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>struts2-rest-showcase</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.0.1</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,73 @@
package org.apache.struts2.rest.example;
public class Order {
String id;
String clientName;
int amount;
public Order() {}
public Order(String id, String clientName, int amount) {
super();
this.id = id;
this.clientName = clientName;
this.amount = amount;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + amount;
result = prime * result
+ ((clientName == null) ? 0 : clientName.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Order other = (Order) obj;
if (amount != other.amount)
return false;
if (clientName == null) {
if (other.clientName != null)
return false;
} else if (!clientName.equals(other.clientName))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
}
@@ -0,0 +1,92 @@
package org.apache.struts2.rest.example;
import java.util.Collection;
import org.apache.struts2.config.Result;
import org.apache.struts2.config.Results;
import org.apache.struts2.dispatcher.ServletActionRedirectResult;
import org.apache.struts2.rest.DefaultHttpHeaders;
import org.apache.struts2.rest.HttpHeaders;
import com.opensymphony.xwork2.ModelDriven;
import com.opensymphony.xwork2.Validateable;
import com.opensymphony.xwork2.ValidationAwareSupport;
@Results({
@Result(name="success", type=ServletActionRedirectResult.class, value="orders")
})
public class OrdersController extends ValidationAwareSupport implements ModelDriven<Object>, Validateable{
private Order model = new Order();
private String id;
private Collection<Order> list;
private OrdersService ordersService = new OrdersService();
// GET /orders/1
public HttpHeaders show() {
return new DefaultHttpHeaders("show");
}
// GET /orders
public HttpHeaders index() {
list = ordersService.getAll();
return new DefaultHttpHeaders("index")
.disableCaching();
}
// GET /orders/1/edit
public String edit() {
return "edit";
}
// GET /orders/new
public String editNew() {
model = new Order();
return "editNew";
}
// GET /orders/1/deleteConfirm
public String deleteConfirm() {
return "deleteConfirm";
}
// DELETE /orders/1
public String destroy() {
ordersService.remove(id);
addActionMessage("Order removed successfully");
return "success";
}
// POST /orders
public HttpHeaders create() {
ordersService.save(model);
addActionMessage("New order created successfully");
return new DefaultHttpHeaders("success")
.setLocationId(model.getId());
}
// PUT /orders/1
public String update() {
ordersService.save(model);
addActionMessage("Order updated successfully");
return "success";
}
public void validate() {
if (model.getClientName() == null || model.getClientName().length() ==0) {
addFieldError("clientName", "The client name is empty");
}
}
public void setId(String id) {
if (id != null) {
this.model = ordersService.get(id);
}
this.id = id;
}
public Object getModel() {
return (list != null ? list : model);
}
}
@@ -0,0 +1,35 @@
package org.apache.struts2.rest.example;
import java.util.*;
public class OrdersService {
private static Map<String,Order> orders = new HashMap<String,Order>();
private static int nextId = 6;
static {
orders.put("3", new Order("3", "Bob", 33));
orders.put("4", new Order("4", "Sarah", 44));
orders.put("5", new Order("5", "Jim", 66));
}
public Order get(String id) {
return orders.get(id);
}
public List<Order> getAll() {
return new ArrayList<Order>(orders.values());
}
public void save(Order order) {
if (order.getId() == null) {
order.setId(String.valueOf(nextId++));
}
orders.put(order.getId(), order);
}
public void remove(String id) {
orders.remove(id);
}
}
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="starter" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Struts 2 Rest Example</display-name>
<!-- Filters -->
<!-- START SNIPPET: filter -->
<filter>
<filter-name>action2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
<init-param>
<param-name>actionPackages</param-name>
<param-value>org.apache.struts2.rest.example</param-value>
</init-param>
<init-param>
<param-name>devMode</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!-- END SNIPPET: filter -->
<filter-mapping>
<filter-name>action2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Welcome file lists -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
@@ -0,0 +1,2 @@
<% response.sendRedirect("orders"); %>
@@ -0,0 +1,23 @@
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Order ${id}</title>
</head>
<body>
<form action="../${id}?_method=DELETE" method="post">
<p>
Are you sure you want to delete order ${id}?
</p>
<div>
<input type="submit" value="Delete" />
<input type="button" value="Cancel" onclick="window.location.href = '../../orders'" />
</div>
</form>
<br />
<a href="../../orders">Back to Orders</a>
</body>
</html>
@@ -0,0 +1,27 @@
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%@taglib prefix="s" uri="/struts-tags" %>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Order <s:property value="id" /></title>
</head>
<body>
<s:form method="post" action="%{#request.contextPath}/orders/%{id}">
<s:hidden name="_method" value="put" />
<table>
<s:textfield name="id" label="ID" disabled="true"/>
<s:textfield name="clientName" label="Client"/>
<s:textfield name="amount" label="Amount" />
<tr>
<td colspan="2">
<s:submit />
</td>
</table>
</s:form>
<a href="<%=request.getContextPath() %>/orders">Back to Orders</a>
</body>
</html>
@@ -0,0 +1,25 @@
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%@taglib prefix="s" uri="/struts-tags" %>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>New Order</title>
</head>
<body>
<s:form method="post" action="%{#request.contextPath}/orders">
<table>
<s:textfield name="clientName" label="Client"/>
<s:textfield name="amount" label="Amount" />
<tr>
<td colspan="2">
<s:submit />
</td>
</table>
</s:form>
<a href="<%=request.getContextPath() %>/orders">Back to Orders</a>
</body>
</html>
@@ -0,0 +1,34 @@
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%@taglib prefix="s" uri="/struts-tags" %>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Orders</title>
</head>
<body>
<s:actionmessage />
<table>
<tr>
<th>ID</th>
<th>Client</th>
<th>Amount</th>
<th>Actions</th>
</tr>
<s:iterator value="model">
<tr>
<td>${id}</td>
<td>${clientName}</td>
<td>${amount}</td>
<td><a href="orders/${id}">View</a> |
<a href="orders/${id}/edit">Edit</a> |
<a href="orders/${id}/deleteConfirm">Delete</a></td>
</tr>
</s:iterator>
</table>
<a href="orders/new">Create a new order</a>
</body>
</html>
@@ -0,0 +1,27 @@
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Order ${id}</title>
</head>
<body>
<table>
<tr>
<th>ID</th>
<td>${id}</td>
</tr>
<tr>
<th>Client</th>
<td>${clientName}</td>
</tr>
<tr>
<th>Amount</th>
<td>${amount}</td>
</tr>
</table>
<a href="../orders">Back to Orders</a>
</body>
</html>
@@ -0,0 +1,166 @@
/*
* $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 com.opensymphony.xwork2.ModelDriven;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.rest.handler.ContentTypeHandler;
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;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Manages {@link ContentTypeHandler} instances and uses them to
* process results
*/
public class ContentTypeHandlerManager {
Map<String,ContentTypeHandler> handlers = new HashMap<String,ContentTypeHandler>();
String defaultExtension;
public static final String STRUTS_REST_HANDLER_OVERRIDE_PREFIX = "struts.rest.handlerOverride.";
@Inject("struts.rest.defaultExtension")
public void setDefaultExtension(String name) {
this.defaultExtension = name;
}
@Inject
public void setContainer(Container container) {
Set<String> names = container.getInstanceNames(ContentTypeHandler.class);
for (String name : names) {
ContentTypeHandler handler = container.getInstance(ContentTypeHandler.class, name);
// Check for overriding handlers for the current extension
String overrideName = container.getInstance(String.class, STRUTS_REST_HANDLER_OVERRIDE_PREFIX +handler.getExtension());
if (overrideName != null) {
if (!handlers.containsKey(handler.getExtension())) {
handler = container.getInstance(ContentTypeHandler.class, overrideName);
} else {
// overriding handler has already been registered
continue;
}
}
this.handlers.put(handler.getExtension(), handler);
}
}
/**
* Gets the handler for the request by looking at the extension
* @param req The request
* @return The appropriate handler
*/
public ContentTypeHandler getHandlerForRequest(HttpServletRequest req) {
String extension = findExtension(req.getRequestURI());
if (extension == null) {
extension = defaultExtension;
}
return handlers.get(extension);
}
/**
* Handles the result using handlers to generate content type-specific content
*
* @param actionConfig The action config for the current request
* @param methodResult The object returned from the action method
* @param target The object to return, usually the action object
* @return The new result code to process
* @throws IOException If unable to write to the response
*/
public String handleResult(ActionConfig actionConfig, Object methodResult, Object target)
throws IOException {
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 = getHandlerForRequest(req);
if (handler != null) {
String extCode = resultCode+"-"+handler.getExtension();
if (actionConfig.getResults().get(extCode) != null) {
resultCode = extCode;
} else {
StringWriter writer = new StringWriter();
resultCode = handler.fromObject(target, resultCode, writer);
String text = writer.toString();
if (text.length() > 0) {
byte[] data = text.getBytes("UTF-8");
res.setContentLength(data.length);
res.setContentType(handler.getContentType());
res.getOutputStream().write(data);
res.getOutputStream().close();
}
}
}
return resultCode;
}
/**
* Finds the extension in the url
*
* @param url The url
* @return The extension
*/
protected String findExtension(String url) {
int dotPos = url.lastIndexOf('.');
int slashPos = url.lastIndexOf('/');
if (dotPos > slashPos && dotPos > -1) {
return url.substring(dotPos+1);
}
return null;
}
}
@@ -0,0 +1,65 @@
/*
* $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 javax.servlet.http.HttpServletRequest;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.rest.handler.ContentTypeHandler;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ModelDriven;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.Interceptor;
/**
* Uses the content handler to apply the request body to the action
*/
public class ContentTypeInterceptor implements Interceptor {
private static final long serialVersionUID = 1L;
ContentTypeHandlerManager selector;
@Inject
public void setContentTypeHandlerSelector(ContentTypeHandlerManager sel) {
this.selector = sel;
}
public void destroy() {}
public void init() {}
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
ContentTypeHandler handler = selector.getHandlerForRequest(request);
Object target = invocation.getAction();
if (target instanceof ModelDriven) {
target = ((ModelDriven)target).getModel();
}
if (request.getContentLength() > 0) {
handler.toObject(request.getReader(), target);
}
return invocation.invoke();
}
}
@@ -0,0 +1,47 @@
/*
* $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 org.apache.struts2.config.ClasspathPackageProvider;
import com.opensymphony.xwork2.util.ResolverUtil.ClassTest;
/**
* Checks for actions ending in Controller indicating a Rest controller
*/
public class ControllerClasspathPackageProvider extends ClasspathPackageProvider {
@Override
protected ClassTest createActionClassTest() {
return new ClassTest() {
// Match Action implementations and classes ending with "Controller"
public boolean matches(Class type) {
return (type.getSimpleName().endsWith("Controller"));
}
};
}
@Override
protected String getClassSuffix() {
return "Controller";
}
}
@@ -0,0 +1,156 @@
/*
* $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 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
*/
public class DefaultHttpHeaders implements HttpHeaders {
String resultCode;
int status = SC_OK;
Object etag;
Object locationId;
String location;
boolean disableCaching;
boolean noETag = false;
Date lastModified;
public DefaultHttpHeaders() {}
public DefaultHttpHeaders(String result) {
resultCode = result;
}
public DefaultHttpHeaders renderResult(String code) {
this.resultCode = code;
return this;
}
public DefaultHttpHeaders withStatus(int code) {
this.status = code;
return this;
}
public DefaultHttpHeaders withETag(Object etag) {
this.etag = etag;
return this;
}
public DefaultHttpHeaders withNoETag() {
this.noETag = true;
return this;
}
public DefaultHttpHeaders setLocationId(Object id) {
this.locationId = id;
return this;
}
public DefaultHttpHeaders setLocation(String loc) {
this.location = loc;
return this;
}
public DefaultHttpHeaders lastModified(Date date) {
this.lastModified = date;
return this;
}
public DefaultHttpHeaders disableCaching() {
this.disableCaching = true;
return this;
}
/* (non-Javadoc)
* @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) {
if (disableCaching) {
response.setHeader("Cache-Control", "no-cache");
}
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);
} else {
url += "/"+locationId;
}
response.setHeader("Location", url);
} else if (location != null) {
response.setHeader("Location", location);
}
if (status == SC_OK && !disableCaching) {
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;
}
public int getStatus() {
return status;
}
}
@@ -0,0 +1,45 @@
/*
* $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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Type-safe rest-related informtion to apply to a response
*/
public interface HttpHeaders {
/**
* Applies the configured information to the response
* @param request The request
* @param response The response
* @param target The target object, usually the action
* @return The result code to process
*/
String apply(HttpServletRequest request,
HttpServletResponse response, Object target);
/**
* The HTTP status code
*/
int getStatus();
}
@@ -0,0 +1,164 @@
/*
* $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 com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionEventListener;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.DefaultActionInvocation;
import com.opensymphony.xwork2.ModelDriven;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.UnknownHandler;
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.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
import com.opensymphony.xwork2.util.logging.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.rest.handler.ContentTypeHandler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Extends the usual {@link ActionInvocation} to add support for processing the object returned
* from the action execution. This allows us to support methods that return {@link HttpHeaders}
* as well as apply content type-specific operations to the result.
*/
public class RestActionInvocation extends DefaultActionInvocation {
private static final long serialVersionUID = 3485701178946428716L;
private static final Logger LOG = LoggerFactory.getLogger(RestActionInvocation.class);
private ContentTypeHandlerManager handlerSelector;
protected RestActionInvocation(Map extraContext, boolean pushAction) throws Exception {
super(extraContext, pushAction);
}
@Inject
public void setMimeTypeHandlerSelector(ContentTypeHandlerManager sel) {
this.handlerSelector = sel;
}
protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
String methodName = proxy.getMethod();
if (LOG.isDebugEnabled()) {
LOG.debug("Executing action method = " + actionConfig.getMethodName());
}
String timerKey = "invokeAction: "+proxy.getActionName();
try {
UtilTimerStack.push(timerKey);
boolean methodCalled = false;
Object methodResult = null;
Method method = null;
try {
method = getAction().getClass().getMethod(methodName, new Class[0]);
} catch (NoSuchMethodException e) {
// hmm -- OK, try doXxx instead
try {
String altMethodName = "do" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
method = getAction().getClass().getMethod(altMethodName, new Class[0]);
} catch (NoSuchMethodException e1) {
// well, give the unknown handler a shot
if (unknownHandler != null) {
try {
methodResult = unknownHandler.handleUnknownActionMethod(action, methodName);
methodCalled = true;
} catch (NoSuchMethodException e2) {
// throw the original one
throw e;
}
} else {
throw e;
}
}
}
if (!methodCalled) {
methodResult = method.invoke(action, new Object[0]);
}
return processResult(actionConfig, methodResult);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("The " + methodName + "() is not defined in action " + getAction().getClass() + "");
} catch (InvocationTargetException e) {
// We try to return the source exception.
Throwable t = e.getTargetException();
if (actionEventListener != null) {
String result = actionEventListener.handleException(t, getStack());
if (result != null) {
return result;
}
}
if (t instanceof Exception) {
throw(Exception) t;
} else {
throw e;
}
} finally {
UtilTimerStack.pop(timerKey);
}
}
protected String processResult(ActionConfig actionConfig, Object methodResult) throws IOException {
if (methodResult instanceof Result) {
this.explicitResult = (Result) methodResult;
return null;
} else if (methodResult != null) {
resultCode = handlerSelector.handleResult(actionConfig, methodResult, action);
}
return resultCode;
}
}
@@ -0,0 +1,329 @@
/*
* $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 com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationManager;
import com.opensymphony.xwork2.config.entities.PackageConfig;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Iterator;
/**
* <!-- START SNIPPET: description -->
*
* This Restful action mapper enforces Ruby-On-Rails Rest-style mappings. If the method
* is not specified (via '!' or 'method:' prefix), the method is "guessed" at using
* ReST-style conventions that examine the URL and the HTTP method. Special care has
* been given to ensure this mapper works correctly with the codebehind plugin so that
* XML configuration is unnecessary.
*
* <p>
* This mapper supports the following parameters:
* </p>
* <ul>
* <li><code>struts.mapper.idParameterName</code> - If set, this value will be the name
* of the parameter under which the id is stored. The id will then be removed
* from the action name. Whether or not the method is specified, the mapper will
* try to truncate the identifier from the url and store it as a parameter.
* </li>
* <li><code>struts.mapper.indexMethodName</code> - The method name to call for a GET
* request with no id parameter. Defaults to 'index'.
* </li>
* <li><code>struts.mapper.getMethodName</code> - The method name to call for a GET
* request with an id parameter. Defaults to 'show'.
* </li>
* <li><code>struts.mapper.postMethodName</code> - The method name to call for a POST
* request with no id parameter. Defaults to 'create'.
* </li>
* <li><code>struts.mapper.putMethodName</code> - The method name to call for a PUT
* request with an id parameter. Defaults to 'update'.
* </li>
* <li><code>struts.mapper.deleteMethodName</code> - The method name to call for a DELETE
* request with an id parameter. Defaults to 'destroy'.
* </li>
* <li><code>struts.mapper.editMethodName</code> - The method name to call for a GET
* request with an id parameter and the 'edit' view specified. Defaults to 'edit'.
* </li>
* <li><code>struts.mapper.newMethodName</code> - The method name to call for a GET
* request with no id parameter and the 'new' view specified. Defaults to 'editNew'.
* </li>
* </ul>
* <p>
* The following URL's will invoke its methods:
* </p>
* <ul>
* <li><code>GET: /movies => method="index"</code></li>
* <li><code>GET: /movies/Thrillers => method="show", id="Thrillers"</code></li>
* <li><code>GET: /movies/Thrillers;edit => method="edit", id="Thrillers"</code></li>
* <li><code>GET: /movies/Thrillers/edit => method="edit", id="Thrillers"</code></li>
* <li><code>GET: /movies/new => method="editNew"</code></li>
* <li><code>POST: /movies => method="create"</code></li>
* <li><code>PUT: /movies/Thrillers => method="update", id="Thrillers"</code></li>
* <li><code>DELETE: /movies/Thrillers => method="destroy", id="Thrillers"</code></li>
* </ul>
* <p>
* To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML,
* the HTTP parameter "_method" will be used.
* </p>
* <!-- END SNIPPET: description -->
*/
public class RestActionMapper extends DefaultActionMapper {
protected static final Logger LOG = LoggerFactory.getLogger(RestActionMapper.class);
public static final String HTTP_METHOD_PARAM = "_method";
private String idParameterName = "id";
private String indexMethodName = "index";
private String getMethodName = "show";
private String postMethodName = "create";
private String editMethodName = "edit";
private String newMethodName = "editNew";
private String deleteMethodName = "destroy";
private String putMethodName = "update";
public RestActionMapper() {
}
public String getIdParameterName() {
return idParameterName;
}
@Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME)
public void setIdParameterName(String idParameterName) {
this.idParameterName = idParameterName;
}
@Inject(required=false,value="struts.mapper.indexMethodName")
public void setIndexMethodName(String indexMethodName) {
this.indexMethodName = indexMethodName;
}
@Inject(required=false,value="struts.mapper.getMethodName")
public void setGetMethodName(String getMethodName) {
this.getMethodName = getMethodName;
}
@Inject(required=false,value="struts.mapper.postMethodName")
public void setPostMethodName(String postMethodName) {
this.postMethodName = postMethodName;
}
@Inject(required=false,value="struts.mapper.editMethodName")
public void setEditMethodName(String editMethodName) {
this.editMethodName = editMethodName;
}
@Inject(required=false,value="struts.mapper.newMethodName")
public void setNewMethodName(String newMethodName) {
this.newMethodName = newMethodName;
}
@Inject(required=false,value="struts.mapper.deleteMethodName")
public void setDeleteMethodName(String deleteMethodName) {
this.deleteMethodName = deleteMethodName;
}
@Inject(required=false,value="struts.mapper.putMethodName")
public void setPutMethodName(String putMethodName) {
this.putMethodName = putMethodName;
}
public ActionMapping getMapping(HttpServletRequest request,
ConfigurationManager configManager) {
ActionMapping mapping = new ActionMapping();
String uri = getUri(request);
uri = dropExtension(uri, mapping);
if (uri == null) {
return null;
}
parseNameAndNamespace(uri, mapping, configManager);
handleSpecialParameters(request, mapping);
if (mapping.getName() == null) {
return null;
}
// handle "name!method" convention.
String name = mapping.getName();
int exclamation = name.lastIndexOf("!");
if (exclamation != -1) {
mapping.setName(name.substring(0, exclamation));
mapping.setMethod(name.substring(exclamation + 1));
}
String fullName = mapping.getName();
// Only try something if the action name is specified
if (fullName != null && fullName.length() > 0) {
int lastSlashPos = fullName.lastIndexOf('/');
String id = null;
if (lastSlashPos > -1) {
// fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
if (prevSlashPos > -1) {
mapping.setMethod(fullName.substring(lastSlashPos+1));
fullName = fullName.substring(0, lastSlashPos);
lastSlashPos = prevSlashPos;
}
id = fullName.substring(lastSlashPos+1);
}
// 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) {
// Index e.g. foo
if (isGet(request)) {
mapping.setMethod(indexMethodName);
// Creating a new entry on POST e.g. foo
} else if (isPost(request)) {
mapping.setMethod(postMethodName);
}
// Handle uris with an id at the end
} else if (id != null) {
// Viewing the form to edit an item e.g. foo/1;edit
if (isGet(request) && id.endsWith(";edit")) {
id = id.substring(0, id.length() - ";edit".length());
mapping.setMethod(editMethodName);
// Viewing the form to create a new item e.g. foo/new
} else if (isGet(request) && "new".equals(id)) {
mapping.setMethod(newMethodName);
// Removing an item e.g. foo/1
} else if (isDelete(request)) {
mapping.setMethod(deleteMethodName);
// Viewing an item e.g. foo/1
} else if (isGet(request)) {
mapping.setMethod(getMethodName);
// Updating an item e.g. foo/1
} else if (isPut(request)) {
mapping.setMethod(putMethodName);
}
}
}
// cut off the id parameter, even if a method is specified
if (id != null) {
if (!"new".equals(id)) {
if (mapping.getParams() == null) {
mapping.setParams(new HashMap());
}
mapping.getParams().put(idParameterName, new String[]{id});
}
fullName = fullName.substring(0, lastSlashPos);
}
mapping.setName(fullName);
}
return mapping;
}
/**
* Parses the name and namespace from the uri. Uses the configured package
* namespaces to determine the name and id parameter, to be parsed later.
*
* @param uri
* The uri
* @param mapping
* The action mapping to populate
*/
protected void parseNameAndNamespace(String uri, ActionMapping mapping,
ConfigurationManager configManager) {
String namespace, name;
int lastSlash = uri.lastIndexOf("/");
if (lastSlash == -1) {
namespace = "";
name = uri;
} else if (lastSlash == 0) {
// ww-1046, assume it is the root namespace, it will fallback to
// default
// namespace anyway if not found in root namespace.
namespace = "/";
name = uri.substring(lastSlash + 1);
} else {
// Try to find the namespace in those defined, defaulting to ""
Configuration config = configManager.getConfiguration();
String prefix = uri.substring(0, lastSlash);
namespace = "";
// Find the longest matching namespace, defaulting to the default
for (Iterator i = config.getPackageConfigs().values().iterator(); i
.hasNext();) {
String ns = ((PackageConfig) i.next()).getNamespace();
if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) {
if (ns.length() > namespace.length()) {
namespace = ns;
}
}
}
name = uri.substring(namespace.length() + 1);
}
mapping.setNamespace(namespace);
mapping.setName(name);
}
protected boolean isGet(HttpServletRequest request) {
return "get".equalsIgnoreCase(request.getMethod());
}
protected boolean isPost(HttpServletRequest request) {
return "post".equalsIgnoreCase(request.getMethod());
}
protected boolean isPut(HttpServletRequest request) {
if ("put".equalsIgnoreCase(request.getMethod())) {
return true;
} else {
return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
}
}
protected boolean isDelete(HttpServletRequest request) {
if ("delete".equalsIgnoreCase(request.getMethod())) {
return true;
} else {
return "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
}
}
}
@@ -0,0 +1,44 @@
/*
* $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 java.util.Map;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.DefaultActionInvocation;
import com.opensymphony.xwork2.DefaultActionProxyFactory;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
/**
* Factory that creates the {@link RestActionInvocation}
*/
public class RestActionProxyFactory extends DefaultActionProxyFactory {
public ActionProxy createActionProxy(String namespace, String actionName, Map extraContext, boolean executeResult, boolean cleanupContext) throws Exception {
ActionInvocation inv = new RestActionInvocation(extraContext, true);
container.inject(inv);
return createActionProxy(inv, namespace, actionName, extraContext, executeResult, cleanupContext);
}
}
@@ -0,0 +1,200 @@
/*
* $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 java.util.HashMap;
import java.util.Map;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ValidationAware;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import static javax.servlet.http.HttpServletResponse.*;
/**
* <!-- START SNIPPET: description -->
*
* An interceptor that makes sure there are not validation errors before allowing the interceptor chain to continue.
* <b>This interceptor does not perform any validation</b>.
*
* <p>Copied from the {@link DefaultWorkflowInterceptor}, this interceptor adds support for error handling of Restful
* operations. For example, if an validation error is discovered, a map of errors is created and processed to be
* returned, using the appropriate content handler for rendering the body.</p>
*
* <p/>This interceptor does nothing if the name of the method being invoked is specified in the <b>excludeMethods</b>
* parameter. <b>excludeMethods</b> accepts a comma-delimited list of method names. For example, requests to
* <b>foo!input.action</b> and <b>foo!back.action</b> will be skipped by this interceptor if you set the
* <b>excludeMethods</b> parameter to "input, back".
*
* <b>Note:</b> As this method extends off MethodFilterInterceptor, it is capable of
* deciding if it is applicable only to selective methods in the action class. This is done by adding param tags
* for the interceptor element, naming either a list of excluded method names and/or a list of included method
* names, whereby includeMethods overrides excludedMethods. A single * sign is interpreted as wildcard matching
* all methods for both parameters.
* See {@link MethodFilterInterceptor} for more info.
*
* <!-- END SNIPPET: description -->
*
* <p/> <u>Interceptor parameters:</u>
*
* <!-- START SNIPPET: parameters -->
*
* <ul>
*
* <li>inputResultName - Default to "input". Determine the result name to be returned when
* an action / field error is found.</li>
*
* </ul>
*
* <!-- END SNIPPET: parameters -->
*
* <p/> <u>Extending the interceptor:</u>
*
* <p/>
*
* <!-- START SNIPPET: extending -->
*
* There are no known extension points for this interceptor.
*
* <!-- END SNIPPET: extending -->
*
* <p/> <u>Example code:</u>
*
* <pre>
* <!-- START SNIPPET: example -->
*
* &lt;action name="someAction" class="com.examples.SomeAction"&gt;
* &lt;interceptor-ref name="params"/&gt;
* &lt;interceptor-ref name="validation"/&gt;
* &lt;interceptor-ref name="workflow"/&gt;
* &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
* &lt;/action&gt;
*
* &lt;-- In this case myMethod as well as mySecondMethod of the action class
* will not pass through the workflow process --&gt;
* &lt;action name="someAction" class="com.examples.SomeAction"&gt;
* &lt;interceptor-ref name="params"/&gt;
* &lt;interceptor-ref name="validation"/&gt;
* &lt;interceptor-ref name="workflow"&gt;
* &lt;param name="excludeMethods"&gt;myMethod,mySecondMethod&lt;/param&gt;
* &lt;/interceptor-ref name="workflow"&gt;
* &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
* &lt;/action&gt;
*
* &lt;-- In this case, the result named "error" will be used when
* an action / field error is found --&gt;
* &lt;-- The Interceptor will only be applied for myWorkflowMethod method of action
* classes, since this is the only included method while any others are excluded --&gt;
* &lt;action name="someAction" class="com.examples.SomeAction"&gt;
* &lt;interceptor-ref name="params"/&gt;
* &lt;interceptor-ref name="validation"/&gt;
* &lt;interceptor-ref name="workflow"&gt;
* &lt;param name="inputResultName"&gt;error&lt;/param&gt;
* &lt;param name="excludeMethods"&gt;*&lt;/param&gt;
* &lt;param name="includeMethods"&gt;myWorkflowMethod&lt;/param&gt;
* &lt;/interceptor-ref&gt;
* &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
* &lt;/action&gt;
*
* <!-- END SNIPPET: example -->
* </pre>
*
* @author Jason Carreira
* @author Rainer Hermanns
* @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
* @author Philip Luppens
* @author tm_jee
*/
public class RestWorkflowInterceptor extends MethodFilterInterceptor {
private static final long serialVersionUID = 7563014655616490865L;
private static final Logger LOG = LoggerFactory.getLogger(RestWorkflowInterceptor.class);
private String inputResultName = Action.INPUT;
private ContentTypeHandlerManager manager;
@Inject
public void setContentTypeHandlerManager(ContentTypeHandlerManager mgr) {
this.manager = mgr;
}
/**
* Set the <code>inputResultName</code> (result name to be returned when
* a action / field error is found registered). Default to {@link Action#INPUT}
*
* @param inputResultName what result name to use when there was validation error(s).
*/
public void setInputResultName(String inputResultName) {
this.inputResultName = inputResultName;
}
/**
* Intercept {@link ActionInvocation} and processes the errors using the {@link org.apache.struts2.rest.handler.ContentTypeHandler}
* appropriate for the request.
*
* @return String result name
*/
protected String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof ValidationAware) {
ValidationAware validationAwareAction = (ValidationAware) action;
if (validationAwareAction.hasErrors()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Errors on action "+validationAwareAction+", returning result name 'input'");
}
ActionMapping mapping = (ActionMapping) ActionContext.getContext().get(ServletActionContext.ACTION_MAPPING);
String method = inputResultName;
if ("create".equals(mapping.getMethod())) {
method = "editNew";
} else if ("update".equals(mapping.getMethod())) {
method = "edit";
}
HttpHeaders info = new DefaultHttpHeaders()
.disableCaching()
.renderResult(method)
.withStatus(SC_BAD_REQUEST);
Map errors = new HashMap();
errors.put("actionErrors", validationAwareAction.getActionErrors());
errors.put("fieldErrors", validationAwareAction.getFieldErrors());
return manager.handleResult(invocation.getProxy().getConfig(), info, errors);
}
}
return invocation.invoke();
}
}
@@ -0,0 +1,63 @@
/*
* $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.handler;
import java.io.*;
import com.opensymphony.xwork2.ActionInvocation;
/**
* Handles transferring content to and from objects for a specific content type
*/
public interface ContentTypeHandler {
/**
* Populates an object using data from the input stream
* @param in The input stream, usually the body of the request
* @param target The target, usually the action class
*/
void toObject(Reader in, Object target) throws IOException;
/**
* Writes content to the stream
*
* @param obj The object to write to the stream, usually the Action class
* @param resultCode The original result code
* @param stream The output stream, usually the response
* @return The new result code
* @throws IOException If unable to write to the output stream
*/
String fromObject(Object obj, String resultCode, Writer stream) throws IOException;
/**
* Gets the content type for this handler
*
* @return The mime type
*/
String getContentType();
/**
* Gets the extension this handler supports
*
* @return The extension
*/
String getExtension();
}
@@ -0,0 +1,47 @@
/*
* $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.handler;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
/**
* Handles HTML content, usually just a simple passthrough to the framework
*/
public class HtmlHandler implements ContentTypeHandler {
public String fromObject(Object obj, String resultCode, Writer out) throws IOException {
return resultCode;
}
public void toObject(Reader in, Object target) {
}
public String getExtension() {
return "xhtml";
}
public String getContentType() {
return "application/xhtml+xml";
}
}
@@ -0,0 +1,82 @@
/*
* $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.handler;
import java.io.*;
import java.util.Collection;
import net.sf.json.JSONObject;
import net.sf.json.JsonConfig;
import net.sf.json.JSONArray;
/**
* Handles JSON content using json-lib
*/
public class JsonLibHandler implements ContentTypeHandler {
public void toObject(Reader in, Object target) throws IOException {
StringBuilder sb = new StringBuilder();
char[] buffer = new char[1024];
int len = 0;
while ((len = in.read(buffer)) > 0) {
sb.append(buffer, 0, len);
}
if (target != null && sb.length() > 0 && sb.charAt(0) == '[') {
JSONArray jsonArray = JSONArray.fromObject(sb.toString());
if (target.getClass().isArray()) {
JSONArray.toArray(jsonArray, target, new JsonConfig());
} else {
JSONArray.toList(jsonArray, target, new JsonConfig());
}
} else {
JSONObject jsonObject = JSONObject.fromObject(sb.toString());
JSONObject.toBean(jsonObject, target, new JsonConfig());
}
}
public String fromObject(Object obj, String resultCode, Writer stream) throws IOException {
if (obj != null) {
if (isArray(obj)) {
JSONArray jsonArray = JSONArray.fromObject(obj);
stream.write(jsonArray.toString());
} else {
JSONObject jsonObject = JSONObject.fromObject(obj);
stream.write(jsonObject.toString());
}
}
return null;
}
private boolean isArray(Object obj) {
return obj instanceof Collection || obj.getClass().isArray();
}
public String getContentType() {
return "text/javascript";
}
public String getExtension() {
return "json";
}
}
@@ -0,0 +1,58 @@
/*
* $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.handler;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import com.thoughtworks.xstream.XStream;
/**
* Handles XML content
*/
public class XStreamHandler implements ContentTypeHandler {
public String fromObject(Object obj, String resultCode, Writer out) throws IOException {
if (obj != null) {
XStream xstream = createXStream();
xstream.toXML(obj, out);
}
return null;
}
public void toObject(Reader in, Object target) {
XStream xstream = createXStream();
xstream.fromXML(in, target);
}
protected XStream createXStream() {
return new XStream();
}
public String getContentType() {
return "application/xml";
}
public String getExtension() {
return "xml";
}
}
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<bean type="com.opensymphony.xwork2.ActionProxyFactory" name="rest" class="org.apache.struts2.rest.RestActionProxyFactory" />
<bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="rest" class="org.apache.struts2.rest.RestActionMapper" />
<bean type="com.opensymphony.xwork2.config.PackageProvider" name="rest" class="org.apache.struts2.rest.ControllerClasspathPackageProvider" />
<bean class="org.apache.struts2.rest.ContentTypeHandlerManager" />
<bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="xml" class="org.apache.struts2.rest.handler.XStreamHandler" />
<bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="json" class="org.apache.struts2.rest.handler.JsonLibHandler" />
<bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="html" class="org.apache.struts2.rest.handler.HtmlHandler" />
<constant name="struts.actionProxyFactory" value="rest" />
<constant name="struts.rest.defaultExtension" value="xhtml" />
<constant name="struts.mapper.class" value="rest" />
<constant name="struts.mapper.idParameterName" value="id" />
<constant name="struts.action.extension" value="xhtml,,xml,json" />
<constant name="struts.configuration.classpath.defaultParentPackage" value="rest-default" />
<package name="rest-default" extends="struts-default">
<result-types>
<result-type name="redirect" class="org.apache.struts2.dispatcher.ServletRedirectResult">
<param name="statusCode">303</param>
</result-type>
<result-type name="redirectAction" class="org.apache.struts2.dispatcher.ServletActionRedirectResult">
<param name="statusCode">303</param>
</result-type>
</result-types>
<interceptors>
<interceptor name="rest" class="org.apache.struts2.rest.ContentTypeInterceptor"/>
<interceptor name="restWorkflow" class="org.apache.struts2.rest.RestWorkflowInterceptor"/>
<interceptor name="messages" class="org.apache.struts2.interceptor.MessageStoreInterceptor" />
<!-- A complete stack with all the common interceptors in place.
Generally, this stack should be the one you use, though it
may do more than you need. Also, the ordering can be
switched around (ex: if you wish to have your servlet-related
objects applied before prepare() is called, you'd need to move
servlet-config interceptor up.
This stack also excludes from the normal validation and workflow
the method names input, back, and cancel. These typically are
associated with requests that should not be validated.
-->
<interceptor-stack name="restDefaultStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="alias"/>
<interceptor-ref name="servletConfig"/>
<interceptor-ref name="messages">
<param name="operationMode">AUTOMATIC</param>
</interceptor-ref>
<interceptor-ref name="prepare"/>
<interceptor-ref name="i18n"/>
<interceptor-ref name="chain"/>
<interceptor-ref name="debugging"/>
<interceptor-ref name="profiling"/>
<interceptor-ref name="actionMappingParams"/>
<interceptor-ref name="scopedModelDriven"/>
<interceptor-ref name="modelDriven">
<param name="refreshModelBeforeResult">true</param>
</interceptor-ref>
<interceptor-ref name="fileUpload"/>
<interceptor-ref name="checkbox"/>
<interceptor-ref name="staticParams"/>
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*</param>
</interceptor-ref>
<interceptor-ref name="rest" />
<interceptor-ref name="conversionError"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse,index,show,edit,editNew</param>
</interceptor-ref>
<interceptor-ref name="restWorkflow">
<param name="excludeMethods">input,back,cancel,browse,index,show,edit,editNew</param>
</interceptor-ref>
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="restDefaultStack"/>
</package>
</struts>
@@ -0,0 +1,128 @@
/*
* $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 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.Reader;
import java.io.Writer;
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(Reader in, Object target) {}
public String fromObject(Object obj, String resultCode, Writer stream) throws IOException {
stream.write(obj.toString());
return resultCode;
}
public String getContentType() { return "foo"; }
public String getExtension() { return "foo"; }
};
mgr.handlers.put("xml", handler);
mgr.defaultExtension = "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);
mockHandlerXml.matchAndReturn("getExtension", "xml");
mockHandlerXml.matchAndReturn("toString", "xml");
Mock mockHandlerJson = new Mock(ContentTypeHandler.class);
mockHandlerJson.matchAndReturn("getExtension", "json");
mockHandlerJson.matchAndReturn("toString", "json");
Mock mockHandlerXmlOverride = new Mock(ContentTypeHandler.class);
mockHandlerXmlOverride.matchAndReturn("getExtension", "xml");
mockHandlerXmlOverride.matchAndReturn("toString", "xmlOverride");
Mock mockContainer = new Mock(Container.class);
mockContainer.matchAndReturn("getInstance", C.args(C.eq(ContentTypeHandler.class), C.eq("xmlOverride")), mockHandlerXmlOverride.proxy());
mockContainer.matchAndReturn("getInstance", C.args(C.eq(ContentTypeHandler.class), C.eq("xml")), mockHandlerXml.proxy());
mockContainer.matchAndReturn("getInstance", C.args(C.eq(ContentTypeHandler.class), C.eq("json")), mockHandlerJson.proxy());
mockContainer.expectAndReturn("getInstanceNames", C.args(C.eq(ContentTypeHandler.class)), new HashSet(Arrays.asList("xml", "xmlOverride", "json")));
mockContainer.matchAndReturn("getInstance", C.args(C.eq(String.class),
C.eq(ContentTypeHandlerManager.STRUTS_REST_HANDLER_OVERRIDE_PREFIX+"xml")), "xmlOverride");
mockContainer.expectAndReturn("getInstance", C.args(C.eq(String.class),
C.eq(ContentTypeHandlerManager.STRUTS_REST_HANDLER_OVERRIDE_PREFIX+"json")), null);
ContentTypeHandlerManager mgr = new ContentTypeHandlerManager();
mgr.setContainer((Container) mockContainer.proxy());
Map<String,ContentTypeHandler> handlers = mgr.handlers;
assertNotNull(handlers);
assertEquals(2, handlers.size());
assertEquals(mockHandlerXmlOverride.proxy(), handlers.get("xml"));
assertEquals(mockHandlerJson.proxy(), handlers.get("json"));
}
}
@@ -0,0 +1,181 @@
/*
* $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;
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 testConditionalGetForLastModifiedAndETagButNoCache() {
Date now = new Date();
DefaultHttpHeaders headers = new DefaultHttpHeaders()
.lastModified(now)
.withETag("asdf")
.disableCaching();
mockRequest.addHeader("If-None-Match", "asdf");
mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime()));
headers.apply(mockRequest, mockResponse, new Object());
assertEquals(SC_OK, 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());
}
}
@@ -0,0 +1,161 @@
package org.apache.struts2.rest;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationManager;
import com.opensymphony.xwork2.config.entities.PackageConfig;
import com.opensymphony.xwork2.config.impl.DefaultConfiguration;
import junit.framework.TestCase;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.springframework.mock.web.MockHttpServletRequest;
public class RestActionMapperTest extends TestCase {
private RestActionMapper mapper;
private ConfigurationManager configManager;
private Configuration config;
private MockHttpServletRequest req;
protected void setUp() throws Exception {
super.setUp();
req = new MockHttpServletRequest();
req.setContextPath("/myapp");
req.setMethod("GET");
mapper = new RestActionMapper();
config = new DefaultConfiguration();
PackageConfig pkg = new PackageConfig("myns", "/animals", false, null);
PackageConfig pkg2 = new PackageConfig("my", "/my", false, null);
config.addPackageConfig("mvns", pkg);
config.addPackageConfig("my", pkg2);
configManager = new ConfigurationManager() {
public Configuration getConfiguration() {
return config;
}
};
}
public void testGetMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog");
req.setServletPath("/animals/dog");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("index", mapping.getMethod());
}
public void testPostMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog");
req.setServletPath("/animals/dog");
req.setMethod("POST");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("create", mapping.getMethod());
}
public void testDeleteMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog/fido");
req.setServletPath("/animals/dog/fido");
req.setMethod("DELETE");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("destroy", mapping.getMethod());
assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
}
public void testPutMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog/fido");
req.setServletPath("/animals/dog/fido");
req.setMethod("PUT");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("update", mapping.getMethod());
assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
}
public void testGetIdMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog/fido");
req.setServletPath("/animals/dog/fido");
req.setMethod("GET");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("show", mapping.getMethod());
assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
}
public void testNewMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog/new");
req.setServletPath("/animals/dog/new");
req.setMethod("GET");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("editNew", mapping.getMethod());
}
public void testEditMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog/fido/edit");
req.setServletPath("/animals/dog/fido/edit");
req.setMethod("GET");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
assertEquals("edit", mapping.getMethod());
}
public void testEditSemicolonMapping() throws Exception {
req.setRequestURI("/myapp/animals/dog/fido;edit");
req.setServletPath("/animals/dog/fido;edit");
req.setMethod("GET");
ActionMapping mapping = mapper.getMapping(req, configManager);
assertEquals("/animals", mapping.getNamespace());
assertEquals("dog", mapping.getName());
assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
assertEquals("edit", mapping.getMethod());
}
public void testParseNameAndNamespace() {
tryUri("/foo/23", "", "foo/23");
tryUri("/foo/", "", "foo/");
tryUri("foo", "", "foo");
tryUri("/", "/", "");
}
public void testParseNameAndNamespaceWithNamespaces() {
tryUri("/my/foo/23", "/my", "foo/23");
tryUri("/my/foo/", "/my", "foo/");
}
public void testParseNameAndNamespaceWithEdit() {
tryUri("/my/foo/23;edit", "/my", "foo/23;edit");
}
private void tryUri(String uri, String expectedNamespace, String expectedName) {
ActionMapping mapping = new ActionMapping();
mapper.parseNameAndNamespace(uri, mapping, configManager);
assertEquals(expectedName, mapping.getName());
assertEquals(expectedNamespace, mapping.getNamespace());
}
}
@@ -0,0 +1,91 @@
/*
* $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.handler;
import java.util.HashMap;
public class Contact {
private String name;
private boolean important;
private int age;
public Contact() {}
public Contact(String name, boolean important, int age) {
this.name = name;
this.important = important;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isImportant() {
return important;
}
public void setImportant(boolean important) {
this.important = important;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Contact contact = (Contact) o;
if (age != contact.age) return false;
if (important != contact.important) return false;
if (name != null ? !name.equals(contact.name) : contact.name != null) return false;
return true;
}
public String toString() {
HashMap map = new HashMap();
map.put("age", age);
map.put("important", important);
map.put("name", name);
return map.toString();
}
public int hashCode() {
int result;
result = (name != null ? name.hashCode() : 0);
result = 31 * result + (important ? 1 : 0);
result = 31 * result + age;
return result;
}
}
@@ -0,0 +1,62 @@
/*
* $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.handler;
import junit.framework.TestCase;
import java.io.StringWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.Arrays;
public class JsonLibHandlerTest extends TestCase {
public void testFromObject() throws IOException {
Contact contact = new Contact("bob", true, 44);
StringWriter writer = new StringWriter();
JsonLibHandler handler = new JsonLibHandler();
handler.fromObject(contact, "success", writer);
assertEquals("{\"age\":44,\"important\":true,\"name\":\"bob\"}", writer.toString());
}
public void testFromObjectArray() throws IOException {
Contact contact = new Contact("bob", true, 44);
StringWriter writer = new StringWriter();
JsonLibHandler handler = new JsonLibHandler();
handler.fromObject(Arrays.asList(contact), "success", writer);
assertEquals("[{\"age\":44,\"important\":true,\"name\":\"bob\"}]", writer.toString());
}
public void testToObject() throws IOException {
Contact contact = new Contact("bob", true, 44);
Contact target = new Contact();
StringReader reader = new StringReader("{\"age\":44,\"important\":true,\"name\":\"bob\"}");
JsonLibHandler handler = new JsonLibHandler();
handler.toObject(reader, target);
assertEquals(contact, target);
}
}