/*
* $Id: JSONValidationInterceptor.java 756478 2009-03-20 14:19:46Z musachy $
*
* 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.interceptor.validation;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.ServletActionContext;
import org.apache.commons.lang.xwork.StringEscapeUtils;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ModelDriven;
import com.opensymphony.xwork2.ValidationAware;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
/**
* <p>Serializes validation and action errors into JSON. This interceptor does not
* perform any validation, so it must follow the 'validation' interceptor on the stack.
* </p>
*
* <p>This stack (defined in struts-default.xml) shows how to use this interceptor with the
* 'validation' interceptor</p>
* <pre>
* <interceptor-stack name="jsonValidationWorkflowStack">
* <interceptor-ref name="basicStack"/>
* <interceptor-ref name="validation">
* <param name="excludeMethods">input,back,cancel</param>
* </interceptor-ref>
* <interceptor-ref name="jsonValidation"/>
* <interceptor-ref name="workflow"/>
* </interceptor-stack>
* </pre>
* <p>If 'validationFailedStatus' is set it will be used as the Response status
* when validation fails.</p>
*
* <p>If the request has a parameter 'struts.validateOnly' execution will return after
* validation (action won't be executed).</p>
*
* <p>A request parameter named 'enableJSONValidation' must be set to 'true' to
* use this interceptor</p>
*/
public class JSONValidationInterceptor extends MethodFilterInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(JSONValidationInterceptor.class);
private static final String VALIDATE_ONLY_PARAM = "struts.validateOnly";
private static final String VALIDATE_JSON_PARAM = "struts.enableJSONValidation";
private int validationFailedStatus = -1;
/**
* HTTP status that will be set in the response if validation fails
* @param validationFailedStatus
*/
public void setValidationFailedStatus(int validationFailedStatus) {
this.validationFailedStatus = validationFailedStatus;
}
@Override
protected String doIntercept(ActionInvocation invocation) throws Exception {
HttpServletResponse response = ServletActionContext.getResponse();
HttpServletRequest request = ServletActionContext.getRequest();
Object action = invocation.getAction();
String jsonEnabled = request.getParameter(VALIDATE_JSON_PARAM);
if (jsonEnabled != null && "true".equals(jsonEnabled)) {
if (action instanceof ValidationAware) {
// generate json
ValidationAware validationAware = (ValidationAware) action;
if (validationAware.hasErrors()) {
if (validationFailedStatus >= 0)
response.setStatus(validationFailedStatus);
response.setCharacterEncoding("UTF-8");
response.getWriter().print(buildResponse(validationAware));
response.setContentType("application/json");
return Action.NONE;
}
}
String validateOnly = request.getParameter(VALIDATE_ONLY_PARAM);
if (validateOnly != null && "true".equals(validateOnly)) {
//there were no errors
response.setCharacterEncoding("UTF-8");
response.getWriter().print("/* {} */");
response.setContentType("application/json");
return Action.NONE;
} else {
return invocation.invoke();
}
} else
return invocation.invoke();
}
/**
* @return JSON string that contains the errors and field errors
*/
@SuppressWarnings("unchecked")
protected String buildResponse(ValidationAware validationAware) {
//should we use FreeMarker here?
StringBuilder sb = new StringBuilder();
sb.append("/* { ");
if (validationAware.hasErrors()) {
//action errors
if (validationAware.hasActionErrors()) {
sb.append("\"errors\":");
sb.append(buildArray(validationAware.getActionErrors()));
}
//field errors
if (validationAware.hasFieldErrors()) {
if (validationAware.hasActionErrors())
sb.append(",");
sb.append("\"fieldErrors\": {");
Map<String, List<String>> fieldErrors = validationAware
.getFieldErrors();
for (Map.Entry<String, List<String>> fieldError : fieldErrors
.entrySet()) {
sb.append("\"");
//if it is model driven, remove "model." see WW-2721
sb.append(validationAware instanceof ModelDriven ? fieldError.getKey().substring(6)
: fieldError.getKey());
sb.append("\":");
sb.append(buildArray(fieldError.getValue()));
sb.append(",");
}
//remove trailing comma, IE creates an empty object, duh
sb.deleteCharAt(sb.length() - 1);
sb.append("}");
}
}
sb.append("} */");
/*response should be something like:
* {
* "errors": ["this", "that"],
* "fieldErrors": {
* field1: "this",
* field2: "that"
* }
* }
*/
return sb.toString();
}
private String buildArray(Collection<String> values) {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (String value : values) {
sb.append("\"");
sb.append(StringEscapeUtils.escapeJavaScript(value));
sb.append("\",");
}
if (values.size() > 0)
sb.deleteCharAt(sb.length() - 1);
sb.append("]");
return sb.toString();
}
}
|