View Javadoc

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