View Javadoc

1   /*
2    * $Id: JSONValidationInterceptor.java 756478 2009-03-20 14:19:46Z musachy $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts2.interceptor.validation;
23  
24  import java.text.CharacterIterator;
25  import java.text.StringCharacterIterator;
26  import java.util.Collection;
27  import java.util.List;
28  import java.util.Map;
29  
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpServletResponse;
32  
33  import org.apache.struts2.ServletActionContext;
34  import org.apache.commons.lang.xwork.StringEscapeUtils;
35  
36  import com.opensymphony.xwork2.Action;
37  import com.opensymphony.xwork2.ActionInvocation;
38  import com.opensymphony.xwork2.ModelDriven;
39  import com.opensymphony.xwork2.ValidationAware;
40  import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
41  import com.opensymphony.xwork2.util.logging.Logger;
42  import com.opensymphony.xwork2.util.logging.LoggerFactory;
43  
44  /***
45   * <p>Serializes validation and action errors into JSON. This interceptor does not
46   * perform any validation, so it must follow the 'validation' interceptor on the stack.
47   * </p>
48   *
49   * <p>This stack (defined in struts-default.xml) shows how to use this interceptor with the
50   * 'validation' interceptor</p>
51   * <pre>
52   * &lt;interceptor-stack name="jsonValidationWorkflowStack"&gt;
53   *      &lt;interceptor-ref name="basicStack"/&gt;
54   *      &lt;interceptor-ref name="validation"&gt;
55   *            &lt;param name="excludeMethods"&gt;input,back,cancel&lt;/param&gt;
56   *      &lt;/interceptor-ref&gt;
57   *      &lt;interceptor-ref name="jsonValidation"/&gt;
58   *      &lt;interceptor-ref name="workflow"/&gt;
59   * &lt;/interceptor-stack&gt;
60   * </pre>
61   * <p>If 'validationFailedStatus' is set it will be used as the Response status
62   * when validation fails.</p>
63   *
64   * <p>If the request has a parameter 'struts.validateOnly' execution will return after
65   * validation (action won't be executed).</p>
66   *
67   * <p>A request parameter named 'enableJSONValidation' must be set to 'true' to
68   * use this interceptor</p>
69   */
70  public class JSONValidationInterceptor extends MethodFilterInterceptor {
71      private static final Logger LOG = LoggerFactory.getLogger(JSONValidationInterceptor.class);
72  
73      private static final String VALIDATE_ONLY_PARAM = "struts.validateOnly";
74      private static final String VALIDATE_JSON_PARAM = "struts.enableJSONValidation";
75  
76      private int validationFailedStatus = -1;
77  
78      /***
79       * HTTP status that will be set in the response if validation fails
80       * @param validationFailedStatus
81       */
82      public void setValidationFailedStatus(int validationFailedStatus) {
83          this.validationFailedStatus = validationFailedStatus;
84      }
85  
86      @Override
87      protected String doIntercept(ActionInvocation invocation) throws Exception {
88          HttpServletResponse response = ServletActionContext.getResponse();
89          HttpServletRequest request = ServletActionContext.getRequest();
90  
91          Object action = invocation.getAction();
92          String jsonEnabled = request.getParameter(VALIDATE_JSON_PARAM);
93  
94          if (jsonEnabled != null && "true".equals(jsonEnabled)) {
95              if (action instanceof ValidationAware) {
96                  // generate json
97                  ValidationAware validationAware = (ValidationAware) action;
98                  if (validationAware.hasErrors()) {
99                      if (validationFailedStatus >= 0)
100                         response.setStatus(validationFailedStatus);
101                     response.setCharacterEncoding("UTF-8");
102                     response.getWriter().print(buildResponse(validationAware));
103                     response.setContentType("application/json");
104                     return Action.NONE;
105                 }
106             }
107 
108             String validateOnly = request.getParameter(VALIDATE_ONLY_PARAM);
109             if (validateOnly != null && "true".equals(validateOnly)) {
110                 //there were no errors
111                 response.setCharacterEncoding("UTF-8");
112                 response.getWriter().print("/* {} */");
113                 response.setContentType("application/json");
114                 return Action.NONE;
115             } else {
116                 return invocation.invoke();
117             }
118         } else
119             return invocation.invoke();
120     }
121 
122     /***
123      * @return JSON string that contains the errors and field errors
124      */
125     @SuppressWarnings("unchecked")
126     protected String buildResponse(ValidationAware validationAware) {
127         //should we use FreeMarker here?
128         StringBuilder sb = new StringBuilder();
129         sb.append("/* { ");
130 
131         if (validationAware.hasErrors()) {
132             //action errors
133             if (validationAware.hasActionErrors()) {
134                 sb.append("\"errors\":");
135                 sb.append(buildArray(validationAware.getActionErrors()));
136             }
137 
138             //field errors
139             if (validationAware.hasFieldErrors()) {
140                 if (validationAware.hasActionErrors())
141                     sb.append(",");
142                 sb.append("\"fieldErrors\": {");
143                 Map<String, List<String>> fieldErrors = validationAware
144                     .getFieldErrors();
145                 for (Map.Entry<String, List<String>> fieldError : fieldErrors
146                     .entrySet()) {
147                     sb.append("\"");
148                     //if it is model driven, remove "model." see WW-2721
149                     sb.append(validationAware instanceof ModelDriven ? fieldError.getKey().substring(6)
150                             : fieldError.getKey());
151                     sb.append("\":");
152                     sb.append(buildArray(fieldError.getValue()));
153                     sb.append(",");
154                 }
155                 //remove trailing comma, IE creates an empty object, duh
156                 sb.deleteCharAt(sb.length() - 1);
157                 sb.append("}");
158             }
159         }
160 
161         sb.append("} */");
162         /*response should be something like:
163          * {
164          *      "errors": ["this", "that"],
165          *      "fieldErrors": {
166          *            field1: "this",
167          *            field2: "that"
168          *      }
169          * }
170          */
171         return sb.toString();
172     }
173 
174     private String buildArray(Collection<String> values) {
175         StringBuilder sb = new StringBuilder();
176         sb.append("[");
177         for (String value : values) {
178             sb.append("\"");
179             sb.append(StringEscapeUtils.escapeJavaScript(value));
180             sb.append("\",");
181         }
182         if (values.size() > 0)
183             sb.deleteCharAt(sb.length() - 1);
184         sb.append("]");
185         return sb.toString();
186     }
187 }