View Javadoc

1   /*
2    * $Id: JSONValidationInterceptor.java 677429 2008-07-16 21:01:56Z 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  
35  import com.opensymphony.xwork2.Action;
36  import com.opensymphony.xwork2.ActionInvocation;
37  import com.opensymphony.xwork2.ModelDriven;
38  import com.opensymphony.xwork2.ValidationAware;
39  import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
40  import com.opensymphony.xwork2.util.logging.Logger;
41  import com.opensymphony.xwork2.util.logging.LoggerFactory;
42  
43  /***
44   * <p>Serializes validation and action errors into JSON. This interceptor does not
45   * perform any validation, so it must follow the 'validation' interceptor on the stack.
46   * </p>
47   *
48   * <p>This stack (defined in struts-default.xml) shows how to use this interceptor with the
49   * 'validation' interceptor</p>
50   * <pre>
51   * &lt;interceptor-stack name="jsonValidationWorkflowStack"&gt;
52   *      &lt;interceptor-ref name="basicStack"/&gt;
53   *      &lt;interceptor-ref name="validation"&gt;
54   *            &lt;param name="excludeMethods"&gt;input,back,cancel&lt;/param&gt;
55   *      &lt;/interceptor-ref&gt;
56   *      &lt;interceptor-ref name="jsonValidation"/&gt;
57   *      &lt;interceptor-ref name="workflow"/&gt;
58   * &lt;/interceptor-stack&gt;
59   * </pre>
60   * <p>If 'validationFailedStatus' is set it will be used as the Response status
61   * when validation fails.</p>
62   *
63   * <p>If the request has a parameter 'struts.validateOnly' execution will return after
64   * validation (action won't be executed).</p>
65   *
66   * <p>A request parameter named 'enableJSONValidation' must be set to 'true' to
67   * use this interceptor</p>
68   */
69  public class JSONValidationInterceptor extends MethodFilterInterceptor {
70      private static final Logger LOG = LoggerFactory.getLogger(JSONValidationInterceptor.class);
71  
72      private static final String VALIDATE_ONLY_PARAM = "struts.validateOnly";
73      private static final String VALIDATE_JSON_PARAM = "struts.enableJSONValidation";
74  
75      static char[] hex = "0123456789ABCDEF".toCharArray();
76  
77      private int validationFailedStatus = -1;
78  
79      /***
80       * HTTP status that will be set in the response if validation fails
81       * @param validationFailedStatus
82       */
83      public void setValidationFailedStatus(int validationFailedStatus) {
84          this.validationFailedStatus = validationFailedStatus;
85      }
86  
87      @Override
88      protected String doIntercept(ActionInvocation invocation) throws Exception {
89          HttpServletResponse response = ServletActionContext.getResponse();
90          HttpServletRequest request = ServletActionContext.getRequest();
91  
92          Object action = invocation.getAction();
93          String jsonEnabled = request.getParameter(VALIDATE_JSON_PARAM);
94  
95          if (jsonEnabled != null && "true".equals(jsonEnabled)) {
96              if (action instanceof ValidationAware) {
97                  // generate json
98                  ValidationAware validationAware = (ValidationAware) action;
99                  if (validationAware.hasErrors()) {
100                     if (validationFailedStatus >= 0)
101                         response.setStatus(validationFailedStatus);
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.getWriter().print("/* {} */");
112                 response.setContentType("application/json");
113                 return Action.NONE;
114             } else {
115                 return invocation.invoke();
116             }
117         } else
118             return invocation.invoke();
119     }
120 
121     /***
122      * @return JSON string that contains the errors and field errors
123      */
124     @SuppressWarnings("unchecked")
125     protected String buildResponse(ValidationAware validationAware) {
126         //should we use FreeMarker here?
127         StringBuilder sb = new StringBuilder();
128         sb.append("/* { ");
129 
130         if (validationAware.hasErrors()) {
131             //action errors
132             if (validationAware.hasActionErrors()) {
133                 sb.append("\"errors\":");
134                 sb.append(buildArray(validationAware.getActionErrors()));
135             }
136 
137             //field errors
138             if (validationAware.hasFieldErrors()) {
139                 if (validationAware.hasActionErrors())
140                     sb.append(",");
141                 sb.append("\"fieldErrors\": {");
142                 Map<String, List<String>> fieldErrors = validationAware
143                     .getFieldErrors();
144                 for (Map.Entry<String, List<String>> fieldError : fieldErrors
145                     .entrySet()) {
146                     sb.append("\"");
147                     //if it is model driven, remove "model." see WW-2721
148                     sb.append(validationAware instanceof ModelDriven ? fieldError.getKey().substring(6)
149                             : fieldError.getKey());
150                     sb.append("\":");
151                     sb.append(buildArray(fieldError.getValue()));
152                     sb.append(",");
153                 }
154                 //remove trailing comma, IE creates an empty object, duh
155                 sb.deleteCharAt(sb.length() - 1);
156                 sb.append("}");
157             }
158         }
159 
160         sb.append("} */");
161         /*response should be something like:
162          * {
163          *      "errors": ["this", "that"],
164          *      "fieldErrors": {
165          *            field1: "this",
166          *            field2: "that"
167          *      }
168          * }
169          */
170         return sb.toString();
171     }
172 
173     private String buildArray(Collection<String> values) {
174         StringBuilder sb = new StringBuilder();
175         sb.append("[");
176         for (String value : values) {
177             sb.append("\"");
178             sb.append(escapeJSON(value));
179             sb.append("\",");
180         }
181         if (values.size() > 0)
182             sb.deleteCharAt(sb.length() - 1);
183         sb.append("]");
184         return sb.toString();
185     }
186 
187     private String escapeJSON(Object obj) {
188         StringBuilder sb = new StringBuilder();
189 
190         CharacterIterator it = new StringCharacterIterator(obj.toString());
191 
192         for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
193             if (c == '"') {
194                 sb.append("//\"");
195             } else if (c == '//') {
196                 sb.append("////");
197             } else if (c == '/') {
198                 sb.append("///");
199             } else if (c == '\b') {
200                 sb.append("//b");
201             } else if (c == '\f') {
202                 sb.append("//f");
203             } else if (c == '\n') {
204                 sb.append("//n");
205             } else if (c == '\r') {
206                 sb.append("//r");
207             } else if (c == '\t') {
208                 sb.append("//t");
209             } else if (Character.isISOControl(c)) {
210                 sb.append(unicode(c));
211             } else {
212                 sb.append(c);
213             }
214         }
215         return sb.toString();
216     }
217 
218     /***
219      * Represent as unicode
220      * @param c character to be encoded
221      */
222     private String unicode(char c) {
223         StringBuilder sb = new StringBuilder();
224         sb.append("//u");
225 
226         int n = c;
227 
228         for (int i = 0; i < 4; ++i) {
229             int digit = (n & 0xf000) >> 12;
230 
231             sb.append(hex[digit]);
232             n <<= 4;
233         }
234         return sb.toString();
235     }
236 
237 }