diff --git a/plugins/struts2-rest-plugin/LICENSE.txt b/plugins/struts2-rest-plugin/LICENSE.txt new file mode 100644 index 000000000..c6055ec8f --- /dev/null +++ b/plugins/struts2-rest-plugin/LICENSE.txt @@ -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. diff --git a/plugins/struts2-rest-plugin/NOTICE.txt b/plugins/struts2-rest-plugin/NOTICE.txt new file mode 100644 index 000000000..5c3c04fc8 --- /dev/null +++ b/plugins/struts2-rest-plugin/NOTICE.txt @@ -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/ diff --git a/plugins/struts2-rest-plugin/pom.xml b/plugins/struts2-rest-plugin/pom.xml new file mode 100644 index 000000000..81c0dc42f --- /dev/null +++ b/plugins/struts2-rest-plugin/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + org.apache.struts + struts2-plugins + 2.1.1-SNAPSHOT + + org.apache.struts + struts2-rest-plugin + 2.1.1-SNAPSHOT + Struts 2 Plugin + + + + org.apache.struts + struts2-codebehind-plugin + ${pom.version} + + + com.thoughtworks.xstream + xstream + 1.2.2 + + + net.sf.json-lib + json-lib + jdk15 + 2.1 + + + javax.servlet + servlet-api + 2.4 + provided + + + junit + junit + 3.8.1 + test + + + + mockobjects + mockobjects-core + 0.09 + test + + + + org.springframework + spring-mock + 1.2.8 + true + + + + org.springframework + spring-core + 1.2.8 + true + + + + + diff --git a/plugins/struts2-rest-plugin/showcase/pom.xml b/plugins/struts2-rest-plugin/showcase/pom.xml new file mode 100644 index 000000000..454064585 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + org.apache.struts + struts2-apps + 2.1.1-SNAPSHOT + + + org.apache.struts + struts2-rest-showcase + war + 2.1.1-SNAPSHOT + Struts 2 Rest Showcase Example + Struts 2 Rest Showcase Example + + + + org.apache.struts + struts2-rest-plugin + 2.1.1-SNAPSHOT + + + junit + junit + 3.8.1 + test + + + + + + + struts2-rest-showcase + + + maven-compiler-plugin + + 1.5 + 1.5 + + + + org.mortbay.jetty + maven-jetty-plugin + 6.0.1 + + 10 + + + + + diff --git a/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/Order.java b/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/Order.java new file mode 100644 index 000000000..9006c82d3 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/Order.java @@ -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; + } + + + + +} diff --git a/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/OrdersController.java b/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/OrdersController.java new file mode 100644 index 000000000..f7679155d --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/OrdersController.java @@ -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, Validateable{ + + private Order model = new Order(); + private String id; + private Collection 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); + } + +} diff --git a/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/OrdersService.java b/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/OrdersService.java new file mode 100644 index 000000000..12bc851a2 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/java/org/apache/struts2/rest/example/OrdersService.java @@ -0,0 +1,35 @@ +package org.apache.struts2.rest.example; + +import java.util.*; + +public class OrdersService { + + private static Map orders = new HashMap(); + 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 getAll() { + return new ArrayList(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); + } + +} diff --git a/plugins/struts2-rest-plugin/showcase/src/main/webapp/WEB-INF/web.xml b/plugins/struts2-rest-plugin/showcase/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..19b41e62a --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,37 @@ + + + + + Struts 2 Rest Example + + + + + + action2 + org.apache.struts2.dispatcher.FilterDispatcher + + actionPackages + org.apache.struts2.rest.example + + + devMode + true + + + + + + action2 + /* + + + + + index.jsp + + + diff --git a/plugins/struts2-rest-plugin/showcase/src/main/webapp/index.jsp b/plugins/struts2-rest-plugin/showcase/src/main/webapp/index.jsp new file mode 100644 index 000000000..7e3fc9d05 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/webapp/index.jsp @@ -0,0 +1,2 @@ + +<% response.sendRedirect("orders"); %> diff --git a/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-deleteConfirm.jsp b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-deleteConfirm.jsp new file mode 100644 index 000000000..8bb655552 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-deleteConfirm.jsp @@ -0,0 +1,23 @@ + + + + + Order ${id} + + +
+

+ Are you sure you want to delete order ${id}? +

+
+ + +
+
+
+ Back to Orders + + + \ No newline at end of file diff --git a/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-edit.jsp b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-edit.jsp new file mode 100644 index 000000000..b327adbf1 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-edit.jsp @@ -0,0 +1,27 @@ + + +<%@taglib prefix="s" uri="/struts-tags" %> + + + + Order <s:property value="id" /> + + + + + + + + + + +
+ +
+
+ Back to Orders + + + \ No newline at end of file diff --git a/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-editNew.jsp b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-editNew.jsp new file mode 100644 index 000000000..a0f4feba1 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-editNew.jsp @@ -0,0 +1,25 @@ + + +<%@taglib prefix="s" uri="/struts-tags" %> + + + + New Order + + + + + + + + +
+ +
+
+ Back to Orders + + + \ No newline at end of file diff --git a/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-index.jsp b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-index.jsp new file mode 100644 index 000000000..4a72729a0 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-index.jsp @@ -0,0 +1,34 @@ + + +<%@taglib prefix="s" uri="/struts-tags" %> + + + + Orders + + + + + + + + + + + + + + + + + + +
IDClientAmountActions
${id}${clientName}${amount}View | + Edit | + Delete
+ Create a new order + + + \ No newline at end of file diff --git a/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-show.jsp b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-show.jsp new file mode 100644 index 000000000..721c846a9 --- /dev/null +++ b/plugins/struts2-rest-plugin/showcase/src/main/webapp/orders-show.jsp @@ -0,0 +1,27 @@ + + + + + Order ${id} + + + + + + + + + + + + + + + +
ID${id}
Client${clientName}
Amount${amount}
+ Back to Orders + + + \ No newline at end of file diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java new file mode 100644 index 000000000..f4e3ab4ba --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java @@ -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 handlers = new HashMap(); + 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 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; + } +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java new file mode 100644 index 000000000..ecdd61204 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java @@ -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(); + } + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ControllerClasspathPackageProvider.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ControllerClasspathPackageProvider.java new file mode 100644 index 000000000..353882f19 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ControllerClasspathPackageProvider.java @@ -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"; + } + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java new file mode 100644 index 000000000..140717a71 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java @@ -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; + } + + + + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/HttpHeaders.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/HttpHeaders.java new file mode 100644 index 000000000..7a4e0a2f8 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/HttpHeaders.java @@ -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(); +} \ No newline at end of file diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java new file mode 100644 index 000000000..557dd391f --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java @@ -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; + } + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java new file mode 100644 index 000000000..96eddd6c0 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java @@ -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; + +/** + * + * + * 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. + * + *

+ * This mapper supports the following parameters: + *

+ *
    + *
  • struts.mapper.idParameterName - 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. + *
  • + *
  • struts.mapper.indexMethodName - The method name to call for a GET + * request with no id parameter. Defaults to 'index'. + *
  • + *
  • struts.mapper.getMethodName - The method name to call for a GET + * request with an id parameter. Defaults to 'show'. + *
  • + *
  • struts.mapper.postMethodName - The method name to call for a POST + * request with no id parameter. Defaults to 'create'. + *
  • + *
  • struts.mapper.putMethodName - The method name to call for a PUT + * request with an id parameter. Defaults to 'update'. + *
  • + *
  • struts.mapper.deleteMethodName - The method name to call for a DELETE + * request with an id parameter. Defaults to 'destroy'. + *
  • + *
  • struts.mapper.editMethodName - The method name to call for a GET + * request with an id parameter and the 'edit' view specified. Defaults to 'edit'. + *
  • + *
  • struts.mapper.newMethodName - The method name to call for a GET + * request with no id parameter and the 'new' view specified. Defaults to 'editNew'. + *
  • + *
+ *

+ * The following URL's will invoke its methods: + *

+ *
    + *
  • GET: /movies => method="index"
  • + *
  • GET: /movies/Thrillers => method="show", id="Thrillers"
  • + *
  • GET: /movies/Thrillers;edit => method="edit", id="Thrillers"
  • + *
  • GET: /movies/Thrillers/edit => method="edit", id="Thrillers"
  • + *
  • GET: /movies/new => method="editNew"
  • + *
  • POST: /movies => method="create"
  • + *
  • PUT: /movies/Thrillers => method="update", id="Thrillers"
  • + *
  • DELETE: /movies/Thrillers => method="destroy", id="Thrillers"
  • + *
+ *

+ * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML, + * the HTTP parameter "_method" will be used. + *

+ * + */ +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)); + } + } + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java new file mode 100644 index 000000000..cce69f695 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java @@ -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); + } + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java new file mode 100644 index 000000000..585ec7ae9 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java @@ -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.*; + +/** + * + * + * An interceptor that makes sure there are not validation errors before allowing the interceptor chain to continue. + * This interceptor does not perform any validation. + * + *

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.

+ * + *

This interceptor does nothing if the name of the method being invoked is specified in the excludeMethods + * parameter. excludeMethods accepts a comma-delimited list of method names. For example, requests to + * foo!input.action and foo!back.action will be skipped by this interceptor if you set the + * excludeMethods parameter to "input, back". + * + * Note: 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. + * + * + * + *

Interceptor parameters: + * + * + * + *

    + * + *
  • inputResultName - Default to "input". Determine the result name to be returned when + * an action / field error is found.
  • + * + *
+ * + * + * + *

Extending the interceptor: + * + *

+ * + * + * + * There are no known extension points for this interceptor. + * + * + * + *

Example code: + * + *

+ * 
+ * 
+ * <action name="someAction" class="com.examples.SomeAction">
+ *     <interceptor-ref name="params"/>
+ *     <interceptor-ref name="validation"/>
+ *     <interceptor-ref name="workflow"/>
+ *     <result name="success">good_result.ftl</result>
+ * </action>
+ * 
+ * <-- In this case myMethod as well as mySecondMethod of the action class
+ *        will not pass through the workflow process -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ *     <interceptor-ref name="params"/>
+ *     <interceptor-ref name="validation"/>
+ *     <interceptor-ref name="workflow">
+ *         <param name="excludeMethods">myMethod,mySecondMethod</param>
+ *     </interceptor-ref name="workflow">
+ *     <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ * <-- In this case, the result named "error" will be used when
+ *        an action / field error is found -->
+ * <-- The Interceptor will only be applied for myWorkflowMethod method of action
+ *        classes, since this is the only included method while any others are excluded -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ *     <interceptor-ref name="params"/>
+ *     <interceptor-ref name="validation"/>
+ *     <interceptor-ref name="workflow">
+ *        <param name="inputResultName">error</param>
+*         <param name="excludeMethods">*</param>
+*         <param name="includeMethods">myWorkflowMethod</param>
+ *     </interceptor-ref>
+ *     <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ * 
+ * 
+ * + * @author Jason Carreira + * @author Rainer Hermanns + * @author Alexandru Popescu + * @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 inputResultName (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(); + } + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java new file mode 100644 index 000000000..32b4ec09b --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java @@ -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(); +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java new file mode 100644 index 000000000..9041395ee --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java @@ -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"; + } + +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/JsonLibHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/JsonLibHandler.java new file mode 100644 index 000000000..789fa4923 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/JsonLibHandler.java @@ -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"; + } +} diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java new file mode 100644 index 000000000..29e12bd23 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java @@ -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"; + } +} diff --git a/plugins/struts2-rest-plugin/src/main/resources/struts-plugin.xml b/plugins/struts2-rest-plugin/src/main/resources/struts-plugin.xml new file mode 100644 index 000000000..fc714d5ff --- /dev/null +++ b/plugins/struts2-rest-plugin/src/main/resources/struts-plugin.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 303 + + + 303 + + + + + + + + + + + + + + + AUTOMATIC + + + + + + + + + + true + + + + + + dojo\..* + + + + + input,back,cancel,browse,index,show,edit,editNew + + + input,back,cancel,browse,index,show,edit,editNew + + + + + + + + + diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java new file mode 100644 index 000000000..480c1e574 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java @@ -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 handlers = mgr.handlers; + assertNotNull(handlers); + assertEquals(2, handlers.size()); + assertEquals(mockHandlerXmlOverride.proxy(), handlers.get("xml")); + assertEquals(mockHandlerJson.proxy(), handlers.get("json")); + } +} diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java new file mode 100644 index 000000000..a0aaf17ab --- /dev/null +++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java @@ -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()); + } +} diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java new file mode 100644 index 000000000..54278694f --- /dev/null +++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java @@ -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()); + } + +} diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/Contact.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/Contact.java new file mode 100644 index 000000000..701798397 --- /dev/null +++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/Contact.java @@ -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; + } +} diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/JsonLibHandlerTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/JsonLibHandlerTest.java new file mode 100644 index 000000000..22303aaea --- /dev/null +++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/JsonLibHandlerTest.java @@ -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); + } +}