From 60fdb065c583fe93d576aaa97c8617dd00d76e91 Mon Sep 17 00:00:00 2001 From: Musachy Barroso Date: Mon, 10 Aug 2009 18:19:48 +0000 Subject: [PATCH] WW-3217 Bring JSON plugin into trunk git-svn-id: https://svn.apache.org/repos/asf/struts/struts2/trunk@802878 13f79535-47bb-0310-9956-ffa450edef68 --- plugins/json/pom.xml | 102 ++++ .../org/apache/struts2/json/JSONCleaner.java | 64 ++ .../apache/struts2/json/JSONException.java | 38 ++ .../apache/struts2/json/JSONInterceptor.java | 480 +++++++++++++++ .../apache/struts2/json/JSONPopulator.java | 444 ++++++++++++++ .../org/apache/struts2/json/JSONReader.java | 289 +++++++++ .../org/apache/struts2/json/JSONResult.java | 522 +++++++++++++++++ .../org/apache/struts2/json/JSONUtil.java | 395 +++++++++++++ .../org/apache/struts2/json/JSONWriter.java | 549 ++++++++++++++++++ .../struts2/json/SerializationParams.java | 120 ++++ .../apache/struts2/json/annotations/JSON.java | 41 ++ .../apache/struts2/json/annotations/SMD.java | 36 ++ .../struts2/json/annotations/SMDMethod.java | 32 + .../json/annotations/SMDMethodParameter.java | 32 + .../org/apache/struts2/json/rpc/RPCError.java | 109 ++++ .../apache/struts2/json/rpc/RPCErrorCode.java | 47 ++ .../apache/struts2/json/rpc/RPCResponse.java | 54 ++ .../java/org/apache/struts2/json/smd/SMD.java | 78 +++ .../apache/struts2/json/smd/SMDMethod.java | 75 +++ .../struts2/json/smd/SMDMethodParameter.java | 57 ++ .../json/src/main/resources/JsonPlugin.tld | 25 + .../json/src/main/resources/struts-plugin.xml | 16 + .../java/org/apache/struts2/json/AnEnum.java | 28 + .../org/apache/struts2/json/AnEnumBean.java | 45 ++ .../java/org/apache/struts2/json/Bean.java | 151 +++++ .../org/apache/struts2/json/JSONEnumTest.java | 127 ++++ .../struts2/json/JSONInterceptorTest.java | 502 ++++++++++++++++ .../struts2/json/JSONPopulatorTest.java | 163 ++++++ .../apache/struts2/json/JSONResultTest.java | 534 +++++++++++++++++ .../org/apache/struts2/json/JSONUtilTest.java | 66 +++ .../org/apache/struts2/json/OtherBean.java | 97 ++++ .../apache/struts2/json/SMDActionTest1.java | 197 +++++++ .../apache/struts2/json/SMDActionTest2.java | 58 ++ .../struts2/json/SMDMethodInterfaceTest.java | 195 +++++++ .../org/apache/struts2/json/SimpleValue.java | 37 ++ .../org/apache/struts2/json/TestAction.java | 188 ++++++ .../org/apache/struts2/json/TestAction2.java | 27 + .../org/apache/struts2/json/TestAction3.java | 27 + .../org/apache/struts2/json/TestAction4.java | 35 ++ .../org/apache/struts2/json/TestUtils.java | 95 +++ .../apache/struts2/json/WrapperClassBean.java | 181 ++++++ .../org/apache/struts2/json/bad-1.txt | 3 + .../org/apache/struts2/json/bad-2.txt | 2 + .../org/apache/struts2/json/bad-3.txt | 3 + .../org/apache/struts2/json/bad-4.txt | 1 + .../org/apache/struts2/json/bad-5.txt | 1 + .../apache/struts2/json/bad-to-the-bone.txt | 0 .../org/apache/struts2/json/json-1.txt | 49 ++ .../org/apache/struts2/json/json-10.txt | 3 + .../org/apache/struts2/json/json-2-enum.txt | 21 + .../org/apache/struts2/json/json-2.txt | 15 + .../org/apache/struts2/json/json-3.txt | 43 ++ .../org/apache/struts2/json/json-4.txt | 4 + .../org/apache/struts2/json/json-5.txt | 8 + .../org/apache/struts2/json/json-6.txt | 1 + .../org/apache/struts2/json/json-7.txt | 20 + .../org/apache/struts2/json/json-8.txt | 13 + .../org/apache/struts2/json/json-9.txt | 3 + .../org/apache/struts2/json/json.txt | 43 ++ .../org/apache/struts2/json/jsonp-1.txt | 15 + .../org/apache/struts2/json/nulls-1.txt | 1 + .../org/apache/struts2/json/prefix-1.txt | 1 + .../org/apache/struts2/json/smd-1.txt | 41 ++ .../org/apache/struts2/json/smd-10.txt | 4 + .../org/apache/struts2/json/smd-11.txt | 5 + .../org/apache/struts2/json/smd-12.txt | 19 + .../org/apache/struts2/json/smd-13.txt | 10 + .../org/apache/struts2/json/smd-14.txt | 5 + .../org/apache/struts2/json/smd-15.txt | 5 + .../org/apache/struts2/json/smd-2.txt | 24 + .../org/apache/struts2/json/smd-3.txt | 5 + .../org/apache/struts2/json/smd-4.txt | 4 + .../org/apache/struts2/json/smd-5.txt | 5 + .../org/apache/struts2/json/smd-6.txt | 5 + .../org/apache/struts2/json/smd-7.txt | 19 + .../org/apache/struts2/json/smd-8.txt | 16 + .../org/apache/struts2/json/smd-9.txt | 5 + .../resources/org/apache/struts2/json/smd.txt | 1 + 78 files changed, 6781 insertions(+) create mode 100644 plugins/json/pom.xml create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONCleaner.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONException.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONResult.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/SerializationParams.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/annotations/SMD.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethod.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethodParameter.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCError.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCErrorCode.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCResponse.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/smd/SMD.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethod.java create mode 100644 plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethodParameter.java create mode 100644 plugins/json/src/main/resources/JsonPlugin.tld create mode 100644 plugins/json/src/main/resources/struts-plugin.xml create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/AnEnum.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/AnEnumBean.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/Bean.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/OtherBean.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest1.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest2.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/SMDMethodInterfaceTest.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/SimpleValue.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/TestAction.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/TestAction2.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/TestAction3.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/TestAction4.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/TestUtils.java create mode 100644 plugins/json/src/test/java/org/apache/struts2/json/WrapperClassBean.java create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/bad-1.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/bad-2.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/bad-3.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/bad-4.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/bad-5.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/bad-to-the-bone.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-1.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-10.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-2-enum.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-2.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-3.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-4.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-5.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-6.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-7.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-8.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json-9.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/json.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/jsonp-1.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/nulls-1.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/prefix-1.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-1.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-10.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-11.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-12.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-13.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-14.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-15.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-2.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-3.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-4.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-5.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-6.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-7.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-8.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd-9.txt create mode 100644 plugins/json/src/test/resources/org/apache/struts2/json/smd.txt diff --git a/plugins/json/pom.xml b/plugins/json/pom.xml new file mode 100644 index 000000000..f62c57c36 --- /dev/null +++ b/plugins/json/pom.xml @@ -0,0 +1,102 @@ + + 4.0.0 + + org.apache.struts + struts2-plugins + 2.1.8-SNAPSHOT + + org.apache.struts + struts2-json-plugin + jar + Struts 2 JSON Plugin + + + scm:svn:http://svn.apache.org/repos/asf/struts/struts2/trunk/struts2-json-plugin + scm:svn:https://svn.apache.org/repos/asf/struts/sandbox/struts2/struts2-json-plugin + http://svn.apache.org/viewcvs.cgi/struts/sandbox/struts2/struts2-json-plugin + + + + + + true + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.struts2.osgi.StrutsActivator + META-INF + + + + + + + + + + + + junit + junit + 4.3 + test + + + + + org.apache.struts + struts2-core + ${pom.version} + + + + org.apache.struts + struts2-junit-plugin + ${pom.version} + + + + javax.servlet + servlet-api + 2.4 + provided + + + + javax.servlet + jsp-api + 2.0 + provided + + + + org.easymock + easymock + 2.3 + test + + + + + org.springframework + spring-test + ${struts2.springPlatformVersion} + true + + + diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONCleaner.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONCleaner.java new file mode 100644 index 000000000..c05a8b69b --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONCleaner.java @@ -0,0 +1,64 @@ +/* + * $Id$ + * + * 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.json; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Isolate the process of cleaning JSON data from the Interceptor class itself. + */ +public abstract class JSONCleaner { + + public Object clean(String ognlPrefix, Object data) throws JSONException { + if (data == null) + return null; + else if (data instanceof List) + return cleanList(ognlPrefix, data); + else if (data instanceof Map) + return cleanMap(ognlPrefix, data); + else + return cleanValue(ognlPrefix, data); + } + + protected Object cleanList(String ognlPrefix, Object data) throws JSONException { + List list = (List) data; + int count = list.size(); + for (int i = 0; i < count; i++) { + list.set(i, clean(ognlPrefix + "[" + i + "]", list.get(i))); + } + return list; + } + + protected Object cleanMap(String ognlPrefix, Object data) throws JSONException { + Map map = (Map) data; + Iterator iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry e = (Map.Entry) iter.next(); + e.setValue(clean((ognlPrefix.length() > 0 ? ognlPrefix + "." : "") + e.getKey(), e.getValue())); + } + return map; + } + + protected abstract Object cleanValue(String ognlName, Object data) throws JSONException; + +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONException.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONException.java new file mode 100644 index 000000000..220eb5bf4 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONException.java @@ -0,0 +1,38 @@ +/* + * $Id$ + * + * 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.json; + +/** + * Wrap exceptions throwed by the JSON serializer + */ +public class JSONException extends Exception { + public JSONException(String message) { + super(message); + } + + public JSONException(Throwable cause) { + super(cause); + } + + public JSONException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java new file mode 100644 index 000000000..2c4ced210 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java @@ -0,0 +1,480 @@ +/* + * $Id$ + * + * 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.json; + +import java.beans.IntrospectionException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.json.annotations.SMDMethod; +import org.apache.struts2.json.rpc.RPCError; +import org.apache.struts2.json.rpc.RPCErrorCode; +import org.apache.struts2.json.rpc.RPCResponse; + +import com.opensymphony.xwork2.Action; +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.interceptor.Interceptor; +import com.opensymphony.xwork2.util.ValueStack; +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +/** + * Populates an action from a JSON string + */ +public class JSONInterceptor implements Interceptor { + private static final long serialVersionUID = 4950170304212158803L; + private static final Logger LOG = LoggerFactory.getLogger(JSONInterceptor.class); + private boolean enableSMD = false; + private boolean enableGZIP = false; + private boolean wrapWithComments; + private boolean prefix; + private String defaultEncoding = "ISO-8859-1"; + private boolean ignoreHierarchy = true; + private String root; + private List excludeProperties; + private List includeProperties; + private boolean ignoreSMDMethodInterfaces = true; + private JSONPopulator populator = new JSONPopulator(); + private JSONCleaner dataCleaner = null; + private boolean debug = false; + private boolean noCache = false; + private boolean excludeNullProperties; + private String callbackParameter; + private String contentType; + + public void destroy() { + } + + public void init() { + } + + @SuppressWarnings("unchecked") + public String intercept(ActionInvocation invocation) throws Exception { + HttpServletRequest request = ServletActionContext.getRequest(); + HttpServletResponse response = ServletActionContext.getResponse(); + String contentType = request.getHeader("content-type"); + if (contentType != null) { + int iSemicolonIdx; + if ((iSemicolonIdx = contentType.indexOf(";")) != -1) + contentType = contentType.substring(0, iSemicolonIdx); + } + + Object rootObject; + if (this.root != null) { + ValueStack stack = invocation.getStack(); + rootObject = stack.findValue(this.root); + + if (rootObject == null) { + throw new RuntimeException("Invalid root expression: '" + this.root + "'."); + } + } else { + rootObject = invocation.getAction(); + } + + if ((contentType != null) && contentType.equalsIgnoreCase("application/json")) { + // load JSON object + Object obj = JSONUtil.deserialize(request.getReader()); + + if (obj instanceof Map) { + Map json = (Map) obj; + + // clean up the values + if (dataCleaner != null) + dataCleaner.clean("", json); + + // populate fields + populator.populateObject(rootObject, json); + } else { + LOG.error("Unable to deserialize JSON object from request"); + throw new JSONException("Unable to deserialize JSON object from request"); + } + } else if ((contentType != null) && contentType.equalsIgnoreCase("application/json-rpc")) { + Object result; + if (this.enableSMD) { + // load JSON object + Object obj = JSONUtil.deserialize(request.getReader()); + + if (obj instanceof Map) { + Map smd = (Map) obj; + + // invoke method + try { + result = this.invoke(rootObject, smd); + } catch (Exception e) { + RPCResponse rpcResponse = new RPCResponse(); + rpcResponse.setId(smd.get("id").toString()); + rpcResponse.setError(new RPCError(e, RPCErrorCode.EXCEPTION, debug)); + + result = rpcResponse; + } + } else { + String message = "SMD request was not in the right format. See http://json-rpc.org"; + + RPCResponse rpcResponse = new RPCResponse(); + rpcResponse.setError(new RPCError(message, RPCErrorCode.INVALID_PROCEDURE_CALL)); + result = rpcResponse; + } + + String json = JSONUtil.serialize(result, excludeProperties, includeProperties, + ignoreHierarchy, excludeNullProperties); + json = addCallbackIfApplicable(request, json); + JSONUtil.writeJSONToResponse(new SerializationParams(response, this.defaultEncoding, + this.wrapWithComments, json, true, false, noCache, -1, -1, prefix, contentType)); + + return Action.NONE; + } else { + String message = "Request with content type of 'application/json-rpc' was received but SMD is " + + "not enabled for this interceptor. Set 'enableSMD' to true to enable it"; + + RPCResponse rpcResponse = new RPCResponse(); + rpcResponse.setError(new RPCError(message, RPCErrorCode.SMD_DISABLED)); + result = rpcResponse; + } + + String json = JSONUtil.serialize(result, excludeProperties, includeProperties, ignoreHierarchy, + excludeNullProperties); + json = addCallbackIfApplicable(request, json); + boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request); + JSONUtil.writeJSONToResponse(new SerializationParams(response, this.defaultEncoding, + this.wrapWithComments, json, true, writeGzip, noCache, -1, -1, prefix, contentType)); + + return Action.NONE; + } else { + if (LOG.isDebugEnabled()) { + LOG + .debug("Content type must be 'application/json' or 'application/json-rpc'. Ignoring request with content type " + + contentType); + } + } + + return invocation.invoke(); + } + + @SuppressWarnings("unchecked") + public RPCResponse invoke(Object object, Map data) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException, JSONException, InstantiationException, + NoSuchMethodException, IntrospectionException { + + RPCResponse response = new RPCResponse(); + + // validate id + Object id = data.get("id"); + if (id == null) { + String message = "'id' is required for JSON RPC"; + response.setError(new RPCError(message, RPCErrorCode.METHOD_NOT_FOUND)); + return response; + } + // could be a numeric value + response.setId(id.toString()); + + // the map is going to have: 'params', 'method' and 'id' (what is the id + // for?) + Class clazz = object.getClass(); + + // parameters + List parameters = (List) data.get("params"); + int parameterCount = parameters != null ? parameters.size() : 0; + + // method + String methodName = (String) data.get("method"); + if (methodName == null) { + String message = "'method' is required for JSON RPC"; + response.setError(new RPCError(message, RPCErrorCode.MISSING_METHOD)); + return response; + } + + Method method = this.getMethod(clazz, methodName, parameterCount); + if (method == null) { + String message = "Method " + methodName + " could not be found in action class."; + response.setError(new RPCError(message, RPCErrorCode.METHOD_NOT_FOUND)); + return response; + } + + // parameters + if (parameterCount > 0) { + Class[] parameterTypes = method.getParameterTypes(); + Type[] genericTypes = method.getGenericParameterTypes(); + List invocationParameters = new ArrayList(); + + // validate size + if (parameterTypes.length != parameterCount) { + // size mismatch + String message = "Parameter count in request, " + parameterCount + + " do not match expected parameter count for " + methodName + ", " + + parameterTypes.length; + + response.setError(new RPCError(message, RPCErrorCode.PARAMETERS_MISMATCH)); + return response; + } + + // convert parameters + for (int i = 0; i < parameters.size(); i++) { + Object parameter = parameters.get(i); + Class paramType = parameterTypes[i]; + Type genericType = genericTypes[i]; + + // clean up the values + if (dataCleaner != null) + parameter = dataCleaner.clean("[" + i + "]", parameter); + + Object converted = populator.convert(paramType, genericType, parameter, method); + invocationParameters.add(converted); + } + + response.setResult(method.invoke(object, invocationParameters.toArray())); + } else { + response.setResult(method.invoke(object, new Object[0])); + } + + return response; + } + + @SuppressWarnings("unchecked") + private Method getMethod(Class clazz, String name, int parameterCount) { + Method[] smdMethods = JSONUtil.listSMDMethods(clazz, ignoreSMDMethodInterfaces); + + for (Method method : smdMethods) { + if (checkSMDMethodSignature(method, name, parameterCount)) { + return method; + } + } + return null; + } + + /** + * Look for a method in clazz carrying the SMDMethod annotation with + * matching name and parametersCount + * + * @return true if matches name and parameterCount + */ + private boolean checkSMDMethodSignature(Method method, String name, int parameterCount) { + + SMDMethod smdMethodAnntotation = method.getAnnotation(SMDMethod.class); + if (smdMethodAnntotation != null) { + String alias = smdMethodAnntotation.name(); + boolean paramsMatch = method.getParameterTypes().length == parameterCount; + if (((alias.length() == 0) && method.getName().equals(name) && paramsMatch) + || (alias.equals(name) && paramsMatch)) { + return true; + } + } + + return false; + } + + protected String addCallbackIfApplicable(HttpServletRequest request, String json) { + if ((callbackParameter != null) && (callbackParameter.length() > 0)) { + String callbackName = request.getParameter(callbackParameter); + if ((callbackName != null) && (callbackName.length() > 0)) + json = callbackName + "(" + json + ")"; + } + return json; + } + + public boolean isEnableSMD() { + return this.enableSMD; + } + + public void setEnableSMD(boolean enableSMD) { + this.enableSMD = enableSMD; + } + + /** + * Ignore annotations on methods in interfaces You may need to set to this + * true if your action is a proxy/enhanced as annotations are not inherited + */ + public void setIgnoreSMDMethodInterfaces(boolean ignoreSMDMethodInterfaces) { + this.ignoreSMDMethodInterfaces = ignoreSMDMethodInterfaces; + } + + /** + * Wrap generated JSON with comments. Only used if SMD is enabled. + * + * @param wrapWithComments + */ + public void setWrapWithComments(boolean wrapWithComments) { + this.wrapWithComments = wrapWithComments; + } + + @Inject(StrutsConstants.STRUTS_I18N_ENCODING) + public void setDefaultEncoding(String val) { + this.defaultEncoding = val; + } + + /** + * Ignore properties defined on base classes of the root object. + * + * @param ignoreHierarchy + */ + public void setIgnoreHierarchy(boolean ignoreHierarchy) { + this.ignoreHierarchy = ignoreHierarchy; + } + + /** + * Sets the root object to be deserialized, defaults to the Action + * + * @param root + * OGNL expression of root object to be serialized + */ + public void setRoot(String root) { + this.root = root; + } + + /** + * Sets the JSONPopulator to be used + * + * @param populator + * JSONPopulator + */ + public void setJSONPopulator(JSONPopulator populator) { + this.populator = populator; + } + + /** + * Sets the JSONCleaner to be used + * + * @param dataCleaner + * JSONCleaner + */ + public void setJSONCleaner(JSONCleaner dataCleaner) { + this.dataCleaner = dataCleaner; + } + + /** + * Turns debugging on or off + * + * @param debug + * true or false + */ + public boolean getDebug() { + return this.debug; + } + + public void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * Sets a comma-delimited list of regular expressions to match properties + * that should be excluded from the JSON output. + * + * @param commaDelim + * A comma-delimited list of regular expressions + */ + public void setExcludeProperties(String commaDelim) { + List excludePatterns = JSONUtil.asList(commaDelim); + if (excludePatterns != null) { + this.excludeProperties = new ArrayList(excludePatterns.size()); + for (String pattern : excludePatterns) { + this.excludeProperties.add(Pattern.compile(pattern)); + } + } + } + + /** + * Sets a comma-delimited list of regular expressions to match properties + * that should be included from the JSON output. + * + * @param commaDelim + * A comma-delimited list of regular expressions + */ + public void setIncludeProperties(String commaDelim) { + List includePatterns = JSONUtil.asList(commaDelim); + if (includePatterns != null) { + this.includeProperties = new ArrayList(includePatterns.size()); + for (String pattern : includePatterns) { + this.includeProperties.add(Pattern.compile(pattern)); + } + } + } + + public boolean isEnableGZIP() { + return enableGZIP; + } + + /** + * Setting this property to "true" will compress the output. + * + * @param enableGZIP + * Enable compressed output + */ + public void setEnableGZIP(boolean enableGZIP) { + this.enableGZIP = enableGZIP; + } + + public boolean isNoCache() { + return noCache; + } + + /** + * Add headers to response to prevent the browser from caching the response + * + * @param noCache + */ + public void setNoCache(boolean noCache) { + this.noCache = noCache; + } + + public boolean isExcludeNullProperties() { + return excludeNullProperties; + } + + /** + * Do not serialize properties with a null value + * + * @param excludeNullProperties + */ + public void setExcludeNullProperties(boolean excludeNullProperties) { + this.excludeNullProperties = excludeNullProperties; + } + + public void setCallbackParameter(String callbackParameter) { + this.callbackParameter = callbackParameter; + } + + public String getCallbackParameter() { + return callbackParameter; + } + + /** + * Add "{} && " to generated JSON + * + * @param prefix + */ + public void setPrefix(boolean prefix) { + this.prefix = prefix; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java new file mode 100644 index 000000000..6d03bdc6e --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONPopulator.java @@ -0,0 +1,444 @@ +/* + * $Id$ + * + * 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.json; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.struts2.json.annotations.JSON; + +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +/** + * Isolate the process of populating JSON objects from the Interceptor class + * itself. + */ +public class JSONPopulator { + + private static final Logger LOG = LoggerFactory.getLogger(JSONPopulator.class); + + private String dateFormat = JSONUtil.RFC3339_FORMAT; + + public JSONPopulator() { + } + + public JSONPopulator(String dateFormat) { + this.dateFormat = dateFormat; + } + + public String getDateFormat() { + return dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + + @SuppressWarnings("unchecked") + public void populateObject(Object object, final Map elements) throws IllegalAccessException, + InvocationTargetException, NoSuchMethodException, IntrospectionException, + IllegalArgumentException, JSONException, InstantiationException { + Class clazz = object.getClass(); + + BeanInfo info = Introspector.getBeanInfo(clazz); + PropertyDescriptor[] props = info.getPropertyDescriptors(); + + // iterate over class fields + for (int i = 0; i < props.length; ++i) { + PropertyDescriptor prop = props[i]; + String name = prop.getName(); + + if (elements.containsKey(name)) { + Object value = elements.get(name); + Method method = prop.getWriteMethod(); + + if (method != null) { + JSON json = method.getAnnotation(JSON.class); + if ((json != null) && !json.deserialize()) { + continue; + } + + // use only public setters + if (Modifier.isPublic(method.getModifiers())) { + Class[] paramTypes = method.getParameterTypes(); + Type[] genericTypes = method.getGenericParameterTypes(); + + if (paramTypes.length == 1) { + Object convertedValue = this.convert(paramTypes[0], genericTypes[0], value, + method); + method.invoke(object, new Object[] { convertedValue }); + } + } + } + } + } + } + + @SuppressWarnings("unchecked") + public Object convert(Class clazz, Type type, Object value, Method method) + throws IllegalArgumentException, JSONException, IllegalAccessException, + InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException { + + if (value == null) { + // if it is a java primitive then get a default value, otherwise + // leave it as null + return clazz.isPrimitive() ? convertPrimitive(clazz, value, method) : null; + } else if (isJSONPrimitive(clazz)) + return convertPrimitive(clazz, value, method); + else if (Collection.class.isAssignableFrom(clazz)) + return convertToCollection(clazz, type, value, method); + else if (Map.class.isAssignableFrom(clazz)) + return convertToMap(clazz, type, value, method); + else if (clazz.isArray()) + return convertToArray(clazz, type, value, method); + else if (value instanceof Map) { + // nested bean + Object convertedValue = clazz.newInstance(); + this.populateObject(convertedValue, (Map) value); + return convertedValue; + } else if (BigDecimal.class.equals(clazz)) { + return new BigDecimal(value != null ? value.toString() : "0"); + } else if (BigInteger.class.equals(clazz)) { + return new BigInteger(value != null ? value.toString() : "0"); + } else + throw new JSONException("Incompatible types for property " + method.getName()); + } + + private static boolean isJSONPrimitive(Class clazz) { + return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Date.class) + || clazz.equals(Boolean.class) || clazz.equals(Byte.class) || clazz.equals(Character.class) + || clazz.equals(Double.class) || clazz.equals(Float.class) || clazz.equals(Integer.class) + || clazz.equals(Long.class) || clazz.equals(Short.class) || clazz.equals(Locale.class) + || clazz.isEnum(); + } + + @SuppressWarnings("unchecked") + private Object convertToArray(Class clazz, Type type, Object value, Method accessor) + throws JSONException, IllegalArgumentException, IllegalAccessException, + InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException { + if (value == null) + return null; + else if (value instanceof List) { + Class arrayType = clazz.getComponentType(); + List values = (List) value; + Object newArray = Array.newInstance(arrayType, values.size()); + + // create an object for each element + for (int j = 0; j < values.size(); j++) { + Object listValue = values.get(j); + + if (arrayType.equals(Object.class)) { + // Object[] + Array.set(newArray, j, listValue); + } else if (isJSONPrimitive(arrayType)) { + // primitive array + Array.set(newArray, j, this.convertPrimitive(arrayType, listValue, accessor)); + } else if (listValue instanceof Map) { + // array of other class + Object newObject = null; + if (Map.class.isAssignableFrom(arrayType)) { + newObject = convertToMap(arrayType, type, listValue, accessor); + } else if (List.class.isAssignableFrom(arrayType)) { + newObject = convertToCollection(arrayType, type, listValue, accessor); + } else { + newObject = arrayType.newInstance(); + this.populateObject(newObject, (Map) listValue); + } + + Array.set(newArray, j, newObject); + } else + throw new JSONException("Incompatible types for property " + accessor.getName()); + } + + return newArray; + } else + throw new JSONException("Incompatible types for property " + accessor.getName()); + } + + @SuppressWarnings("unchecked") + private Object convertToCollection(Class clazz, Type type, Object value, Method accessor) + throws JSONException, IllegalArgumentException, IllegalAccessException, + InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException { + if (value == null) + return null; + else if (value instanceof List) { + Class itemClass = Object.class; + Type itemType = null; + if ((type != null) && (type instanceof ParameterizedType)) { + ParameterizedType ptype = (ParameterizedType) type; + itemType = ptype.getActualTypeArguments()[0]; + if (itemType.getClass().equals(Class.class)) { + itemClass = (Class) itemType; + } else { + itemClass = (Class) ((ParameterizedType) itemType).getRawType(); + } + } + List values = (List) value; + + Collection newCollection = null; + try { + newCollection = (Collection) clazz.newInstance(); + } catch (InstantiationException ex) { + // fallback if clazz represents an interface or abstract class + if (Set.class.isAssignableFrom(clazz)) { + newCollection = new HashSet(); + } else { + newCollection = new ArrayList(); + } + } + + // create an object for each element + for (int j = 0; j < values.size(); j++) { + Object listValue = values.get(j); + + if (itemClass.equals(Object.class)) { + // Object[] + newCollection.add(listValue); + } else if (isJSONPrimitive(itemClass)) { + // primitive array + newCollection.add(this.convertPrimitive(itemClass, listValue, accessor)); + } else if (Map.class.isAssignableFrom(itemClass)) { + Object newObject = convertToMap(itemClass, itemType, listValue, accessor); + newCollection.add(newObject); + } else if (List.class.isAssignableFrom(itemClass)) { + Object newObject = convertToCollection(itemClass, itemType, listValue, accessor); + newCollection.add(newObject); + } else if (listValue instanceof Map) { + // array of beans + Object newObject = itemClass.newInstance(); + this.populateObject(newObject, (Map) listValue); + newCollection.add(newObject); + } else + throw new JSONException("Incompatible types for property " + accessor.getName()); + } + + return newCollection; + } else + throw new JSONException("Incompatible types for property " + accessor.getName()); + } + + @SuppressWarnings("unchecked") + private Object convertToMap(Class clazz, Type type, Object value, Method accessor) throws JSONException, + IllegalArgumentException, IllegalAccessException, InvocationTargetException, + InstantiationException, NoSuchMethodException, IntrospectionException { + if (value == null) + return null; + else if (value instanceof Map) { + Class itemClass = Object.class; + Type itemType = null; + if ((type != null) && (type instanceof ParameterizedType)) { + ParameterizedType ptype = (ParameterizedType) type; + itemType = ptype.getActualTypeArguments()[1]; + if (itemType.getClass().equals(Class.class)) { + itemClass = (Class) itemType; + } else { + itemClass = (Class) ((ParameterizedType) itemType).getRawType(); + } + } + Map values = (Map) value; + + Map newMap = null; + try { + newMap = (Map) clazz.newInstance(); + } catch (InstantiationException ex) { + // fallback if clazz represents an interface or abstract class + newMap = new HashMap(); + } + + // create an object for each element + Iterator iter = values.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = (Map.Entry) iter.next(); + String key = (String) entry.getKey(); + Object v = entry.getValue(); + + if (itemClass.equals(Object.class)) { + // String, Object + newMap.put(key, v); + } else if (isJSONPrimitive(itemClass)) { + // primitive map + newMap.put(key, this.convertPrimitive(itemClass, v, accessor)); + } else if (Map.class.isAssignableFrom(itemClass)) { + Object newObject = convertToMap(itemClass, itemType, v, accessor); + newMap.put(key, newObject); + } else if (List.class.isAssignableFrom(itemClass)) { + Object newObject = convertToCollection(itemClass, itemType, v, accessor); + newMap.put(key, newObject); + } else if (v instanceof Map) { + // map of beans + Object newObject = itemClass.newInstance(); + this.populateObject(newObject, (Map) v); + newMap.put(key, newObject); + } else + throw new JSONException("Incompatible types for property " + accessor.getName()); + } + + return newMap; + } else + throw new JSONException("Incompatible types for property " + accessor.getName()); + } + + /** + * Converts numbers to the desired class, if possible + * + * @throws JSONException + */ + @SuppressWarnings("unchecked") + private Object convertPrimitive(Class clazz, Object value, Method method) throws JSONException { + if (value == null) { + if (Short.TYPE.equals(clazz) || Short.class.equals(clazz)) + return (short) 0; + else if (Byte.TYPE.equals(clazz) || Byte.class.equals(clazz)) + return (byte) 0; + else if (Integer.TYPE.equals(clazz) || Integer.class.equals(clazz)) + return 0; + else if (Long.TYPE.equals(clazz) || Long.class.equals(clazz)) + return 0L; + else if (Float.TYPE.equals(clazz) || Float.class.equals(clazz)) + return 0f; + else if (Double.TYPE.equals(clazz) || Double.class.equals(clazz)) + return 0d; + else if (Boolean.TYPE.equals(clazz) || Boolean.class.equals(clazz)) + return Boolean.FALSE; + else + return null; + } else if (value instanceof Number) { + Number number = (Number) value; + + if (Short.TYPE.equals(clazz)) + return number.shortValue(); + else if (Short.class.equals(clazz)) + return new Short(number.shortValue()); + else if (Byte.TYPE.equals(clazz)) + return number.byteValue(); + else if (Byte.class.equals(clazz)) + return new Byte(number.byteValue()); + else if (Integer.TYPE.equals(clazz)) + return number.intValue(); + else if (Integer.class.equals(clazz)) + return new Integer(number.intValue()); + else if (Long.TYPE.equals(clazz)) + return number.longValue(); + else if (Long.class.equals(clazz)) + return new Long(number.longValue()); + else if (Float.TYPE.equals(clazz)) + return number.floatValue(); + else if (Float.class.equals(clazz)) + return new Float(number.floatValue()); + else if (Double.TYPE.equals(clazz)) + return number.doubleValue(); + else if (Double.class.equals(clazz)) + return new Double(number.doubleValue()); + else if (String.class.equals(clazz)) + return value.toString(); + } else if (clazz.equals(Date.class)) { + try { + JSON json = method.getAnnotation(JSON.class); + + DateFormat formatter = new SimpleDateFormat( + (json != null) && (json.format().length() > 0) ? json.format() : this.dateFormat); + return formatter.parse((String) value); + } catch (ParseException e) { + LOG.error(e.getMessage(), e); + throw new JSONException("Unable to parse date from: " + value); + } + } else if (clazz.isEnum()) { + String sValue = (String) value; + return Enum.valueOf(clazz, sValue); + } else if (value instanceof String) { + String sValue = (String) value; + if (Boolean.TYPE.equals(clazz)) + return Boolean.parseBoolean(sValue); + else if (Boolean.class.equals(clazz)) + return Boolean.valueOf(sValue); + else if (Short.TYPE.equals(clazz)) + return Short.parseShort(sValue); + else if (Short.class.equals(clazz)) + return Short.valueOf(sValue); + else if (Byte.TYPE.equals(clazz)) + return Byte.parseByte(sValue); + else if (Byte.class.equals(clazz)) + return Byte.valueOf(sValue); + else if (Integer.TYPE.equals(clazz)) + return Integer.parseInt(sValue); + else if (Integer.class.equals(clazz)) + return Integer.valueOf(sValue); + else if (Long.TYPE.equals(clazz)) + return Long.parseLong(sValue); + else if (Long.class.equals(clazz)) + return Long.valueOf(sValue); + else if (Float.TYPE.equals(clazz)) + return Float.parseFloat(sValue); + else if (Float.class.equals(clazz)) + return Float.valueOf(sValue); + else if (Double.TYPE.equals(clazz)) + return Double.parseDouble(sValue); + else if (Double.class.equals(clazz)) + return Double.valueOf(sValue); + else if (Character.TYPE.equals(clazz) || Character.class.equals(clazz)) { + char charValue = 0; + if (sValue.length() > 0) { + charValue = sValue.charAt(0); + } + if (Character.TYPE.equals(clazz)) + return charValue; + else + return new Character(charValue); + } else if (clazz.equals(Locale.class)) { + String[] components = sValue.split("_", 2); + if (components.length == 2) { + return new Locale(components[0], components[1]); + } else { + return new Locale(sValue); + } + } else if (Enum.class.isAssignableFrom(clazz)) { + return Enum.valueOf(clazz, sValue); + } + } + + return value; + } + +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java new file mode 100644 index 000000000..11a358759 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java @@ -0,0 +1,289 @@ +/* + * $Id$ + * + * 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.json; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

+ * Deserializes and object from a JSON string + *

+ */ +class JSONReader { + private static final Object OBJECT_END = new Object(); + private static final Object ARRAY_END = new Object(); + private static final Object COLON = new Object(); + private static final Object COMMA = new Object(); + private static Map escapes = new HashMap(); + + static { + escapes.put(new Character('"'), new Character('"')); + escapes.put(new Character('\\'), new Character('\\')); + escapes.put(new Character('/'), new Character('/')); + escapes.put(new Character('b'), new Character('\b')); + escapes.put(new Character('f'), new Character('\f')); + escapes.put(new Character('n'), new Character('\n')); + escapes.put(new Character('r'), new Character('\r')); + escapes.put(new Character('t'), new Character('\t')); + } + + private CharacterIterator it; + private char c; + private Object token; + private StringBuilder buf = new StringBuilder(); + + private char next() { + this.c = this.it.next(); + + return this.c; + } + + private void skipWhiteSpace() { + while (Character.isWhitespace(this.c)) { + this.next(); + } + } + + public Object read(String string) throws JSONException { + this.it = new StringCharacterIterator(string); + this.c = this.it.first(); + + return this.read(); + } + + private Object read() throws JSONException { + Object ret = null; + + this.skipWhiteSpace(); + + if (this.c == '"') { + this.next(); + ret = this.string('"'); + } else if (this.c == '\'') { + this.next(); + ret = this.string('\''); + } else if (this.c == '[') { + this.next(); + ret = this.array(); + } else if (this.c == ']') { + ret = ARRAY_END; + this.next(); + } else if (this.c == ',') { + ret = COMMA; + this.next(); + } else if (this.c == '{') { + this.next(); + ret = this.object(); + } else if (this.c == '}') { + ret = OBJECT_END; + this.next(); + } else if (this.c == ':') { + ret = COLON; + this.next(); + } else if ((this.c == 't') && (this.next() == 'r') && (this.next() == 'u') && (this.next() == 'e')) { + ret = Boolean.TRUE; + this.next(); + } else if ((this.c == 'f') && (this.next() == 'a') && (this.next() == 'l') && (this.next() == 's') + && (this.next() == 'e')) { + ret = Boolean.FALSE; + this.next(); + } else if ((this.c == 'n') && (this.next() == 'u') && (this.next() == 'l') && (this.next() == 'l')) { + ret = null; + this.next(); + } else if (Character.isDigit(this.c) || (this.c == '-')) { + ret = this.number(); + } else { + throw buildInvalidInputException(); + } + + this.token = ret; + + return ret; + } + + @SuppressWarnings("unchecked") + private Map object() throws JSONException { + Map ret = new HashMap(); + Object next = this.read(); + if (next != OBJECT_END) { + String key = (String) next; + while (this.token != OBJECT_END) { + this.read(); // should be a colon + + if (this.token != OBJECT_END) { + ret.put(key, this.read()); + + if (this.read() == COMMA) { + Object name = this.read(); + + if (name instanceof String) { + key = (String) name; + } else + throw buildInvalidInputException(); + } + } + } + } + + return ret; + } + + private JSONException buildInvalidInputException() { + return new JSONException("Input string is not well formed JSON (invalid char " + this.c + ")"); + } + + @SuppressWarnings("unchecked") + private List array() throws JSONException { + List ret = new ArrayList(); + Object value = this.read(); + + while (this.token != ARRAY_END) { + ret.add(value); + + Object read = this.read(); + if (read == COMMA) { + value = this.read(); + } else if (read != ARRAY_END) { + throw buildInvalidInputException(); + } + } + + return ret; + } + + private Object number() { + this.buf.setLength(0); + + if (this.c == '-') { + this.add(); + } + + this.addDigits(); + + if (this.c == '.') { + this.add(); + this.addDigits(); + } + + if ((this.c == 'e') || (this.c == 'E')) { + this.add(); + + if ((this.c == '+') || (this.c == '-')) { + this.add(); + } + + this.addDigits(); + } + + return (this.buf.indexOf(".") >= 0) ? (Object) Double.parseDouble(this.buf.toString()) + : (Object) Long.parseLong(this.buf.toString()); + } + + private Object string(char quote) { + this.buf.setLength(0); + + while ((this.c != quote) && (this.c != CharacterIterator.DONE)) { + if (this.c == '\\') { + this.next(); + + if (this.c == 'u') { + this.add(this.unicode()); + } else { + Object value = escapes.get(new Character(this.c)); + + if (value != null) { + this.add(((Character) value).charValue()); + } + } + } else { + this.add(); + } + } + + this.next(); + + return this.buf.toString(); + } + + private void add(char cc) { + this.buf.append(cc); + this.next(); + } + + private void add() { + this.add(this.c); + } + + private void addDigits() { + while (Character.isDigit(this.c)) { + this.add(); + } + } + + private char unicode() { + int value = 0; + + for (int i = 0; i < 4; ++i) { + switch (this.next()) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + value = (value << 4) + (this.c - '0'); + + break; + + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + value = (value << 4) + (this.c - 'W'); + + break; + + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + value = (value << 4) + (this.c - '7'); + + break; + } + } + + return (char) value; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONResult.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONResult.java new file mode 100644 index 000000000..7edf94b1a --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONResult.java @@ -0,0 +1,522 @@ +/* + * $Id$ + * + * 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.json; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.StrutsStatics; +import org.apache.struts2.json.annotations.SMD; +import org.apache.struts2.json.annotations.SMDMethod; +import org.apache.struts2.json.annotations.SMDMethodParameter; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.Result; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.ValueStack; +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +/** + *

This result serializes an action + * into JSON.

Result + * parameters:

+ *

    + *

    + *

  • excludeProperties - list of regular expressions matching the properties + * to be excluded. The regular expressions are evaluated against the OGNL + * expression representation of the properties.
  • + *

    + *

+ *

Example:

+ * + *

+ * <!-- START SNIPPET: example -->
+ * <result name="success" type="json" />
+ * <!-- END SNIPPET: example -->
+ * 
+ */ +public class JSONResult implements Result { + private static final long serialVersionUID = 8624350183189931165L; + private static final Logger LOG = LoggerFactory.getLogger(JSONResult.class); + + private String defaultEncoding = "ISO-8859-1"; + private List includeProperties; + private List excludeProperties; + private String root; + private boolean wrapWithComments; + private boolean prefix; + private boolean enableSMD = false; + private boolean enableGZIP = false; + private boolean ignoreHierarchy = true; + private boolean ignoreInterfaces = true; + private boolean enumAsBean = JSONWriter.ENUM_AS_BEAN_DEFAULT; + private boolean noCache = false; + private boolean excludeNullProperties = false; + private int statusCode; + private int errorCode; + private String callbackParameter; + private String contentType; + private String wrapPrefix; + private String wrapSuffix; + + @Inject(StrutsConstants.STRUTS_I18N_ENCODING) + public void setDefaultEncoding(String val) { + this.defaultEncoding = val; + } + + /** + * Gets a list of regular expressions of properties to exclude from the JSON + * output. + * + * @return A list of compiled regular expression patterns + */ + public List getExcludePropertiesList() { + return this.excludeProperties; + } + + /** + * Sets a comma-delimited list of regular expressions to match properties + * that should be excluded from the JSON output. + * + * @param commaDelim + * A comma-delimited list of regular expressions + */ + public void setExcludeProperties(String commaDelim) { + List excludePatterns = JSONUtil.asList(commaDelim); + if (excludePatterns != null) { + this.excludeProperties = new ArrayList(excludePatterns.size()); + for (String pattern : excludePatterns) { + this.excludeProperties.add(Pattern.compile(pattern)); + } + } + } + + /** + * @return the includeProperties + */ + public List getIncludePropertiesList() { + return includeProperties; + } + + /** + * @param includedProperties + * the includeProperties to set + */ + public void setIncludeProperties(String commaDelim) { + List includePatterns = JSONUtil.asList(commaDelim); + if (includePatterns != null) { + this.includeProperties = new ArrayList(includePatterns.size()); + + HashMap existingPatterns = new HashMap(); + + for (String pattern : includePatterns) { + // Compile a pattern for each *unique* "level" of the object + // hierarchy specified in the regex. + String[] patternPieces = pattern.split("\\\\\\."); + + String patternExpr = ""; + for (String patternPiece : patternPieces) { + if (patternExpr.length() > 0) { + patternExpr += "\\."; + } + patternExpr += patternPiece; + + // Check for duplicate patterns so that there is no overlap. + if (!existingPatterns.containsKey(patternExpr)) { + existingPatterns.put(patternExpr, patternExpr); + + // Add a pattern that does not have the indexed property + // matching (ie. list\[\d+\] becomes list). + if (patternPiece.endsWith("\\]")) { + this.includeProperties.add(Pattern.compile(patternExpr.substring(0, patternPiece + .lastIndexOf("\\[")))); + + if (LOG.isDebugEnabled()) + LOG.debug("Adding include property expression: " + + patternExpr.substring(0, patternPiece.lastIndexOf("\\["))); + } + + this.includeProperties.add(Pattern.compile(patternExpr)); + + if (LOG.isDebugEnabled()) + LOG.debug("Adding include property expression: " + patternExpr); + } + } + } + } + } + + public void execute(ActionInvocation invocation) throws Exception { + ActionContext actionContext = invocation.getInvocationContext(); + HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST); + HttpServletResponse response = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE); + + try { + String json; + Object rootObject; + if (this.enableSMD) { + // generate SMD + rootObject = this.writeSMD(invocation); + } else { + // generate JSON + if (this.root != null) { + ValueStack stack = invocation.getStack(); + rootObject = stack.findValue(this.root); + } else { + rootObject = invocation.getAction(); + } + } + json = JSONUtil.serialize(rootObject, excludeProperties, includeProperties, ignoreHierarchy, + enumAsBean, excludeNullProperties); + json = addCallbackIfApplicable(request, json); + + boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request); + + writeToResponse(response, json, writeGzip); + + } catch (IOException exception) { + LOG.error(exception.getMessage(), exception); + throw exception; + } + } + + protected void writeToResponse(HttpServletResponse response, String json, boolean gzip) + throws IOException { + JSONUtil.writeJSONToResponse(new SerializationParams(response, getEncoding(), isWrapWithComments(), + json, false, gzip, noCache, statusCode, errorCode, prefix, contentType, wrapPrefix, + wrapSuffix)); + } + + @SuppressWarnings("unchecked") + protected org.apache.struts2.json.smd.SMD writeSMD(ActionInvocation invocation) { + ActionContext actionContext = invocation.getInvocationContext(); + HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST); + + // root is based on OGNL expression (action by default) + Object rootObject = null; + if (this.root != null) { + ValueStack stack = invocation.getStack(); + rootObject = stack.findValue(this.root); + } else { + rootObject = invocation.getAction(); + } + + Class clazz = rootObject.getClass(); + org.apache.struts2.json.smd.SMD smd = new org.apache.struts2.json.smd.SMD(); + // URL + smd.setServiceUrl(request.getRequestURI()); + + // customize SMD + SMD smdAnnotation = (SMD) clazz.getAnnotation(SMD.class); + if (smdAnnotation != null) { + smd.setObjectName(smdAnnotation.objectName()); + smd.setServiceType(smdAnnotation.serviceType()); + smd.setVersion(smdAnnotation.version()); + } + + // get public methods + Method[] methods = JSONUtil.listSMDMethods(clazz, ignoreInterfaces); + + for (Method method : methods) { + SMDMethod smdMethodAnnotation = method.getAnnotation(SMDMethod.class); + + // SMDMethod annotation is required + if (((smdMethodAnnotation != null) && !this.shouldExcludeProperty(method.getName()))) { + String methodName = smdMethodAnnotation.name().length() == 0 ? method.getName() + : smdMethodAnnotation.name(); + + org.apache.struts2.json.smd.SMDMethod smdMethod = new org.apache.struts2.json.smd.SMDMethod( + methodName); + smd.addSMDMethod(smdMethod); + + // find params for this method + int parametersCount = method.getParameterTypes().length; + if (parametersCount > 0) { + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + + for (int i = 0; i < parametersCount; i++) { + // are you ever going to pick shorter names? nope + SMDMethodParameter smdMethodParameterAnnotation = this + .getSMDMethodParameterAnnotation(parameterAnnotations[i]); + + String paramName = smdMethodParameterAnnotation != null ? smdMethodParameterAnnotation + .name() + : "p" + i; + + // goog thing this is the end of the hierarchy, + // oitherwise I would need that 21'' LCD ;) + smdMethod.addSMDMethodParameter(new org.apache.struts2.json.smd.SMDMethodParameter( + paramName)); + } + } + + } else { + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring property " + method.getName()); + } + } + return smd; + } + + /** + * Find an SMDethodParameter annotation on this array + */ + private org.apache.struts2.json.annotations.SMDMethodParameter getSMDMethodParameterAnnotation( + Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation instanceof org.apache.struts2.json.annotations.SMDMethodParameter) + return (org.apache.struts2.json.annotations.SMDMethodParameter) annotation; + } + + return null; + } + + private boolean shouldExcludeProperty(String expr) { + if (this.excludeProperties != null) { + for (Pattern pattern : this.excludeProperties) { + if (pattern.matcher(expr).matches()) + return true; + } + } + return false; + } + + /** + * Retrieve the encoding

+ * + * @return The encoding associated with this template (defaults to the value + * of 'struts.i18n.encoding' property) + */ + protected String getEncoding() { + String encoding = this.defaultEncoding; + + if (encoding == null) { + encoding = System.getProperty("file.encoding"); + } + + if (encoding == null) { + encoding = "UTF-8"; + } + + return encoding; + } + + protected String addCallbackIfApplicable(HttpServletRequest request, String json) { + if ((callbackParameter != null) && (callbackParameter.length() > 0)) { + String callbackName = request.getParameter(callbackParameter); + if ((callbackName != null) && (callbackName.length() > 0)) + json = callbackName + "(" + json + ")"; + } + return json; + } + + /** + * @return OGNL expression of root object to be serialized + */ + public String getRoot() { + return this.root; + } + + /** + * Sets the root object to be serialized, defaults to the Action + * + * @param root + * OGNL expression of root object to be serialized + */ + public void setRoot(String root) { + this.root = root; + } + + /** + * @return Generated JSON must be enclosed in comments + */ + public boolean isWrapWithComments() { + return this.wrapWithComments; + } + + /** + * Wrap generated JSON with comments + * + * @param wrapWithComments + */ + public void setWrapWithComments(boolean wrapWithComments) { + this.wrapWithComments = wrapWithComments; + } + + /** + * @return Result has SMD generation enabled + */ + public boolean isEnableSMD() { + return this.enableSMD; + } + + /** + * Enable SMD generation for action, which can be used for JSON-RPC + * + * @param enableSMD + */ + public void setEnableSMD(boolean enableSMD) { + this.enableSMD = enableSMD; + } + + public void setIgnoreHierarchy(boolean ignoreHierarchy) { + this.ignoreHierarchy = ignoreHierarchy; + } + + /** + * Controls whether interfaces should be inspected for method annotations + * You may need to set to this true if your action is a proxy as annotations + * on methods are not inherited + */ + public void setIgnoreInterfaces(boolean ignoreInterfaces) { + this.ignoreInterfaces = ignoreInterfaces; + } + + /** + * Controls how Enum's are serialized : If true, an Enum is serialized as a + * name=value pair (name=name()) (default) If false, an Enum is serialized + * as a bean with a special property _name=name() + * + * @param enumAsBean + */ + public void setEnumAsBean(boolean enumAsBean) { + this.enumAsBean = enumAsBean; + } + + public boolean isEnumAsBean() { + return enumAsBean; + } + + public boolean isEnableGZIP() { + return enableGZIP; + } + + public void setEnableGZIP(boolean enableGZIP) { + this.enableGZIP = enableGZIP; + } + + public boolean isNoCache() { + return noCache; + } + + /** + * Add headers to response to prevent the browser from caching the response + * + * @param noCache + */ + public void setNoCache(boolean noCache) { + this.noCache = noCache; + } + + public boolean isIgnoreHierarchy() { + return ignoreHierarchy; + } + + public boolean isExcludeNullProperties() { + return excludeNullProperties; + } + + /** + * Do not serialize properties with a null value + * + * @param excludeNullProperties + */ + public void setExcludeNullProperties(boolean excludeNullProperties) { + this.excludeNullProperties = excludeNullProperties; + } + + /** + * Status code to be set in the response + * + * @param statusCode + */ + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + /** + * Error code to be set in the response + * + * @param errorCode + */ + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public void setCallbackParameter(String callbackParameter) { + this.callbackParameter = callbackParameter; + } + + public String getCallbackParameter() { + return callbackParameter; + } + + /** + * Prefix JSON with "{} &&" + * + * @param prefix + */ + public void setPrefix(boolean prefix) { + this.prefix = prefix; + } + + /** + * Content type to be set in the response + * + * @param contentType + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getWrapPrefix() { + return wrapPrefix; + } + + /** + * Text to be inserted at the begining of the response + */ + public void setWrapPrefix(String wrapPrefix) { + this.wrapPrefix = wrapPrefix; + } + + public String getWrapSuffix() { + return wrapSuffix; + } + + /** + * Text to be inserted at the end of the response + */ + public void setWrapSuffix(String wrapSuffix) { + this.wrapSuffix = wrapSuffix; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java new file mode 100644 index 000000000..806aa6dad --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java @@ -0,0 +1,395 @@ +/* + * $Id$ + * + * 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.json; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.zip.GZIPOutputStream; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.xwork.StringUtils; +import org.apache.struts2.json.annotations.SMDMethod; + +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +/** + * Wrapper for JSONWriter with some utility methods. + */ +public class JSONUtil { + final static String RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + private static final Logger LOG = LoggerFactory.getLogger(JSONUtil.class); + + /** + * Serializes an object into JSON. + * + * @param object + * to be serialized + * @return JSON string + * @throws JSONException + */ + public static String serialize(Object object) throws JSONException { + JSONWriter writer = new JSONWriter(); + + return writer.write(object); + } + + /** + * Serializes an object into JSON, excluding any properties matching any of + * the regular expressions in the given collection. + * + * @param object + * to be serialized + * @param excludeProperties + * Patterns matching properties to exclude + * @param ignoreHierarchy + * whether to ignore properties defined on base classes of the + * root object + * @return JSON string + * @throws JSONException + */ + public static String serialize(Object object, Collection excludeProperties, + Collection includeProperties, boolean ignoreHierarchy, boolean excludeNullProperties) + throws JSONException { + JSONWriter writer = new JSONWriter(); + writer.setIgnoreHierarchy(ignoreHierarchy); + return writer.write(object, excludeProperties, includeProperties, excludeNullProperties); + } + + /** + * Serializes an object into JSON, excluding any properties matching any of + * the regular expressions in the given collection. + * + * @param object + * to be serialized + * @param excludeProperties + * Patterns matching properties to exclude + * @param ignoreHierarchy + * whether to ignore properties defined on base classes of the + * root object + * @param enumAsBean + * whether to serialized enums a Bean or name=value pair + * @return JSON string + * @throws JSONException + */ + public static String serialize(Object object, Collection excludeProperties, + Collection includeProperties, boolean ignoreHierarchy, boolean enumAsBean, + boolean excludeNullProperties) throws JSONException { + JSONWriter writer = new JSONWriter(); + writer.setIgnoreHierarchy(ignoreHierarchy); + writer.setEnumAsBean(enumAsBean); + return writer.write(object, excludeProperties, includeProperties, excludeNullProperties); + } + + /** + * Serializes an object into JSON to the given writer. + * + * @param writer + * Writer to serialize the object to + * @param object + * object to be serialized + * @throws IOException + * @throws JSONException + */ + public static void serialize(Writer writer, Object object) throws IOException, JSONException { + writer.write(serialize(object)); + } + + /** + * Serializes an object into JSON to the given writer, excluding any + * properties matching any of the regular expressions in the given + * collection. + * + * @param writer + * Writer to serialize the object to + * @param object + * object to be serialized + * @param excludeProperties + * Patterns matching properties to ignore + * @throws IOException + * @throws JSONException + */ + public static void serialize(Writer writer, Object object, Collection excludeProperties, + Collection includeProperties, boolean excludeNullProperties) throws IOException, + JSONException { + writer.write(serialize(object, excludeProperties, includeProperties, true, excludeNullProperties)); + } + + /** + * Deserializes a object from JSON + * + * @param json + * string in JSON + * @return desrialized object + * @throws JSONException + */ + public static Object deserialize(String json) throws JSONException { + JSONReader reader = new JSONReader(); + return reader.read(json); + } + + /** + * Deserializes a object from JSON + * + * @param reader + * Reader to read a JSON string from + * @return deserialized object + * @throws JSONException + * when IOException happens + */ + public static Object deserialize(Reader reader) throws JSONException { + // read content + BufferedReader bufferReader = new BufferedReader(reader); + String line = null; + StringBuilder buffer = new StringBuilder(); + + try { + while ((line = bufferReader.readLine()) != null) { + buffer.append(line); + } + } catch (IOException e) { + throw new JSONException(e); + } + + return deserialize(buffer.toString()); + } + + public static void writeJSONToResponse(SerializationParams serializationParams) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + if (StringUtils.isNotBlank(serializationParams.getSerializedJSON())) + stringBuilder.append(serializationParams.getSerializedJSON()); + + if (StringUtils.isNotBlank(serializationParams.getWrapPrefix())) + stringBuilder.insert(0, serializationParams.getWrapPrefix()); + else if (serializationParams.isWrapWithComments()) { + stringBuilder.insert(0, "/* "); + stringBuilder.append(" */"); + } else if (serializationParams.isPrefix()) + stringBuilder.insert(0, "{}&& "); + + if (StringUtils.isNotBlank(serializationParams.getWrapSuffix())) + stringBuilder.append(serializationParams.getWrapSuffix()); + + String json = stringBuilder.toString(); + + if (LOG.isDebugEnabled()) { + LOG.debug("[JSON]" + json); + } + + HttpServletResponse response = serializationParams.getResponse(); + + // status or error code + if (serializationParams.getStatusCode() > 0) + response.setStatus(serializationParams.getStatusCode()); + else if (serializationParams.getErrorCode() > 0) + response.sendError(serializationParams.getErrorCode()); + + // content type + if (serializationParams.isSmd()) + response.setContentType("application/json-rpc;charset=" + serializationParams.getEncoding()); + else + response.setContentType(serializationParams.getContentType() + ";charset=" + + serializationParams.getEncoding()); + + if (serializationParams.isNoCache()) { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); + response.setHeader("Pragma", "No-cache"); + } + + if (serializationParams.isGzip()) { + response.addHeader("Content-Encoding", "gzip"); + GZIPOutputStream out = null; + InputStream in = null; + try { + out = new GZIPOutputStream(response.getOutputStream()); + in = new ByteArrayInputStream(json.getBytes()); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } finally { + if (in != null) + in.close(); + if (out != null) { + out.finish(); + out.close(); + } + } + + } else { + response.setContentLength(json.getBytes(serializationParams.getEncoding()).length); + PrintWriter out = response.getWriter(); + out.print(json); + } + } + + public static List asList(String commaDelim) { + if ((commaDelim == null) || (commaDelim.trim().length() == 0)) + return null; + List list = new ArrayList(); + String[] split = commaDelim.split(","); + for (int i = 0; i < split.length; i++) { + String trimmed = split[i].trim(); + if (trimmed.length() > 0) { + list.add(trimmed); + } + } + return list; + } + + /** + * List visible methods carrying the + * + * @SMDMethod annotation + * + * @param ignoreInterfaces + * if true, only the methods of the class are examined. If false, + * annotations on every interfaces' methods are examined. + */ + @SuppressWarnings("unchecked") + public static Method[] listSMDMethods(Class clazz, boolean ignoreInterfaces) { + final List methods = new LinkedList(); + if (ignoreInterfaces) { + for (Method method : clazz.getMethods()) { + SMDMethod smdMethodAnnotation = method.getAnnotation(SMDMethod.class); + if (smdMethodAnnotation != null) { + methods.add(method); + } + } + } else { + // recurse the entire superclass/interface hierarchy and add in + // order encountered + JSONUtil.visitInterfaces(clazz, new JSONUtil.ClassVisitor() { + public boolean visit(Class aClass) { + for (Method method : aClass.getMethods()) { + SMDMethod smdMethodAnnotation = method.getAnnotation(SMDMethod.class); + if ((smdMethodAnnotation != null) && !methods.contains(method)) { + methods.add(method); + } + } + return true; + } + }); + } + + Method[] methodResult = new Method[methods.size()]; + return methods.toArray(methodResult); + } + + /** + * Realizes the visit(Class) method called by vistInterfaces for all + * encountered classes/interfaces + */ + public static interface ClassVisitor { + + /** + * Called when a new interface/class is encountered + * + * @param aClass + * the encountered class/interface + * @return true if the recursion should continue, false to stop + * recursion immediately + */ + @SuppressWarnings("unchecked") + boolean visit(Class aClass); + } + + /** + * Visit all the interfaces realized by the specified object, its + * superclasses and its interfaces

Visitation is performed in the + * following order: aClass aClass' interfaces the interface's superclasses + * (interfaces) aClass' superclass superclass' interfaces superclass' + * interface's superclasses (interfaces) super-superclass and so on

The + * Object base class is base excluded. Classes/interfaces are only visited + * once each + * + * @param aClass + * the class to start recursing upwards from + * @param visitor + * this vistor is called for each class/interface encountered + * @return true if all classes/interfaces were visited, false if it was + * exited early as specified by a ClassVisitor result + */ + @SuppressWarnings("unchecked") + public static boolean visitInterfaces(Class aClass, ClassVisitor visitor) { + List classesVisited = new LinkedList(); + return visitUniqueInterfaces(aClass, visitor, classesVisited); + } + + /** + * Recursive method to visit all the interfaces of a class (and its + * superclasses and super-interfaces) if they haven't already been visited. + *

Always visits itself if it hasn't already been visited + * + * @param thisClass + * the current class to visit (if not already done so) + * @param classesVisited + * classes already visited + * @param visitor + * this vistor is called for each class/interface encountered + * @return true if recursion can continue, false if it should be aborted + */ + private static boolean visitUniqueInterfaces(Class thisClass, ClassVisitor visitor, + List classesVisited) { + boolean okayToContinue = true; + + if (!classesVisited.contains(thisClass)) { + classesVisited.add(thisClass); + okayToContinue = visitor.visit(thisClass); + + if (okayToContinue) { + Class[] interfaces = thisClass.getInterfaces(); + int index = 0; + while ((index < interfaces.length) && (okayToContinue)) { + okayToContinue = visitUniqueInterfaces(interfaces[index++], visitor, classesVisited); + } + + if (okayToContinue) { + Class superClass = thisClass.getSuperclass(); + if ((superClass != null) && (!Object.class.equals(superClass))) { + okayToContinue = visitUniqueInterfaces(superClass, visitor, classesVisited); + } + } + } + } + return okayToContinue; + } + + public static boolean isGzipInRequest(HttpServletRequest request) { + String header = request.getHeader("Accept-Encoding"); + return (header != null) && (header.indexOf("gzip") >= 0); + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java new file mode 100644 index 000000000..c60367c3e --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONWriter.java @@ -0,0 +1,549 @@ +/* + * $Id$ + * + * 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.json; + +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.text.CharacterIterator; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.text.StringCharacterIterator; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Pattern; + +import org.apache.struts2.json.annotations.JSON; + +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +/** + *

+ * Serializes an object into JavaScript Object Notation (JSON). If cyclic + * references are detected they will be nulled out. + *

+ */ +class JSONWriter { + private static final Logger LOG = LoggerFactory.getLogger(JSONWriter.class); + + /** + * By default, enums are serialzied as name=value pairs + */ + public static final boolean ENUM_AS_BEAN_DEFAULT = false; + + static char[] hex = "0123456789ABCDEF".toCharArray(); + private StringBuilder buf = new StringBuilder(); + private Stack stack = new Stack(); + private boolean ignoreHierarchy = true; + private Object root; + private boolean buildExpr = true; + private String exprStack = ""; + private Collection excludeProperties; + private Collection includeProperties; + private DateFormat formatter; + private boolean enumAsBean = ENUM_AS_BEAN_DEFAULT; + private boolean excludeNullProperties; + + /** + * @param object + * Object to be serialized into JSON + * @return JSON string for object + * @throws JSONException + */ + public String write(Object object) throws JSONException { + return this.write(object, null, null, false); + } + + /** + * @param object + * Object to be serialized into JSON + * @return JSON string for object + * @throws JSONException + */ + public String write(Object object, Collection excludeProperties, + Collection includeProperties, boolean excludeNullProperties) throws JSONException { + this.excludeNullProperties = excludeNullProperties; + this.buf.setLength(0); + this.root = object; + this.exprStack = ""; + this.buildExpr = ((excludeProperties != null) && !excludeProperties.isEmpty()) + || ((includeProperties != null) && !includeProperties.isEmpty()); + this.excludeProperties = excludeProperties; + this.includeProperties = includeProperties; + this.value(object, null); + + return this.buf.toString(); + } + + /** + * Detect cyclic references + */ + private void value(Object object, Method method) throws JSONException { + if (object == null) { + this.add("null"); + + return; + } + + if (this.stack.contains(object)) { + Class clazz = object.getClass(); + + // cyclic reference + if (clazz.isPrimitive() || clazz.equals(String.class)) { + this.process(object, method); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Cyclic reference detected on " + object); + } + + this.add("null"); + } + + return; + } + + this.process(object, method); + } + + /** + * Serialize object into json + */ + private void process(Object object, Method method) throws JSONException { + this.stack.push(object); + + if (object instanceof Class) { + this.string(object); + } else if (object instanceof Boolean) { + this.bool(((Boolean) object).booleanValue()); + } else if (object instanceof Number) { + this.add(object); + } else if (object instanceof String) { + this.string(object); + } else if (object instanceof Character) { + this.string(object); + } else if (object instanceof Map) { + this.map((Map) object, method); + } else if (object.getClass().isArray()) { + this.array(object, method); + } else if (object instanceof Iterable) { + this.array(((Iterable) object).iterator(), method); + } else if (object instanceof Date) { + this.date((Date) object, method); + } else if (object instanceof Calendar) { + this.date(((Calendar) object).getTime(), method); + } else if (object instanceof Locale) { + this.string(object); + } else if (object instanceof Enum) { + this.enumeration((Enum) object); + } else { + this.bean(object); + } + + this.stack.pop(); + } + + /** + * Instrospect bean and serialize its properties + */ + private void bean(Object object) throws JSONException { + this.add("{"); + + BeanInfo info; + + try { + Class clazz = object.getClass(); + + info = ((object == this.root) && this.ignoreHierarchy) ? Introspector.getBeanInfo(clazz, clazz + .getSuperclass()) : Introspector.getBeanInfo(clazz); + + PropertyDescriptor[] props = info.getPropertyDescriptors(); + + boolean hasData = false; + for (int i = 0; i < props.length; ++i) { + PropertyDescriptor prop = props[i]; + String name = prop.getName(); + Method accessor = prop.getReadMethod(); + Method baseAccessor = null; + if (clazz.getName().indexOf("$$EnhancerByCGLIB$$") > -1) { + try { + baseAccessor = Class.forName( + clazz.getName().substring(0, clazz.getName().indexOf("$$"))).getMethod( + accessor.getName(), accessor.getParameterTypes()); + } catch (Exception ex) { + LOG.debug(ex.getMessage(), ex); + } + } else + baseAccessor = accessor; + + if (baseAccessor != null) { + + JSON json = baseAccessor.getAnnotation(JSON.class); + if (json != null) { + if (!json.serialize()) + continue; + else if (json.name().length() > 0) + name = json.name(); + } + + // ignore "class" and others + if (this.shouldExcludeProperty(clazz, prop)) { + continue; + } + String expr = null; + if (this.buildExpr) { + expr = this.expandExpr(name); + if (this.shouldExcludeProperty(expr)) { + continue; + } + expr = this.setExprStack(expr); + } + + Object value = accessor.invoke(object, new Object[0]); + boolean propertyPrinted = this.add(name, value, accessor, hasData); + hasData = hasData || propertyPrinted; + if (this.buildExpr) { + this.setExprStack(expr); + } + } + } + + // special-case handling for an Enumeration - include the name() as + // a property */ + if (object instanceof Enum) { + Object value = ((Enum) object).name(); + this.add("_name", value, object.getClass().getMethod("name"), hasData); + } + } catch (Exception e) { + throw new JSONException(e); + } + + this.add("}"); + } + + /** + * Instrospect an Enum and serialize it as a name/value pair or as a bean + * including all its own properties + */ + private void enumeration(Enum enumeration) throws JSONException { + if (enumAsBean) { + this.bean(enumeration); + } else { + this.string(enumeration.name()); + } + } + + /** + * Ignore "class" field + */ + private boolean shouldExcludeProperty(Class clazz, PropertyDescriptor prop) throws SecurityException, + NoSuchFieldException { + String name = prop.getName(); + + if (name.equals("class") || name.equals("declaringClass") || name.equals("cachedSuperClass") + || name.equals("metaClass")) { + return true; + } + + return false; + } + + private String expandExpr(int i) { + return this.exprStack + "[" + i + "]"; + } + + private String expandExpr(String property) { + if (this.exprStack.length() == 0) + return property; + return this.exprStack + "." + property; + } + + private String setExprStack(String expr) { + String s = this.exprStack; + this.exprStack = expr; + return s; + } + + private boolean shouldExcludeProperty(String expr) { + if (this.excludeProperties != null) { + for (Pattern pattern : this.excludeProperties) { + if (pattern.matcher(expr).matches()) { + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring property because of exclude rule: " + expr); + return true; + } + } + } + + if (this.includeProperties != null) { + for (Pattern pattern : this.includeProperties) { + if (pattern.matcher(expr).matches()) { + return false; + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring property because of include rule: " + expr); + return true; + } + + return false; + } + + /** + * Add name/value pair to buffer + */ + private boolean add(String name, Object value, Method method, boolean hasData) throws JSONException { + if (!excludeNullProperties || (value != null)) { + if (hasData) { + this.add(','); + } + this.add('"'); + this.add(name); + this.add("\":"); + this.value(value, method); + return true; + } + + return false; + } + + /** + * Add map to buffer + */ + private void map(Map map, Method method) throws JSONException { + this.add("{"); + + Iterator it = map.entrySet().iterator(); + + boolean warnedNonString = false; // one report per map + boolean hasData = false; + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + Object key = entry.getKey(); + String expr = null; + if (this.buildExpr) { + if (key == null) { + LOG.error("Cannot build expression for null key in " + this.exprStack); + continue; + } else { + expr = this.expandExpr(key.toString()); + if (this.shouldExcludeProperty(expr)) { + continue; + } + expr = this.setExprStack(expr); + } + } + if (hasData) { + this.add(','); + } + hasData = true; + if (!warnedNonString && !(key instanceof String)) { + LOG.warn("JavaScript doesn't support non-String keys, using toString() on " + + key.getClass().getName()); + warnedNonString = true; + } + this.value(key.toString(), method); + this.add(":"); + this.value(entry.getValue(), method); + if (this.buildExpr) { + this.setExprStack(expr); + } + } + + this.add("}"); + } + + /** + * Add date to buffer + */ + private void date(Date date, Method method) { + JSON json = null; + if (method != null) + json = method.getAnnotation(JSON.class); + if (this.formatter == null) + this.formatter = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT); + + DateFormat formatter = (json != null) && (json.format().length() > 0) ? new SimpleDateFormat(json + .format()) : this.formatter; + this.string(formatter.format(date)); + } + + /** + * Add array to buffer + */ + private void array(Iterator it, Method method) throws JSONException { + this.add("["); + + boolean hasData = false; + for (int i = 0; it.hasNext(); i++) { + String expr = null; + if (this.buildExpr) { + expr = this.expandExpr(i); + if (this.shouldExcludeProperty(expr)) { + it.next(); + continue; + } + expr = this.setExprStack(expr); + } + if (hasData) { + this.add(','); + } + hasData = true; + this.value(it.next(), method); + if (this.buildExpr) { + this.setExprStack(expr); + } + } + + this.add("]"); + } + + /** + * Add array to buffer + */ + private void array(Object object, Method method) throws JSONException { + this.add("["); + + int length = Array.getLength(object); + + boolean hasData = false; + for (int i = 0; i < length; ++i) { + String expr = null; + if (this.buildExpr) { + expr = this.expandExpr(i); + if (this.shouldExcludeProperty(expr)) { + continue; + } + expr = this.setExprStack(expr); + } + if (hasData) { + this.add(','); + } + hasData = true; + this.value(Array.get(object, i), method); + if (this.buildExpr) { + this.setExprStack(expr); + } + } + + this.add("]"); + } + + /** + * Add boolean to buffer + */ + private void bool(boolean b) { + this.add(b ? "true" : "false"); + } + + /** + * escape characters + */ + private void string(Object obj) { + this.add('"'); + + CharacterIterator it = new StringCharacterIterator(obj.toString()); + + for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) { + if (c == '"') { + this.add("\\\""); + } else if (c == '\\') { + this.add("\\\\"); + } else if (c == '/') { + this.add("\\/"); + } else if (c == '\b') { + this.add("\\b"); + } else if (c == '\f') { + this.add("\\f"); + } else if (c == '\n') { + this.add("\\n"); + } else if (c == '\r') { + this.add("\\r"); + } else if (c == '\t') { + this.add("\\t"); + } else if (Character.isISOControl(c)) { + this.unicode(c); + } else { + this.add(c); + } + } + + this.add('"'); + } + + /** + * Add object to buffer + */ + private void add(Object obj) { + this.buf.append(obj); + } + + /** + * Add char to buffer + */ + private void add(char c) { + this.buf.append(c); + } + + /** + * Represent as unicode + * + * @param c + * character to be encoded + */ + private void unicode(char c) { + this.add("\\u"); + + int n = c; + + for (int i = 0; i < 4; ++i) { + int digit = (n & 0xf000) >> 12; + + this.add(hex[digit]); + n <<= 4; + } + } + + public void setIgnoreHierarchy(boolean ignoreHierarchy) { + this.ignoreHierarchy = ignoreHierarchy; + } + + /** + * If true, an Enum is serialized as a bean with a special property + * _name=name() as all as all other properties defined within the enum.
+ * If false, an Enum is serialized as a name=value pair (name=name()) + * + * @param enumAsBean + * true to serialize an enum as a bean instead of as a name=value + * pair (default=false) + */ + public void setEnumAsBean(boolean enumAsBean) { + this.enumAsBean = enumAsBean; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/SerializationParams.java b/plugins/json/src/main/java/org/apache/struts2/json/SerializationParams.java new file mode 100644 index 000000000..efc6c5a48 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/SerializationParams.java @@ -0,0 +1,120 @@ +/* + * $Id$ + * + * 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.json; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.xwork.StringUtils; + +public class SerializationParams { + private static final String DEFAULT_CONTENT_TYPE = "application/json"; + + private final HttpServletResponse response; + private final String encoding; + private final boolean wrapWithComments; + private final String serializedJSON; + private final boolean smd; + private final boolean gzip; + private final boolean noCache; + private final int statusCode; + private final int errorCode; + private final boolean prefix; + private String contentType = DEFAULT_CONTENT_TYPE; + private String wrapPrefix; + private String wrapSuffix; + + public SerializationParams(HttpServletResponse response, String encoding, boolean wrapWithComments, + String serializedJSON, boolean smd, boolean gzip, boolean noCache, int statusCode, int errorCode, + boolean prefix, String contentType, String wrapPrefix, String wrapSuffix) { + this.response = response; + this.encoding = encoding; + this.wrapWithComments = wrapWithComments; + this.serializedJSON = serializedJSON; + this.smd = smd; + this.gzip = gzip; + this.noCache = noCache; + this.statusCode = statusCode; + this.errorCode = errorCode; + this.prefix = prefix; + this.contentType = StringUtils.defaultString(contentType, DEFAULT_CONTENT_TYPE); + this.wrapPrefix = wrapPrefix; + this.wrapSuffix = wrapSuffix; + } + + public SerializationParams(HttpServletResponse response, String defaultEncoding, + boolean wrapWithComments, String json, boolean b, boolean b1, boolean noCache, int i, int i1, + boolean prefix, String contentType) { + this(response, defaultEncoding, wrapWithComments, json, b, b1, noCache, i, i1, prefix, contentType, + null, null); + } + + public String getWrapSuffix() { + return wrapSuffix; + } + + public String getWrapPrefix() { + return wrapPrefix; + } + + public HttpServletResponse getResponse() { + return response; + } + + public String getEncoding() { + return encoding; + } + + public boolean isWrapWithComments() { + return wrapWithComments; + } + + public String getSerializedJSON() { + return serializedJSON; + } + + public boolean isSmd() { + return smd; + } + + public boolean isGzip() { + return gzip; + } + + public boolean isNoCache() { + return noCache; + } + + public int getStatusCode() { + return statusCode; + } + + public int getErrorCode() { + return errorCode; + } + + public boolean isPrefix() { + return prefix; + } + + public String getContentType() { + return contentType; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java b/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java new file mode 100644 index 000000000..584ea72cf --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java @@ -0,0 +1,41 @@ +/* + * $Id$ + * + * 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.json.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* + * Annotation used to customize serialization + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface JSON { + String name() default ""; + + boolean serialize() default true; + + boolean deserialize() default true; + + String format() default ""; +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMD.java b/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMD.java new file mode 100644 index 000000000..a3bd2797c --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMD.java @@ -0,0 +1,36 @@ +/* + * $Id$ + * + * 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.json.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface SMD { + String version() default org.apache.struts2.json.smd.SMD.DEFAULT_VERSION; + + String objectName(); + + String serviceType() default org.apache.struts2.json.smd.SMD.DEFAULT_SERVICE_TYPE; +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethod.java b/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethod.java new file mode 100644 index 000000000..a910135fa --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethod.java @@ -0,0 +1,32 @@ +/* + * $Id$ + * + * 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.json.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SMDMethod { + String name() default ""; +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethodParameter.java b/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethodParameter.java new file mode 100644 index 000000000..2d28d5007 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/annotations/SMDMethodParameter.java @@ -0,0 +1,32 @@ +/* + * $Id$ + * + * 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.json.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface SMDMethodParameter { + String name(); +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCError.java b/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCError.java new file mode 100644 index 000000000..c9a66ceb6 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCError.java @@ -0,0 +1,109 @@ +/* + * $Id$ + * + * 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.json.rpc; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +/* + * Used to serialize RPC Errors + */ +public class RPCError { + private static final Logger LOG = LoggerFactory.getLogger(RPCError.class); + + private int code; + private String name; + private String message; + private String stack; + + public RPCError() { + } + + public RPCError(String message, int code) { + this.code = code; + this.message = message; + + LOG.error(message); + } + + public RPCError(String message, RPCErrorCode code) { + this(message, code.code()); + } + + public RPCError(Throwable t, int code, boolean debug) { + while (t.getCause() != null) { + t = t.getCause(); + } + + this.code = code; + this.message = t.getMessage(); + this.name = t.getClass().getName(); + + if (debug) { + StringWriter s = new StringWriter(); + PrintWriter w = new PrintWriter(s); + t.printStackTrace(w); + w.flush(); + this.stack = s.toString(); + } + + LOG.error(t.getMessage(), t); + } + + public RPCError(Throwable t, RPCErrorCode code, boolean debug) { + this(t, code.code(), debug); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getStack() { + return stack; + } + + public void setStack(String stack) { + this.stack = stack; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCErrorCode.java b/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCErrorCode.java new file mode 100644 index 000000000..bef720d86 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCErrorCode.java @@ -0,0 +1,47 @@ +/* + * $Id$ + * + * 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.json.rpc; + +public enum RPCErrorCode { + MISSING_METHOD(100, "'method' parameter is missing in request"), + MISSING_ID(100, "'id' parameter is missing in request"), + INVALID_PROCEDURE_CALL(0, "Invalid procedure call"), + METHOD_NOT_FOUND(101, "Procedure not found"), + PARAMETERS_MISMATCH(102, "Parameters count in request does not patch parameters count on method"), + EXCEPTION(103, "An exception was thrown"), + SMD_DISABLED(104, "SMD is disabled"); + + private int code; + private String message; + + RPCErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int code() { + return code; + } + + public String message() { + return this.message; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCResponse.java b/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCResponse.java new file mode 100644 index 000000000..24b2537e1 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/rpc/RPCResponse.java @@ -0,0 +1,54 @@ +/* + * $Id$ + * + * 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.json.rpc; + +/** + * Class that will be serialized as a response to an RPC call + */ +public class RPCResponse { + private String id; + private Object result; + private RPCError error; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public RPCError getError() { + return error; + } + + public void setError(RPCError error) { + this.error = error; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/smd/SMD.java b/plugins/json/src/main/java/org/apache/struts2/json/smd/SMD.java new file mode 100644 index 000000000..c5ddf05c3 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/smd/SMD.java @@ -0,0 +1,78 @@ +/* + * $Id$ + * + * 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.json.smd; + +import java.util.Set; +import java.util.TreeSet; + +/* + * HOlds SMD declarations for a class + */ +public class SMD { + public static final String DEFAULT_VERSION = ".1"; + public static final String DEFAULT_SERVICE_TYPE = "JSON-RPC"; + + private String version = DEFAULT_VERSION; + private String objectName; + private String serviceType = DEFAULT_SERVICE_TYPE; + private String serviceUrl; + private Set methods = new TreeSet(); + + public void addSMDMethod(SMDMethod method) { + this.methods.add(method); + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getObjectName() { + return this.objectName; + } + + public void setObjectName(String objectName) { + this.objectName = objectName; + } + + public String getServiceType() { + return this.serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public String getServiceUrl() { + return this.serviceUrl; + } + + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + public Set getMethods() { + return this.methods; + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethod.java b/plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethod.java new file mode 100644 index 000000000..abd42b327 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethod.java @@ -0,0 +1,75 @@ +/* + * $Id$ + * + * 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.json.smd; + +import java.util.Set; +import java.util.TreeSet; + +public class SMDMethod implements Comparable { + private String name; + private Set parameters = new TreeSet(); + + public SMDMethod(String name) { + this.name = name; + } + + public void addSMDMethodParameter(SMDMethodParameter parameter) { + this.parameters.add(parameter); + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getParameters() { + return this.parameters; + } + + public int compareTo(Object o) { + if (!(o instanceof SMDMethod)) + return 1; + if (o == null) + return 1; + SMDMethod other = (SMDMethod) o; + if ((name == null) && (other.name == null)) + return 0; + if (name == null) + return -1; + if (name.equals(other.name)) + return parameters.size() - other.parameters.size(); + + return name.compareTo(other.name); + } + + public boolean equals(Object obj) { + if (!(obj instanceof SMDMethod)) + return false; + SMDMethod toCompare = (SMDMethod) obj; + if ((name == null) && (toCompare.name == null)) + return true; + return (name != null) && name.equals(toCompare.name) + && (parameters.size() == toCompare.parameters.size()); + } +} diff --git a/plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethodParameter.java b/plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethodParameter.java new file mode 100644 index 000000000..95797f21d --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/smd/SMDMethodParameter.java @@ -0,0 +1,57 @@ +/* + * $Id$ + * + * 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.json.smd; + +public class SMDMethodParameter implements Comparable { + private String name; + + public SMDMethodParameter(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public int compareTo(Object o) { + if (!(o instanceof SMDMethodParameter)) + return 1; + if (o == null) + return 1; + if ((name == null) && (((SMDMethodParameter) o).name == null)) + return 0; + if (name == null) + return -1; + return name.compareTo(((SMDMethodParameter) o).name); + } + + public boolean equals(Object o) { + if (!(o instanceof SMDMethodParameter)) + return false; + if ((name == null) && (((SMDMethodParameter) o).name == null)) + return true; + return (name != null) && name.equals(((SMDMethodParameter) o).name); + } +} diff --git a/plugins/json/src/main/resources/JsonPlugin.tld b/plugins/json/src/main/resources/JsonPlugin.tld new file mode 100644 index 000000000..8c2c10dc4 --- /dev/null +++ b/plugins/json/src/main/resources/JsonPlugin.tld @@ -0,0 +1,25 @@ + + + + + This exposes JSON util functions from the Struts JSON plugin + + + 1.0 + + json + + /struts-json-tags + + + serialize + org.apache.struts2.json.JSONUtil + + java.lang.String serialize(java.lang.Object) + + + + diff --git a/plugins/json/src/main/resources/struts-plugin.xml b/plugins/json/src/main/resources/struts-plugin.xml new file mode 100644 index 000000000..447987177 --- /dev/null +++ b/plugins/json/src/main/resources/struts-plugin.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/plugins/json/src/test/java/org/apache/struts2/json/AnEnum.java b/plugins/json/src/test/java/org/apache/struts2/json/AnEnum.java new file mode 100644 index 000000000..94226d979 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/AnEnum.java @@ -0,0 +1,28 @@ +/* + * $Id$ + * + * 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.json; + +/** + * An enumeration for JSON serialization testing + */ +public enum AnEnum { + ValueA, ValueB, ValueC +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/AnEnumBean.java b/plugins/json/src/test/java/org/apache/struts2/json/AnEnumBean.java new file mode 100644 index 000000000..0f6313e62 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/AnEnumBean.java @@ -0,0 +1,45 @@ +/* + * $Id$ + * + * 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.json; + +/** + * A more complicated Enum with additional properties. + */ +public enum AnEnumBean { + + One("A", "B"), Two("C", "D"), Three("E", "F"); + + private String propA; + private String propB; + + AnEnumBean(String propA, String propB) { + this.propA = propA; + this.propB = propB; + } + + public String getPropA() { + return propA; + } + + public String getPropB() { + return propB; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/Bean.java b/plugins/json/src/test/java/org/apache/struts2/json/Bean.java new file mode 100644 index 000000000..8fb725a1c --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/Bean.java @@ -0,0 +1,151 @@ +/* + * $Id$ + * + * 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.json; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class Bean { + private String stringField; + private int intField; + private boolean booleanField; + private char charField; + private long longField; + private float floatField; + private double doubleField; + private Object objectField; + private byte byteField; + private AnEnum enumField; + private AnEnumBean enumBean; + private BigDecimal bigDecimal; + private BigInteger bigInteger; + + /** + * @return the byteField + */ + public byte getByteField() { + return this.byteField; + } + + /** + * @param byteField + * the byteField to set + */ + public void setByteField(byte byteField) { + this.byteField = byteField; + } + + public boolean isBooleanField() { + return this.booleanField; + } + + public void setBooleanField(boolean booleanField) { + this.booleanField = booleanField; + } + + public char getCharField() { + return this.charField; + } + + public void setCharField(char charField) { + this.charField = charField; + } + + public double getDoubleField() { + return this.doubleField; + } + + public void setDoubleField(double doubleField) { + this.doubleField = doubleField; + } + + public float getFloatField() { + return this.floatField; + } + + public void setFloatField(float floatField) { + this.floatField = floatField; + } + + public int getIntField() { + return this.intField; + } + + public void setIntField(int intField) { + this.intField = intField; + } + + public long getLongField() { + return this.longField; + } + + public void setLongField(long longField) { + this.longField = longField; + } + + public Object getObjectField() { + return this.objectField; + } + + public void setObjectField(Object objectField) { + this.objectField = objectField; + } + + public String getStringField() { + return this.stringField; + } + + public void setStringField(String stringField) { + this.stringField = stringField; + } + + public AnEnum getEnumField() { + return enumField; + } + + public void setEnumField(AnEnum enumField) { + this.enumField = enumField; + } + + public AnEnumBean getEnumBean() { + return enumBean; + } + + public void setEnumBean(AnEnumBean enumBean) { + this.enumBean = enumBean; + } + + public BigInteger getBigInteger() { + return bigInteger; + } + + public void setBigInteger(BigInteger bigInteger) { + this.bigInteger = bigInteger; + } + + public BigDecimal getBigDecimal() { + return bigDecimal; + } + + public void setBigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java new file mode 100644 index 000000000..b8b095436 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java @@ -0,0 +1,127 @@ +/* + * $Id$ + * + * 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.json; + +import java.util.Map; + +import junit.framework.TestCase; + +/** + * Test serialization of an Enum in the two supported modes: - Enum as a + * name=value pair; - Enum as a bean + */ +public class JSONEnumTest extends TestCase { + + /** + * Asserts that a bean can be serialized to JSON and restored as a map + */ + public void testEnumAsNameValue() throws Exception { + Bean bean1 = new Bean(); + + bean1.setStringField("str"); + bean1.setBooleanField(true); + bean1.setCharField('s'); + bean1.setDoubleField(10.1); + bean1.setFloatField(1.5f); + bean1.setIntField(10); + bean1.setLongField(100); + bean1.setEnumField(AnEnum.ValueA); + bean1.setEnumBean(AnEnumBean.Two); + + JSONWriter jsonWriter = new JSONWriter(); + jsonWriter.setEnumAsBean(false); + String json = jsonWriter.write(bean1); + + Map result = (Map) JSONUtil.deserialize(json); + assertEquals("str", result.get("stringField")); + assertEquals(true, result.get("booleanField")); + assertEquals("s", result.get("charField")); // note: this is a + // String + assertEquals(10.1, result.get("doubleField")); + assertEquals(1.5, result.get("floatField")); // note: this is a + // Double + assertEquals(10L, result.get("intField")); // note: this is a + // Long + assertEquals(AnEnum.ValueA, AnEnum.valueOf((String) result.get("enumField"))); // note: + // this + // is a + // String + assertEquals(AnEnumBean.Two, AnEnumBean.valueOf((String) result.get("enumBean"))); // note: + // this + // is a + // String + } + + /** + * Asserts that a bean can be serialized to JSON and restored as a map

+ * In this case, the name of the enum is in _name and the two properties of + * AnEnumBean are also serialized + */ + public void testEnumAsBean() throws Exception { + Bean bean1 = new Bean(); + + bean1.setStringField("str"); + bean1.setBooleanField(true); + bean1.setCharField('s'); + bean1.setDoubleField(10.1); + bean1.setFloatField(1.5f); + bean1.setIntField(10); + bean1.setLongField(100); + bean1.setEnumField(AnEnum.ValueA); + bean1.setEnumBean(AnEnumBean.Two); + + JSONWriter jsonWriter = new JSONWriter(); + jsonWriter.setEnumAsBean(true); + String json = jsonWriter.write(bean1); + + Map result = (Map) JSONUtil.deserialize(json); + assertEquals("str", result.get("stringField")); + assertEquals(true, result.get("booleanField")); + assertEquals("s", result.get("charField")); // note: this is a + // String + assertEquals(10.1, result.get("doubleField")); + assertEquals(1.5, result.get("floatField")); // note: this is a + // Double + assertEquals(10L, result.get("intField")); // note: this is a + // Long + Map enumBean1 = (Map) result.get("enumField"); + assertNotNull(enumBean1); + assertEquals(AnEnum.ValueA, AnEnum.valueOf((String) enumBean1.get("_name"))); // get + // the + // special + // name + // property + Map enumBean2 = (Map) result.get("enumBean"); + assertEquals(AnEnumBean.Two, AnEnumBean.valueOf((String) enumBean2.get("_name"))); // get + // the + // special + // name + // property + assertEquals(AnEnumBean.Two.getPropA(), (String) enumBean2.get("propA")); // get + // the + // propA + // property + assertEquals(AnEnumBean.Two.getPropB(), (String) enumBean2.get("propB")); // get + // the + // propA + // property + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java new file mode 100644 index 000000000..86557ba53 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java @@ -0,0 +1,502 @@ +/* + * $Id$ + * + * 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.json; + +import java.util.Calendar; +import java.util.List; +import java.util.Map; + +import org.apache.struts2.StrutsStatics; +import org.apache.struts2.StrutsTestCase; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.mock.MockActionInvocation; +import com.opensymphony.xwork2.util.ValueStack; + +public class JSONInterceptorTest extends StrutsTestCase { + private MockActionInvocationEx invocation; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + private void setRequestContent(String fileName) throws Exception { + String content = TestUtils.readContent(JSONInterceptorTest.class.getResource(fileName)); + this.request.setContent(content.getBytes()); + } + + public void testBadJSON1() throws Exception { + tryBadJSON("bad-1.txt"); + } + + public void testBadJSON2() throws Exception { + tryBadJSON("bad-2.txt"); + } + + public void testBadJSON3() throws Exception { + tryBadJSON("bad-3.txt"); + } + + public void testBadJSON4() throws Exception { + tryBadJSON("bad-4.txt"); + } + + public void testBadJSON5() throws Exception { + tryBadJSON("bad-5.txt"); + } + + public void testBadToTheBoneJSON4() throws Exception { + tryBadJSON("bad-to-the-bone.txt"); + } + + private void tryBadJSON(String fileName) throws Exception { + // request + setRequestContent(fileName); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + + // JSON is not well formed, throw exception + try { + interceptor.intercept(this.invocation); + fail("Should have thrown an exception"); + } catch (JSONException e) { + // I can't get JUnit to ignore the exception + // @Test(expected = JSONException.class) + } + } + + public void testSMDDisabledSMD() throws Exception { + // request + setRequestContent("smd-3.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + + // SMD was not enabled so invocation must happen + try { + interceptor.intercept(this.invocation); + } catch (JSONException e) { + fail("Should have not thrown an exception"); + } + + } + + public void testSMDAliasedMethodCall1() throws Exception { + // request + setRequestContent("smd-14.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest2 action = new SMDActionTest2(); + + this.invocation.setAction(action); + + interceptor.intercept(this.invocation); + // method was aliased, but was invoked with the regular name + // so method must not be invoked + assertFalse(this.invocation.isInvoked()); + assertFalse(action.isDoSomethingInvoked()); + } + + public void testSMDAliasedMethodCall2() throws Exception { + // request + setRequestContent("smd-15.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest2 action = new SMDActionTest2(); + + this.invocation.setAction(action); + + interceptor.intercept(this.invocation); + // method was aliased, but was invoked with the aliased name + // so method must be invoked + assertFalse(this.invocation.isInvoked()); + assertTrue(action.isDoSomethingInvoked()); + } + + public void testSMDNoMethod() throws Exception { + // request + setRequestContent("smd-4.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + + // SMD was enabled so invocation must happen + + interceptor.intercept(this.invocation); + + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("smd-13.txt")); + assertEquals(normalizedExpected, normalizedActual); + + assertFalse(this.invocation.isInvoked()); + } + + public void testSMDMethodWithoutAnnotations() throws Exception { + // request + setRequestContent("smd-9.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + + // SMD was enabled so invocation must happen + try { + interceptor.intercept(this.invocation); + assertTrue("Exception was expected here!", true); + } catch (Exception e) { + // ok + } + assertFalse(this.invocation.isInvoked()); + } + + public void testSMDPrimitivesNoResult() throws Exception { + // request + setRequestContent("smd-6.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + + // can't be invoked + interceptor.intercept(this.invocation); + assertFalse(this.invocation.isInvoked()); + + // asert values were passed properly + assertEquals("string", action.getStringParam()); + assertEquals(1, action.getIntParam()); + assertEquals(true, action.isBooleanParam()); + assertEquals('c', action.getCharParam()); + assertEquals(2, action.getLongParam()); + assertEquals(new Float(3.3), action.getFloatParam()); + assertEquals(4.4, action.getDoubleParam()); + assertEquals(5, action.getShortParam()); + assertEquals(6, action.getByteParam()); + + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("smd-11.txt")); + assertEquals(normalizedExpected, normalizedActual); + + assertEquals("application/json-rpc;charset=ISO-8859-1", response.getContentType()); + } + + public void testSMDReturnObject() throws Exception { + // request + setRequestContent("smd-10.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest2 action = new SMDActionTest2(); + + this.invocation.setAction(action); + + // can't be invoked + interceptor.intercept(this.invocation); + assertFalse(this.invocation.isInvoked()); + + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("smd-12.txt")); + assertEquals(normalizedExpected, normalizedActual); + + assertEquals("application/json-rpc;charset=ISO-8859-1", response.getContentType()); + } + + @SuppressWarnings("unchecked") + public void testSMDObjectsNoResult() throws Exception { + // request + setRequestContent("smd-7.txt"); + this.request.addHeader("content-type", "application/json-rpc"); + + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setEnableSMD(true); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + + // can't be invoked + interceptor.intercept(this.invocation); + assertFalse(this.invocation.isInvoked()); + + // asert values were passed properly + Bean bean = action.getBeanParam(); + assertNotNull(bean); + assertTrue(bean.isBooleanField()); + assertEquals(bean.getStringField(), "test"); + assertEquals(bean.getIntField(), 10); + assertEquals(bean.getCharField(), 's'); + assertEquals(bean.getDoubleField(), 10.1); + assertEquals(bean.getByteField(), 3); + + List list = action.getListParam(); + assertNotNull(list); + assertEquals("str0", list.get(0)); + assertEquals("str1", list.get(1)); + + Map map = action.getMapParam(); + assertNotNull(map); + assertNotNull(map.get("a")); + assertEquals(new Long(1), map.get("a")); + assertNotNull(map.get("c")); + List insideList = (List) map.get("c"); + assertEquals(1.0d, insideList.get(0)); + assertEquals(2.0d, insideList.get(1)); + + String json = response.getContentAsString(); + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("smd-11.txt")); + assertEquals(normalizedExpected, normalizedActual); + + assertEquals("application/json-rpc;charset=ISO-8859-1", response.getContentType()); + } + + @SuppressWarnings( { "unchecked", "unchecked" }) + public void testReadEmpty() throws Exception { + // request + setRequestContent("json-6.txt"); + this.request.addHeader("content-type", "application/json"); + + // interceptor + JSONInterceptor interceptor = new JSONInterceptor(); + TestAction action = new TestAction(); + + this.invocation.setAction(action); + + interceptor.intercept(this.invocation); + } + + @SuppressWarnings( { "unchecked", "unchecked" }) + public void test() throws Exception { + // request + setRequestContent("json-1.txt"); + this.request.addHeader("content-type", "application/json"); + + // interceptor + JSONInterceptor interceptor = new JSONInterceptor(); + TestAction action = new TestAction(); + + this.invocation.setAction(action); + + interceptor.intercept(this.invocation); + + // serialize and compare + List list = action.getList(); + + assertNotNull(list); + assertEquals(list.size(), 10); + + list = action.getCollection(); + assertNotNull(list); + assertEquals(list.size(), 3); + assertEquals(list.get(0), "b"); + assertEquals(list.get(1), 1L); + list = (List) list.get(2); + assertNotNull(list); + assertEquals(list.size(), 2); + assertEquals(list.get(0), 10L); + assertEquals(list.get(1), 12L); + + list = action.getCollection2(); + assertNotNull(list); + assertEquals(list.size(), 1); + + // inside a map any primitive is either: String, Long, Boolean or Double + Map bean = (Map) list.get(0); + + assertNotNull(bean); + assertTrue((Boolean) bean.get("booleanField")); + assertEquals(bean.get("charField"), "s"); + assertEquals(bean.get("doubleField"), 10.1); + assertEquals(bean.get("floatField"), 1.5); + assertEquals(bean.get("intField"), 10L); + assertEquals(bean.get("longField"), 100L); + assertEquals(bean.get("stringField"), "str"); + + bean = (Map) bean.get("objectField"); + assertNotNull(bean); + assertFalse((Boolean) bean.get("booleanField")); + assertEquals(bean.get("charField"), "\u0000"); + assertEquals(bean.get("doubleField"), 2.2); + assertEquals(bean.get("floatField"), 1.1); + assertEquals(bean.get("intField"), 0L); + assertEquals(bean.get("longField"), 0L); + assertEquals(bean.get("stringField"), " "); + + assertEquals(action.getFoo(), "foo"); + + Map map = action.getMap(); + + assertNotNull(map); + assertEquals(map.size(), 2); + assertEquals(map.get("a"), 1L); + list = (List) map.get("c"); + assertNotNull(list); + assertEquals(list.size(), 2); + assertEquals(list.get(0), 1.0); + assertEquals(list.get(1), 2.0); + + assertEquals(action.getResult(), null); + + Bean bean2 = action.getBean(); + + assertNotNull(bean2); + assertTrue(bean2.isBooleanField()); + assertEquals(bean2.getStringField(), "test"); + assertEquals(bean2.getIntField(), 10); + assertEquals(bean2.getCharField(), 's'); + assertEquals(bean2.getDoubleField(), 10.1); + assertEquals(bean2.getByteField(), 3); + + String[] strArray = action.getArray(); + + assertNotNull(strArray); + assertEquals(strArray.length, 2); + assertEquals(strArray[0], "str0"); + assertEquals(strArray[1], "str1"); + + int[] intArray = action.getIntArray(); + + assertNotNull(intArray); + assertEquals(intArray.length, 2); + assertEquals(intArray[0], 1); + assertEquals(intArray[1], 2); + + Bean[] beanArray = action.getBeanArray(); + + assertNotNull(beanArray); + assertNotNull(beanArray[0]); + assertEquals(beanArray[0].getStringField(), "bean1"); + assertNotNull(beanArray[1]); + assertEquals(beanArray[1].getStringField(), "bean2"); + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(action.getDate()); + + assertEquals(calendar.get(Calendar.YEAR), 1999); + assertEquals(calendar.get(Calendar.MONTH), Calendar.DECEMBER); + assertEquals(calendar.get(Calendar.DAY_OF_MONTH), 31); + assertEquals(calendar.get(Calendar.HOUR), 11); + assertEquals(calendar.get(Calendar.MINUTE), 59); + assertEquals(calendar.get(Calendar.SECOND), 59); + + calendar.setTime(action.getDate2()); + assertEquals(calendar.get(Calendar.YEAR), 1999); + assertEquals(calendar.get(Calendar.MONTH), Calendar.DECEMBER); + assertEquals(calendar.get(Calendar.DAY_OF_MONTH), 31); + + // test desrialize=false + assertNull(action.getFoo2()); + } + + public void testRoot() throws Exception { + setRequestContent("json-5.txt"); + this.request.addHeader("content-type", "application/json"); + + // interceptor + JSONInterceptor interceptor = new JSONInterceptor(); + interceptor.setRoot("bean"); + TestAction4 action = new TestAction4(); + + this.invocation.setAction(action); + this.invocation.getStack().push(action); + + interceptor.intercept(this.invocation); + + Bean bean2 = action.getBean(); + + assertNotNull(bean2); + assertTrue(bean2.isBooleanField()); + assertEquals(bean2.getStringField(), "test"); + assertEquals(bean2.getIntField(), 10); + assertEquals(bean2.getCharField(), 's'); + assertEquals(bean2.getDoubleField(), 10.1); + assertEquals(bean2.getByteField(), 3); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + + ActionContext context = ActionContext.getContext(); + ValueStack stack = context.getValueStack(); + + ActionContext.setContext(context); + context.put(StrutsStatics.HTTP_REQUEST, this.request); + context.put(StrutsStatics.HTTP_RESPONSE, this.response); + + MockServletContext servletContext = new MockServletContext(); + + context.put(StrutsStatics.SERVLET_CONTEXT, servletContext); + this.invocation = new MockActionInvocationEx(); + this.invocation.setInvocationContext(context); + this.invocation.setStack(stack); + } +} + +class MockActionInvocationEx extends MockActionInvocation { + private boolean invoked; + + @Override + public String invoke() throws Exception { + this.invoked = true; + return super.invoke(); + } + + public boolean isInvoked() { + return this.invoked; + } + + public void setInvoked(boolean invoked) { + this.invoked = invoked; + } + +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java new file mode 100644 index 000000000..0fdf04073 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java @@ -0,0 +1,163 @@ +/* + * $Id$ + * + * 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.json; + +import java.beans.IntrospectionException; +import java.io.StringReader; +import java.lang.reflect.InvocationTargetException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +public class JSONPopulatorTest extends TestCase { + + public void testNulls() throws IntrospectionException, InvocationTargetException, NoSuchMethodException, + JSONException, InstantiationException, IllegalAccessException { + JSONPopulator populator = new JSONPopulator(); + OtherBean bean = new OtherBean(); + Map jsonMap = new HashMap(); + + jsonMap.put("intField", null); + jsonMap.put("booleanField", null); + jsonMap.put("charField", null); + jsonMap.put("longField", null); + jsonMap.put("floatField", null); + jsonMap.put("doubleField", null); + jsonMap.put("byteField", null); + + populator.populateObject(bean, jsonMap); + assertNull(bean.getIntField()); + assertNull(bean.isBooleanField()); + assertNull(bean.getCharField()); + assertNull(bean.getLongField()); + assertNull(bean.getDoubleField()); + assertNull(bean.getByteField()); + } + + public void testPrimitiveBean() throws Exception { + StringReader stringReader = new StringReader(TestUtils.readContent(JSONInterceptorTest.class + .getResource("json-7.txt"))); + Object json = JSONUtil.deserialize(stringReader); + assertNotNull(json); + assertTrue(json instanceof Map); + Map jsonMap = (Map) json; + JSONPopulator populator = new JSONPopulator(); + Bean bean = new Bean(); + populator.populateObject(bean, jsonMap); + assertTrue(bean.isBooleanField()); + assertEquals("test\u000E\u000f", bean.getStringField()); + assertEquals(10, bean.getIntField()); + assertEquals('s', bean.getCharField()); + assertEquals(10.1d, bean.getDoubleField(), 0d); + assertEquals(3, bean.getByteField()); + assertEquals(new BigDecimal(111111.5d), bean.getBigDecimal()); + assertEquals(new BigInteger("111111"), bean.getBigInteger()); + } + + public void testObjectBean() throws Exception { + String text = TestUtils.readContent(JSONInterceptorTest.class.getResource("json-7.txt")); + Object json = JSONUtil.deserialize(text); + assertNotNull(json); + assertTrue(json instanceof Map); + Map jsonMap = (Map) json; + JSONPopulator populator = new JSONPopulator(); + WrapperClassBean bean = new WrapperClassBean(); + populator.populateObject(bean, jsonMap); + assertEquals(Boolean.TRUE, bean.getBooleanField()); + assertEquals(true, bean.isPrimitiveBooleanField1()); + assertEquals(false, bean.isPrimitiveBooleanField2()); + assertEquals(false, bean.isPrimitiveBooleanField3()); + assertEquals("test\u000E\u000f", bean.getStringField()); + assertEquals(new Integer(10), bean.getIntField()); + assertEquals(0, bean.getNullIntField()); + assertEquals(new Character('s'), bean.getCharField()); + assertEquals(10.1d, bean.getDoubleField()); + assertEquals(new Byte((byte) 3), bean.getByteField()); + + assertEquals(2, bean.getListField().size()); + assertEquals("1", bean.getListField().get(0).getValue()); + assertEquals("2", bean.getListField().get(1).getValue()); + + assertEquals(1, bean.getListMapField().size()); + assertEquals(2, bean.getListMapField().get(0).size()); + assertEquals(new Long(2073501), bean.getListMapField().get(0).get("id1")); + assertEquals(new Long(3), bean.getListMapField().get(0).get("id2")); + + assertEquals(2, bean.getMapListField().size()); + assertEquals(3, bean.getMapListField().get("id1").size()); + assertEquals(new Long(2), bean.getMapListField().get("id1").get(1)); + assertEquals(4, bean.getMapListField().get("id2").size()); + assertEquals(new Long(3), bean.getMapListField().get("id2").get(1)); + + assertEquals(1, bean.getArrayMapField().length); + assertEquals(2, bean.getArrayMapField()[0].size()); + assertEquals(new Long(2073501), bean.getArrayMapField()[0].get("id1")); + assertEquals(new Long(3), bean.getArrayMapField()[0].get("id2")); + } + + public void testObjectBeanWithStrings() throws Exception { + StringReader stringReader = new StringReader(TestUtils.readContent(JSONInterceptorTest.class + .getResource("json-8.txt"))); + Object json = JSONUtil.deserialize(stringReader); + assertNotNull(json); + assertTrue(json instanceof Map); + Map jsonMap = (Map) json; + JSONPopulator populator = new JSONPopulator(); + WrapperClassBean bean = new WrapperClassBean(); + populator.populateObject(bean, jsonMap); + assertEquals(Boolean.TRUE, bean.getBooleanField()); + assertEquals("test", bean.getStringField()); + assertEquals(new Integer(10), bean.getIntField()); + assertEquals(new Character('s'), bean.getCharField()); + assertEquals(10.1d, bean.getDoubleField()); + assertEquals(new Byte((byte) 3), bean.getByteField()); + + assertEquals(null, bean.getListField()); + assertEquals(null, bean.getListMapField()); + assertEquals(null, bean.getMapListField()); + assertEquals(null, bean.getArrayMapField()); + } + + public void testInfiniteLoop() throws JSONException { + try { + JSONReader reader = new JSONReader(); + reader.read("[1,\"a]"); + fail("Should have thrown an exception"); + } catch (JSONException e) { + // I can't get JUnit to ignore the exception + // @Test(expected = JSONException.class) + } + } + + public void testParseBadInput() throws JSONException { + try { + JSONReader reader = new JSONReader(); + reader.read("[1,\"a\"1]"); + fail("Should have thrown an exception"); + } catch (JSONException e) { + // I can't get JUnit to ignore the exception + // @Test(expected = JSONException.class) + } + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java new file mode 100644 index 000000000..942aa9a44 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java @@ -0,0 +1,534 @@ +/* + + * $Id$ + + * + * 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.json; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.struts2.StrutsStatics; +import org.apache.struts2.StrutsTestCase; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.mock.MockActionInvocation; +import com.opensymphony.xwork2.util.ValueStack; + +/** + * JSONResultTest + */ +public class JSONResultTest extends StrutsTestCase { + MockActionInvocation invocation; + MockHttpServletResponse response; + MockServletContext servletContext; + ActionContext context; + ValueStack stack; + MockHttpServletRequest request; + + public void testJSONUtilNPEOnNullMehtod() { + Map map = new HashMap(); + map.put("createtime", new Date()); + try { + JSONUtil.serialize(map); + } catch (JSONException e) { + fail(e.getMessage()); + } + } + + public void testJSONWriterEndlessLoopOnExludedProperties() throws JSONException { + Pattern all = Pattern.compile(".*"); + + JSONWriter writer = new JSONWriter(); + writer.write(Arrays.asList("a", "b"), Arrays.asList(all), null, false); + } + + public void testSMDDisabledSMD() throws Exception { + JSONResult result = new JSONResult(); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String smd = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(smd, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("smd-8.txt")); + assertEquals(normalizedExpected, normalizedActual); + } + + public void testSMDDefault() throws Exception { + JSONResult result = new JSONResult(); + result.setEnableSMD(true); + SMDActionTest1 action = new SMDActionTest1(); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String smd = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(smd, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("smd-1.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + public void testSMDDefaultAnnotations() throws Exception { + JSONResult result = new JSONResult(); + result.setEnableSMD(true); + SMDActionTest2 action = new SMDActionTest2(); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String smd = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(smd, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("smd-2.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + public void testExcludeNullPropeties() throws Exception { + JSONResult result = new JSONResult(); + result.setExcludeNullProperties(true); + TestAction action = new TestAction(); + action.setFoo("fool"); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String smd = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(smd, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("nulls-1.txt")); + assertEquals(normalizedExpected, normalizedActual); + } + + public void testWrapPrefix() throws Exception { + JSONResult result = new JSONResult(); + result.setWrapPrefix("_prefix_"); + TestAction2 action = new TestAction2(); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String out = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(out, true); + String normalizedExpected = "_prefix_{\"name\":\"name\"}"; + assertEquals(normalizedExpected, normalizedActual); + } + + public void testSuffix() throws Exception { + JSONResult result = new JSONResult(); + result.setWrapSuffix("_suffix_"); + TestAction2 action = new TestAction2(); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String out = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(out, true); + String normalizedExpected = "{\"name\":\"name\"}_suffix_"; + assertEquals(normalizedExpected, normalizedActual); + } + + public void testPrefixAndSuffix() throws Exception { + JSONResult result = new JSONResult(); + result.setWrapPrefix("_prefix_"); + result.setWrapSuffix("_suffix_"); + TestAction2 action = new TestAction2(); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String out = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(out, true); + String normalizedExpected = "_prefix_{\"name\":\"name\"}_suffix_"; + assertEquals(normalizedExpected, normalizedActual); + } + + public void testPrefix() throws Exception { + JSONResult result = new JSONResult(); + result.setExcludeNullProperties(true); + result.setPrefix(true); + TestAction action = new TestAction(); + action.setFoo("fool"); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String smd = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(smd, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("prefix-1.txt")); + assertEquals(normalizedExpected, normalizedActual); + } + + @SuppressWarnings("unchecked") + public void test() throws Exception { + JSONResult result = new JSONResult(); + + TestAction action = new TestAction(); + + // test scape characters + action.setArray(new String[] { "a", "a", "\"", "\\", "/", "\b", "\f", "\n", "\r", "\t" }); + + List list = new ArrayList(); + + list.add("b"); + list.add(1); + list.add(new int[] { 10, 12 }); + action.setCollection(list); + + // beans + List collection2 = new ArrayList(); + Bean bean1 = new Bean(); + + bean1.setBigDecimal(new BigDecimal("111111.111111")); + bean1.setBigInteger(new BigInteger("111111111111")); + bean1.setStringField("str"); + bean1.setBooleanField(true); + bean1.setCharField('s'); + bean1.setDoubleField(10.1); + bean1.setFloatField(1.5f); + bean1.setIntField(10); + bean1.setLongField(100); + bean1.setEnumField(AnEnum.ValueA); + bean1.setEnumBean(AnEnumBean.One); + + Bean bean2 = new Bean(); + + bean2.setStringField(" "); + bean2.setBooleanField(false); + bean2.setFloatField(1.1f); + bean2.setDoubleField(2.2); + bean2.setEnumField(AnEnum.ValueB); + bean2.setEnumBean(AnEnumBean.Two); + + // circular reference + bean1.setObjectField(bean2); + bean2.setObjectField(bean1); + + collection2.add(bean1); + action.setCollection2(collection2); + + // keep order in map + Map map = new LinkedHashMap(); + + map.put("a", 1); + map.put("c", new float[] { 1.0f, 2.0f }); + action.setMap(map); + + action.setFoo("foo"); + // should be ignored, marked 'transient' + action.setBar("bar"); + + // date + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, 1999); + calendar.set(Calendar.MONTH, Calendar.DECEMBER); + calendar.set(Calendar.DAY_OF_MONTH, 31); + calendar.set(Calendar.HOUR_OF_DAY, 11); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + action.setDate(calendar.getTime()); + action.setDate2(calendar.getTime()); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("json.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + @SuppressWarnings("unchecked") + public void testHierarchy() throws Exception { + JSONResult result = new JSONResult(); + result.setIgnoreHierarchy(false); + + TestAction3 action = new TestAction3(); + this.invocation.setAction(action); + result.execute(this.invocation); + + String json = response.getContentAsString(); + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("json-4.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + @SuppressWarnings("unchecked") + public void testCommentWrap() throws Exception { + JSONResult result = new JSONResult(); + + TestAction action = new TestAction(); + + // test scape characters + action.setArray(new String[] { "a", "a", "\"", "\\", "/", "\b", "\f", "\n", "\r", "\t" }); + + List list = new ArrayList(); + + list.add("b"); + list.add(1); + list.add(new int[] { 10, 12 }); + action.setCollection(list); + + // beans + List collection2 = new ArrayList(); + Bean bean1 = new Bean(); + + bean1.setStringField("str"); + bean1.setBooleanField(true); + bean1.setCharField('s'); + bean1.setDoubleField(10.1); + bean1.setFloatField(1.5f); + bean1.setIntField(10); + bean1.setLongField(100); + bean1.setEnumField(null); + bean1.setEnumBean(null); + + Bean bean2 = new Bean(); + + bean2.setStringField(" "); + bean2.setBooleanField(false); + bean2.setFloatField(1.1f); + bean2.setDoubleField(2.2); + bean2.setEnumField(AnEnum.ValueC); + bean2.setEnumBean(AnEnumBean.Three); + + // circular reference + bean1.setObjectField(bean2); + bean2.setObjectField(bean1); + + collection2.add(bean1); + action.setCollection2(collection2); + + // keep order in map + Map map = new LinkedHashMap(); + + map.put("a", 1); + map.put("c", new float[] { 1.0f, 2.0f }); + action.setMap(map); + + action.setFoo("foo"); + // should be ignored, marked 'transient' + action.setBar("bar"); + + // date + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, 1999); + calendar.set(Calendar.MONTH, Calendar.DECEMBER); + calendar.set(Calendar.DAY_OF_MONTH, 31); + calendar.set(Calendar.HOUR_OF_DAY, 11); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + action.setDate(calendar.getTime()); + action.setDate2(calendar.getTime()); + + this.invocation.setAction(action); + result.setWrapWithComments(true); + result.execute(this.invocation); + + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("json-3.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + private void executeTest2Action(JSONResult result) throws Exception { + TestAction action = new TestAction(); + + // beans + Bean bean1 = new Bean(); + + bean1.setStringField("str"); + bean1.setBooleanField(true); + bean1.setCharField('s'); + bean1.setDoubleField(10.1); + bean1.setFloatField(1.5f); + bean1.setIntField(10); + bean1.setLongField(100); + bean1.setEnumField(AnEnum.ValueA); + bean1.setEnumBean(AnEnumBean.One); + + // set root + action.setBean(bean1); + result.setRoot("bean"); + + stack.push(action); + this.invocation.setStack(stack); + this.invocation.setAction(action); + + result.execute(this.invocation); + } + + public void test2() throws Exception { + JSONResult result = new JSONResult(); + + executeTest2Action(result); + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("json-2.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + public void testJSONP() throws Exception { + JSONResult result = new JSONResult(); + result.setCallbackParameter("callback"); + request.addParameter("callback", "exec"); + + executeTest2Action(result); + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("jsonp-1.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + public void testNoCache() throws Exception { + JSONResult result = new JSONResult(); + result.setNoCache(true); + + executeTest2Action(result); + + assertEquals("no-cache", response.getHeader("Cache-Control")); + assertEquals("0", response.getHeader("Expires")); + assertEquals("No-cache", response.getHeader("Pragma")); + } + + public void testContentType() throws Exception { + JSONResult result = new JSONResult(); + result.setContentType("some_super_content"); + + executeTest2Action(result); + + assertEquals("some_super_content;charset=ISO-8859-1", response.getContentType()); + } + + public void testStatusCode() throws Exception { + JSONResult result = new JSONResult(); + result.setStatusCode(HttpServletResponse.SC_CONTINUE); + + executeTest2Action(result); + + assertEquals(HttpServletResponse.SC_CONTINUE, response.getStatus()); + } + + /** + * Repeats test2 but with the Enum serialized as a bean + */ + public void test2WithEnumBean() throws Exception { + JSONResult result = new JSONResult(); + result.setEnumAsBean(true); + + executeTest2Action(result); + + String json = response.getContentAsString(); + + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("json-2-enum.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + /** + * Ensures that properties of given root object are read as shallow + * (non-recursive) unless specifically included. + */ + public void testIncludeProperties() throws Exception { + JSONResult result = new JSONResult(); + result.setIncludeProperties("foo"); + TestAction action = new TestAction(); + action.setFoo("fooValue"); + action.setBean(new Bean()); + this.invocation.setAction(action); + result.execute(this.invocation); + + String json = response.getContentAsString(); + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("json-9.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + public void testIncludePropertiesWithList() throws Exception { + JSONResult result = new JSONResult(); + result.setIncludeProperties("^list\\[\\d+\\]\\.booleanField"); + TestAction action = new TestAction(); + + List list = new ArrayList(); + + list.add(new Bean()); + list.add(new Bean()); + list.add(new Bean()); + + action.setList(list); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String json = response.getContentAsString(); + String normalizedActual = TestUtils.normalize(json, true); + String normalizedExpected = TestUtils.normalize(JSONResultTest.class.getResource("json-10.txt")); + assertEquals(normalizedExpected, normalizedActual); + assertEquals("application/json;charset=ISO-8859-1", response.getContentType()); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + this.response = new MockHttpServletResponse(); + this.request = new MockHttpServletRequest(); + this.request.setRequestURI("http://sumeruri"); + this.context = ActionContext.getContext(); + this.context.put(StrutsStatics.HTTP_RESPONSE, this.response); + this.context.put(StrutsStatics.HTTP_REQUEST, this.request); + this.stack = context.getValueStack(); + this.servletContext = new MockServletContext(); + this.context.put(StrutsStatics.SERVLET_CONTEXT, this.servletContext); + this.invocation = new MockActionInvocation(); + this.invocation.setInvocationContext(this.context); + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java new file mode 100644 index 000000000..3f1e3c306 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java @@ -0,0 +1,66 @@ +/* + * $Id$ + * + * 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.json; + +import java.util.Map; + +import junit.framework.TestCase; + +public class JSONUtilTest extends TestCase { + + /** + * Asserts that a bean can be serialized to JSON and restored as a map + */ + public void testSerializeDeserialize() throws Exception { + Bean bean1 = new Bean(); + + bean1.setStringField("str"); + bean1.setBooleanField(true); + bean1.setCharField('s'); + bean1.setDoubleField(10.1); + bean1.setFloatField(1.5f); + bean1.setIntField(10); + bean1.setLongField(100); + bean1.setEnumField(AnEnum.ValueA); + bean1.setEnumBean(AnEnumBean.Two); + + String json = JSONUtil.serialize(bean1); + + Map result = (Map) JSONUtil.deserialize(json); + assertEquals("str", result.get("stringField")); + assertEquals(true, result.get("booleanField")); + assertEquals("s", result.get("charField")); // note: this is a + // String + assertEquals(10.1, result.get("doubleField")); + assertEquals(1.5, result.get("floatField")); // note: this is a + // Double + assertEquals(10L, result.get("intField")); // note: this is a + // Long + assertEquals(AnEnum.ValueA, AnEnum.valueOf((String) result.get("enumField"))); // note: + // this + // is a + // String + assertEquals(AnEnumBean.Two, AnEnumBean.valueOf((String) result.get("enumBean"))); // note: + // this + // is a + // String + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/OtherBean.java b/plugins/json/src/test/java/org/apache/struts2/json/OtherBean.java new file mode 100644 index 000000000..fa6f62933 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/OtherBean.java @@ -0,0 +1,97 @@ +/* + * $Id$ + * + * 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.json; + +public class OtherBean { + private int primitiveInt; + + private Integer intField; + private Boolean booleanField; + private Character charField; + private Long longField; + private Float floatField; + private Double doubleField; + private Byte byteField; + + public Boolean isBooleanField() { + return booleanField; + } + + public void setBooleanField(Boolean booleanField) { + this.booleanField = booleanField; + } + + public Byte getByteField() { + return byteField; + } + + public void setByteField(Byte byteField) { + this.byteField = byteField; + } + + public Character getCharField() { + return charField; + } + + public void setCharField(Character charField) { + this.charField = charField; + } + + public Double getDoubleField() { + return doubleField; + } + + public void setDoubleField(Double doubleField) { + this.doubleField = doubleField; + } + + public Float getFloatField() { + return floatField; + } + + public void setFloatField(Float floatField) { + this.floatField = floatField; + } + + public Integer getIntField() { + return intField; + } + + public void setIntField(Integer intField) { + this.intField = intField; + } + + public Long getLongField() { + return longField; + } + + public void setLongField(Long longField) { + this.longField = longField; + } + + public int getPrimitiveInt() { + return primitiveInt; + } + + public void setPrimitiveInt(int primitiveInt) { + this.primitiveInt = primitiveInt; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest1.java b/plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest1.java new file mode 100644 index 000000000..245ccf2f2 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest1.java @@ -0,0 +1,197 @@ +/* + * $Id$ + * + * 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.json; + +import java.util.List; +import java.util.Map; + +import org.apache.struts2.json.annotations.SMDMethod; + +public class SMDActionTest1 { + private boolean addWasCalled; + @SuppressWarnings("unchecked") + private List listParam; + @SuppressWarnings("unchecked") + private Map mapParam; + private Bean beanParam; + private String stringParam; + private int intParam; + private boolean booleanParam; + private char charParam; + private long longParam; + private float floatParam; + private double doubleParam; + private short shortParam; + private Object objectParam; + private byte byteParam; + + @SMDMethod + public void doSomethingPrimitives(String stringParam, int intParam, boolean booleanParam, char charParam, + long longParam, float floatParam, double doubleParam, short shortParam, byte byteParam) { + this.stringParam = stringParam; + this.intParam = intParam; + this.booleanParam = booleanParam; + this.charParam = charParam; + this.longParam = longParam; + this.floatParam = floatParam; + this.doubleParam = doubleParam; + this.byteParam = byteParam; + this.shortParam = shortParam; + } + + @SuppressWarnings("unchecked") + @SMDMethod + public void doSomethingObjects(Bean beanParam, Map mapParam, List listParam) { + this.beanParam = beanParam; + this.mapParam = mapParam; + this.listParam = listParam; + } + + @SMDMethod + public void add(int a, int b) { + this.addWasCalled = true; + } + + @SMDMethod + public void doSomething() { + + } + + public void methodWithoutAnnotation() { + + } + + public boolean isAddWasCalled() { + return this.addWasCalled; + } + + public void setAddWasCalled(boolean addWasCalled) { + this.addWasCalled = addWasCalled; + } + + @SuppressWarnings("unchecked") + public List getListParam() { + return this.listParam; + } + + @SuppressWarnings("unchecked") + public void setListParam(List listParam) { + this.listParam = listParam; + } + + @SuppressWarnings("unchecked") + public Map getMapParam() { + return this.mapParam; + } + + @SuppressWarnings("unchecked") + public void setMapParam(Map mapParam) { + this.mapParam = mapParam; + } + + public Bean getBeanParam() { + return this.beanParam; + } + + public void setBeanParam(Bean beanParam) { + this.beanParam = beanParam; + } + + public String getStringParam() { + return this.stringParam; + } + + public void setStringParam(String stringParam) { + this.stringParam = stringParam; + } + + public int getIntParam() { + return this.intParam; + } + + public void setIntParam(int intParam) { + this.intParam = intParam; + } + + public boolean isBooleanParam() { + return this.booleanParam; + } + + public void setBooleanParam(boolean booleanParam) { + this.booleanParam = booleanParam; + } + + public char getCharParam() { + return this.charParam; + } + + public void setCharParam(char charParam) { + this.charParam = charParam; + } + + public long getLongParam() { + return this.longParam; + } + + public void setLongParam(long longParam) { + this.longParam = longParam; + } + + public float getFloatParam() { + return this.floatParam; + } + + public void setFloatParam(float floatParam) { + this.floatParam = floatParam; + } + + public double getDoubleParam() { + return this.doubleParam; + } + + public void setDoubleParam(double doubleParam) { + this.doubleParam = doubleParam; + } + + public Object getObjectParam() { + return this.objectParam; + } + + public void setObjectParam(Object objectParam) { + this.objectParam = objectParam; + } + + public byte getByteParam() { + return this.byteParam; + } + + public void setByteParam(byte byteParam) { + this.byteParam = byteParam; + } + + public short getShortParam() { + return this.shortParam; + } + + public void setShortParam(short shortParam) { + this.shortParam = shortParam; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest2.java b/plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest2.java new file mode 100644 index 000000000..f842fc917 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/SMDActionTest2.java @@ -0,0 +1,58 @@ +/* + * $Id$ + * + * 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.json; + +import org.apache.struts2.json.annotations.SMD; +import org.apache.struts2.json.annotations.SMDMethod; +import org.apache.struts2.json.annotations.SMDMethodParameter; + +@SMD(objectName = "testaction", serviceType = "service", version = "10.0") +public class SMDActionTest2 { + private boolean doSomethingInvoked; + + @SMDMethod + public void add(@SMDMethodParameter(name = "a") + int a, @SMDMethodParameter(name = "b") + int b) { + } + + @SMDMethod(name = "doSomethingElse") + public void doSomething() { + doSomethingInvoked = true; + } + + @SMDMethod + public Bean getBean() { + Bean bean = new Bean(); + bean.setStringField("str"); + bean.setBooleanField(true); + bean.setCharField('s'); + bean.setDoubleField(10.1); + bean.setFloatField(1.5f); + bean.setIntField(10); + bean.setLongField(100); + return bean; + } + + public boolean isDoSomethingInvoked() { + return doSomethingInvoked; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/SMDMethodInterfaceTest.java b/plugins/json/src/test/java/org/apache/struts2/json/SMDMethodInterfaceTest.java new file mode 100644 index 000000000..4c098999a --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/SMDMethodInterfaceTest.java @@ -0,0 +1,195 @@ +/* + * $Id$ + * + * 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.json; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import junit.framework.TestCase; + +import org.apache.struts2.json.annotations.SMDMethod; + +/** + * Tests that the SMDMethod annotation can be found in interfaces when + * ignoreSMDMethodInterface is false + */ +public class SMDMethodInterfaceTest extends TestCase { + + public interface InterfaceA { + String getA(); + } + + public interface InterfaceB { + @SMDMethod + String getB(); + } + + public interface InterfaceC { + String getC(); + } + + public interface InterfaceD { + String getD(); + } + + public interface InterfaceE { + String getE(); + } + + public static class ClassA extends ClassB implements InterfaceA { + + private String a; + private String z; + + public ClassA(String a, String b, String c, String d, String e, String x, String y, String z) { + super(b, c, d, e, x, y); + this.a = a; + this.z = z; + } + + public String getA() { + return a; + } + + @SMDMethod + public String getZ() { + return z; + } + } + + public static class ClassB extends ClassC implements InterfaceB, InterfaceC { + + private String b; + private String c; + private String y; + + public ClassB(String b, String c, String d, String e, String x, String y) { + super(d, e, x); + this.b = b; + this.c = c; + this.y = y; + } + + public String getC() { + return c; + } + + public String getB() { + return b; + } + + public String getY() { + return y; + } + } + + public static class ClassC implements InterfaceD, InterfaceE { + + private String d; + private String e; + private String x; + + public ClassC(String d, String e, String x) { + this.d = d; + this.e = e; + this.x = x; + } + + public String getD() { + return d; + } + + public String getE() { + return e; + } + + @SMDMethod + public String getX() { + return x; + } + } + + /** + * Asserts that the SMDMethod annotation is only detected on the classes + * when ignoreSMDMethodInterfaces is true + */ + public void testBaseClassOnly() { + Method[] smdMethodsA = JSONUtil.listSMDMethods(ClassA.class, true); + assertEquals(2, smdMethodsA.length); + assertEquals("getZ", smdMethodsA[0].getName()); + assertEquals("getX", smdMethodsA[1].getName()); + + Method[] smdMethodsB = JSONUtil.listSMDMethods(ClassB.class, true); + assertEquals(1, smdMethodsB.length); + assertEquals("getX", smdMethodsB[0].getName()); + + Method[] smdMethodsC = JSONUtil.listSMDMethods(ClassC.class, true); + assertEquals(1, smdMethodsC.length); + assertEquals("getX", smdMethodsC[0].getName()); + } + + /** + * Asserts that the SMDMethod annotation is also detected on the interfaces + * and superclasses when ignoreSMDMethodInterfaces is false + */ + public void testInterfaces() { + Method[] smdMethodsA = JSONUtil.listSMDMethods(ClassA.class, false); + assertEquals(3, smdMethodsA.length); + assertEquals("getZ", smdMethodsA[0].getName()); + assertEquals("getX", smdMethodsA[1].getName()); + assertEquals("getB", smdMethodsA[2].getName()); + + Method[] smdMethodsB = JSONUtil.listSMDMethods(ClassB.class, false); + assertEquals(2, smdMethodsB.length); + assertEquals("getX", smdMethodsB[0].getName()); + assertEquals("getB", smdMethodsB[1].getName()); + + Method[] smdMethodsC = JSONUtil.listSMDMethods(ClassC.class, false); + assertEquals(1, smdMethodsC.length); + assertEquals("getX", smdMethodsC[0].getName()); + } + + /** + * This is the important case: detects the SMDMethod annotation on a proxy + */ + public void testWithProxy() { + + InvocationHandler handler = new InvocationHandler() { + // dummy implementation + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return null; + } + }; + // proxy is proxy to an impl of ClassA + InterfaceA proxy = (InterfaceA) Proxy.newProxyInstance(ClassA.class.getClassLoader(), new Class[] { + InterfaceA.class, InterfaceB.class, InterfaceC.class }, handler); + + // first, without the recursion + Method[] smdMethodsA = JSONUtil.listSMDMethods(proxy.getClass(), true); + assertEquals(0, smdMethodsA.length); + + // now with the recursion + Method[] smdMethodsB = JSONUtil.listSMDMethods(proxy.getClass(), false); + assertEquals(1, smdMethodsB.length); + assertEquals("getB", smdMethodsB[0].getName()); + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/SimpleValue.java b/plugins/json/src/test/java/org/apache/struts2/json/SimpleValue.java new file mode 100644 index 000000000..ed133ee0f --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/SimpleValue.java @@ -0,0 +1,37 @@ +/* + * $Id$ + * + * 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.json; + +public class SimpleValue { + + private String value; + + public SimpleValue() { + } + + public String getValue() { + return value; + } + + public void setValue(String v) { + this.value = v; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/TestAction.java b/plugins/json/src/test/java/org/apache/struts2/json/TestAction.java new file mode 100644 index 000000000..7251fc4e3 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/TestAction.java @@ -0,0 +1,188 @@ +/* + * $Id$ + * + * 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.json; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.apache.struts2.json.annotations.JSON; + +import com.opensymphony.xwork2.Action; +import com.opensymphony.xwork2.ActionSupport; + +/** + */ +@SuppressWarnings("unchecked") +public class TestAction extends ActionSupport { + private static final long serialVersionUID = -8891365561914451494L; + private List collection; + private List collection2; + private Map map; + private String foo; + private String result; + private String[] array; + private Bean[] beanArray; + private int[] intArray; + private List list; + private String bar; + private String nogetter; + private Date date2; + private Bean bean; + private Date date; + private String foo2 = null; + + public Bean getBean() { + return this.bean; + } + + public void setBean(Bean bean) { + this.bean = bean; + } + + public List getCollection() { + return this.collection; + } + + public void setCollection(List collection) { + this.collection = collection; + } + + public List getCollection2() { + return this.collection2; + } + + public void setCollection2(List collection2) { + this.collection2 = collection2; + } + + public Map getMap() { + return this.map; + } + + public void setMap(Map map) { + this.map = map; + } + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getResult() { + return this.result; + } + + public void setResult(String result) { + this.result = result; + } + + public String[] getArray() { + return this.array; + } + + public void setArray(String[] array) { + this.array = array; + } + + public List getList() { + return this.list; + } + + public void setList(List list) { + this.list = list; + } + + @Override + public String execute() throws Exception { + if (this.result == null) { + this.result = Action.SUCCESS; + } + + return this.result; + } + + public String doInput() throws Exception { + return INPUT; + } + + public void setBar(String bar) { + this.bar = bar; + } + + @JSON(serialize = false) + public String getBar() { + return this.bar; + } + + public void setNogetter(String nogetter) { + this.nogetter = nogetter; + } + + @JSON(serialize = false) + public int[] getIntArray() { + return this.intArray; + } + + public void setIntArray(int[] intArray) { + this.intArray = intArray; + } + + @JSON(serialize = false) + public Bean[] getBeanArray() { + return this.beanArray; + } + + public void setBeanArray(Bean[] beanArray) { + this.beanArray = beanArray; + } + + public Date getDate() { + return this.date; + } + + public void setDate(Date date) { + this.date = date; + } + + @JSON(format = "dd/MM/yy") + public Date getDate2() { + return this.date2; + } + + @JSON(format = "dd/MM/yy") + public void setDate2(Date date2) { + this.date2 = date2; + } + + @JSON(serialize = false) + public String getFoo2() { + return this.foo2; + } + + @JSON(deserialize = false) + public void setFoo2(String foo2) { + this.foo2 = foo2; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/TestAction2.java b/plugins/json/src/test/java/org/apache/struts2/json/TestAction2.java new file mode 100644 index 000000000..6ccf85a0d --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/TestAction2.java @@ -0,0 +1,27 @@ +/* + * $Id$ + * + * 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.json; + +public class TestAction2 { + public String getName() { + return "name"; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/TestAction3.java b/plugins/json/src/test/java/org/apache/struts2/json/TestAction3.java new file mode 100644 index 000000000..516eba22c --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/TestAction3.java @@ -0,0 +1,27 @@ +/* + * $Id$ + * + * 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.json; + +public class TestAction3 extends TestAction2 { + public String getName2() { + return "name"; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/TestAction4.java b/plugins/json/src/test/java/org/apache/struts2/json/TestAction4.java new file mode 100644 index 000000000..fa7dcf93b --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/TestAction4.java @@ -0,0 +1,35 @@ +/* + * $Id$ + * + * 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.json; + +public class TestAction4 { + private Bean bean; + + public Bean getBean() { + if (this.bean == null) + this.bean = new Bean(); + return this.bean; + } + + public void setBean(Bean bean) { + this.bean = bean; + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/TestUtils.java b/plugins/json/src/test/java/org/apache/struts2/json/TestUtils.java new file mode 100644 index 000000000..96f6f60e9 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/TestUtils.java @@ -0,0 +1,95 @@ +/* + * $Id$ + * + * 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.json; + +import java.io.InputStream; +import java.net.URL; +import java.util.StringTokenizer; + +/** + * Utility methods for test classes + */ +public class TestUtils { + /** + * normalizes a string so that strings generated on different platforms can + * be compared. any group of one or more space, tab, \r, and \n characters + * are converted to a single space character + * + * @param obj + * the object to be normalized. normalize will perform its + * operation on obj.toString().trim() ; + * @param appendSpace + * @return the normalized string + */ + public static String normalize(Object obj, boolean appendSpace) { + StringTokenizer st = new StringTokenizer(obj.toString().trim(), " \t\r\n"); + StringBuffer buffer = new StringBuffer(128); + + while (st.hasMoreTokens()) { + buffer.append(st.nextToken()); + } + + return buffer.toString(); + } + + public static String normalize(URL url) throws Exception { + return normalize(readContent(url), true); + } + + /** + * Attempt to verify the contents of text against the contents of the URL + * specified. Performs a trim on both ends + * + * @param url + * the HTML snippet that we want to validate against + * @throws Exception + * if the validation failed + */ + public static boolean compare(URL url, String text) throws Exception { + /** + * compare the trimmed values of each buffer and make sure they're + * equivalent. however, let's make sure to normalize the strings first + * to account for line termination differences between platforms. + */ + String writerString = TestUtils.normalize(text, true); + String bufferString = TestUtils.normalize(readContent(url), true); + + return bufferString.equals(writerString); + } + + public static String readContent(URL url) throws Exception { + if (url == null) + throw new Exception("unable to verify a null URL"); + + StringBuffer buffer = new StringBuffer(128); + InputStream in = url.openStream(); + byte[] buf = new byte[4096]; + int nbytes; + + while ((nbytes = in.read(buf)) > 0) { + buffer.append(new String(buf, 0, nbytes)); + } + + in.close(); + + return buffer.toString(); + } +} diff --git a/plugins/json/src/test/java/org/apache/struts2/json/WrapperClassBean.java b/plugins/json/src/test/java/org/apache/struts2/json/WrapperClassBean.java new file mode 100644 index 000000000..65956fc38 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/WrapperClassBean.java @@ -0,0 +1,181 @@ +/* + * $Id$ + * + * 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.json; + +import java.util.List; +import java.util.Map; + +public class WrapperClassBean { + + private String stringField; + private Integer intField; + private int nullIntField; + private Boolean booleanField; + private boolean primitiveBooleanField1; + private boolean primitiveBooleanField2; + private boolean primitiveBooleanField3; + private Character charField; + private Long longField; + private Float floatField; + private Double doubleField; + private Object objectField; + private Byte byteField; + private List listField; + private List> listMapField; + private Map> mapListField; + private Map[] arrayMapField; + + public List getListField() { + return listField; + } + + public void setListField(List listField) { + this.listField = listField; + } + + public List> getListMapField() { + return listMapField; + } + + public void setListMapField(List> listMapField) { + this.listMapField = listMapField; + } + + public Map> getMapListField() { + return mapListField; + } + + public void setMapListField(Map> mapListField) { + this.mapListField = mapListField; + } + + public Map[] getArrayMapField() { + return arrayMapField; + } + + public void setArrayMapField(Map[] arrayMapField) { + this.arrayMapField = arrayMapField; + } + + public Boolean getBooleanField() { + return booleanField; + } + + public void setBooleanField(Boolean booleanField) { + this.booleanField = booleanField; + } + + public boolean isPrimitiveBooleanField1() { + return primitiveBooleanField1; + } + + public void setPrimitiveBooleanField1(boolean primitiveBooleanField1) { + this.primitiveBooleanField1 = primitiveBooleanField1; + } + + public boolean isPrimitiveBooleanField2() { + return primitiveBooleanField2; + } + + public void setPrimitiveBooleanField2(boolean primitiveBooleanField2) { + this.primitiveBooleanField2 = primitiveBooleanField2; + } + + public boolean isPrimitiveBooleanField3() { + return primitiveBooleanField3; + } + + public void setPrimitiveBooleanField3(boolean primitiveBooleanField3) { + this.primitiveBooleanField3 = primitiveBooleanField3; + } + + public Byte getByteField() { + return byteField; + } + + public void setByteField(Byte byteField) { + this.byteField = byteField; + } + + public Character getCharField() { + return charField; + } + + public void setCharField(Character charField) { + this.charField = charField; + } + + public Double getDoubleField() { + return doubleField; + } + + public void setDoubleField(Double doubleField) { + this.doubleField = doubleField; + } + + public Float getFloatField() { + return floatField; + } + + public void setFloatField(Float floatField) { + this.floatField = floatField; + } + + public Integer getIntField() { + return intField; + } + + public void setIntField(Integer intField) { + this.intField = intField; + } + + public int getNullIntField() { + return nullIntField; + } + + public void setNullIntField(int nullIntField) { + this.nullIntField = nullIntField; + } + + public Long getLongField() { + return longField; + } + + public void setLongField(Long longField) { + this.longField = longField; + } + + public Object getObjectField() { + return objectField; + } + + public void setObjectField(Object objectField) { + this.objectField = objectField; + } + + public String getStringField() { + return stringField; + } + + public void setStringField(String stringField) { + this.stringField = stringField; + } +} diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/bad-1.txt b/plugins/json/src/test/resources/org/apache/struts2/json/bad-1.txt new file mode 100644 index 000000000..52cbbe3b2 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/bad-1.txt @@ -0,0 +1,3 @@ +{ + aField: NaN +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/bad-2.txt b/plugins/json/src/test/resources/org/apache/struts2/json/bad-2.txt new file mode 100644 index 000000000..106980249 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/bad-2.txt @@ -0,0 +1,2 @@ +{ + aField: 10 diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/bad-3.txt b/plugins/json/src/test/resources/org/apache/struts2/json/bad-3.txt new file mode 100644 index 000000000..4286558c6 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/bad-3.txt @@ -0,0 +1,3 @@ + + aField: 10 + } \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/bad-4.txt b/plugins/json/src/test/resources/org/apache/struts2/json/bad-4.txt new file mode 100644 index 000000000..a6704fd01 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/bad-4.txt @@ -0,0 +1 @@ +bad 10 \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/bad-5.txt b/plugins/json/src/test/resources/org/apache/struts2/json/bad-5.txt new file mode 100644 index 000000000..68b1c616b --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/bad-5.txt @@ -0,0 +1 @@ +{ a: "aaaa } \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/bad-to-the-bone.txt b/plugins/json/src/test/resources/org/apache/struts2/json/bad-to-the-bone.txt new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-1.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-1.txt new file mode 100644 index 000000000..c8115c664 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-1.txt @@ -0,0 +1,49 @@ +{ + "date": "1999-12-31T23:59:59", + "date2": "31\/12\/99", + "array": ["str0", "str1"], + "intArray": [1, 2], + "beanArray": [ { + "stringField": "bean1" + }, + { + "stringField": "bean2" + } + ], + "list": ["a", "a", "\"", "\\", "\/", "\b", "\f", "\n", "\r", "\t"], + "collection": ["b", 1, [10, 12]], + "collection2": [{ + "booleanField": true, + "charField": "s", + "doubleField": 10.1, + "floatField": 1.5, + "intField": 10, + "longField": 100, + "objectField": { + "booleanField": false, + "charField": "\u0000", + "doubleField": 2.2, + "floatField": 1.1, + "intField": 0, + "longField":0, + "objectField":null, + "stringField":" " + }, + "stringField": "str" + }], + "foo": "foo", + "map": { + "a": 1, + "c": [1.0, 2.0] + }, + "result": null, + "bean": { + "booleanField": true, + "stringField" : "test", + "intField" : 10, + "charField": "s", + "doubleField": 10.1, + "byteField": 3 + }, + "foo2" : "ignoreme" +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-10.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-10.txt new file mode 100644 index 000000000..e635f9c7a --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-10.txt @@ -0,0 +1,3 @@ +{ + "list":[{"booleanField":false},{"booleanField":false},{"booleanField":false}] +} diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-2-enum.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-2-enum.txt new file mode 100644 index 000000000..de79079d1 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-2-enum.txt @@ -0,0 +1,21 @@ +{ + "bigDecimal": null, + "bigInteger": null, + "booleanField": true, + "byteField": 0, + "charField": "s", + "doubleField": 10.1, + "enumBean": { + "propA":"A", + "propB":"B", + "_name":"One" + }, + "enumField": { + "_name":"ValueA" + }, + "floatField": 1.5, + "intField": 10, + "longField": 100, + "objectField": null, + "stringField": "str" +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-2.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-2.txt new file mode 100644 index 000000000..2583a007b --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-2.txt @@ -0,0 +1,15 @@ +{ + "bigDecimal": null, + "bigInteger": null, + "booleanField": true, + "byteField": 0, + "charField": "s", + "doubleField": 10.1, + "enumBean": "One", + "enumField": "ValueA", + "floatField": 1.5, + "intField": 10, + "longField": 100, + "objectField": null, + "stringField": "str" +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-3.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-3.txt new file mode 100644 index 000000000..c38a233cd --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-3.txt @@ -0,0 +1,43 @@ +/* { + "array": ["a", "a", "\"", "\\", "\/", "\b", "\f", "\n", "\r", "\t"], + "bean": null, + "collection": ["b", 1, [10, 12]], + "collection2": [{ + "bigDecimal": null, + "bigInteger": null, + "booleanField": true, + "byteField": 0, + "charField": "s", + "doubleField": 10.1, + "enumBean": null, + "enumField": null, + "floatField": 1.5, + "intField": 10, + "longField": 100, + "objectField": { + "bigDecimal": null, + "bigInteger": null, + "booleanField": false, + "byteField": 0, + "charField": "\u0000", + "doubleField": 2.2, + "enumBean": "Three", + "enumField": "ValueC", + "floatField": 1.1, + "intField": 0, + "longField":0, + "objectField":null, + "stringField":" " + }, + "stringField": "str" + }], + "date": "1999-12-31T11:59:59", + "date2": "31\/12\/99", + "foo": "foo", + "list": null, + "map": { + "a": 1, + "c": [1.0, 2.0] + }, + "result":null +} */ \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-4.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-4.txt new file mode 100644 index 000000000..8dda46897 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-4.txt @@ -0,0 +1,4 @@ +{ + "name":"name", + "name2":"name" +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-5.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-5.txt new file mode 100644 index 000000000..44ba97f73 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-5.txt @@ -0,0 +1,8 @@ +{ + "booleanField": true, + "stringField" : "test", + "intField" : 10, + "charField": "s", + "doubleField": 10.1, + "byteField": 3 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-6.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-6.txt new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-6.txt @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-7.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-7.txt new file mode 100644 index 000000000..3ee1b612a --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-7.txt @@ -0,0 +1,20 @@ +{ + "bigDecimal": 111111.5, + "bigInteger": 111111, + "booleanField": true, + "primitiveBooleanField1": true, + "primitiveBooleanField2": false, + "primitiveBooleanField3": null, + "stringField" : "test\u000e\u000F", + "intField" : 10, + "nullIntField" : null, + "charField": "s", + "doubleField": 10.1, + "byteField": 3, + "objectField": { "empty": "to test issue 28 (http://code.google.com/p/jsonplugin/issues/detail?id=28) the value of objectField should be simply empty curly braces { } " }, + "enumField": "ValueA", + "listField": [{"value":"1"},{"value":"2"}], + "listMapField": [{"id1":2073501,"id2":3}], + "mapListField": {"id1":[1,2,3],"id2":[4,3,2,1]}, + "arrayMapField": [{"id1":2073501,"id2":3}] +} diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-8.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-8.txt new file mode 100644 index 000000000..995b3e0c3 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-8.txt @@ -0,0 +1,13 @@ +{ + "booleanField": "true", + "stringField" : "test", + "intField" : "10", + "charField": "s", + "doubleField": "10.1", + "byteField": "3", + "objectField": { "empty": "to test issue 28 (http://code.google.com/p/jsonplugin/issues/detail?id=28) the value of objectField should be simply empty curly braces { } " }, + "listField": null, + "listMapField": null, + "mapListField": null, + "arrayMapField": null +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json-9.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json-9.txt new file mode 100644 index 000000000..7b735ce0a --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json-9.txt @@ -0,0 +1,3 @@ +{ + "foo":"fooValue" +} diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/json.txt b/plugins/json/src/test/resources/org/apache/struts2/json/json.txt new file mode 100644 index 000000000..51b1a4e36 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/json.txt @@ -0,0 +1,43 @@ +{ + "array": ["a", "a", "\"", "\\", "\/", "\b", "\f", "\n", "\r", "\t"], + "bean": null, + "collection": ["b", 1, [10, 12]], + "collection2": [{ + "bigDecimal": 111111.111111, + "bigInteger": 111111111111, + "booleanField": true, + "byteField": 0, + "charField": "s", + "doubleField": 10.1, + "enumBean": "One", + "enumField": "ValueA", + "floatField": 1.5, + "intField": 10, + "longField": 100, + "objectField": { + "bigDecimal": null, + "bigInteger": null, + "booleanField": false, + "byteField": 0, + "charField": "\u0000", + "doubleField": 2.2, + "enumBean": "Two", + "enumField": "ValueB", + "floatField": 1.1, + "intField": 0, + "longField":0, + "objectField":null, + "stringField":" " + }, + "stringField": "str" + }], + "date": "1999-12-31T11:59:59", + "date2": "31\/12\/99", + "foo": "foo", + "list": null, + "map": { + "a": 1, + "c": [1.0, 2.0] + }, + "result":null +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/jsonp-1.txt b/plugins/json/src/test/resources/org/apache/struts2/json/jsonp-1.txt new file mode 100644 index 000000000..18efd3355 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/jsonp-1.txt @@ -0,0 +1,15 @@ +exec({ + "bigDecimal": null, + "bigInteger": null, + "booleanField": true, + "byteField": 0, + "charField": "s", + "doubleField": 10.1, + "enumBean": "One", + "enumField": "ValueA", + "floatField": 1.5, + "intField": 10, + "longField": 100, + "objectField": null, + "stringField": "str" +}) \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/nulls-1.txt b/plugins/json/src/test/resources/org/apache/struts2/json/nulls-1.txt new file mode 100644 index 000000000..7e5d1bed4 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/nulls-1.txt @@ -0,0 +1 @@ +{"foo":"fool"} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/prefix-1.txt b/plugins/json/src/test/resources/org/apache/struts2/json/prefix-1.txt new file mode 100644 index 000000000..560dd6dfb --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/prefix-1.txt @@ -0,0 +1 @@ +{}&& {"foo":"fool"} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-1.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-1.txt new file mode 100644 index 000000000..ba9e555d1 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-1.txt @@ -0,0 +1,41 @@ +{ + "methods":[ + { + "name":"add", + "parameters":[ + {"name":"p0"}, + {"name":"p1"} + ] + }, + { + "name":"doSomething", + "parameters":[] + }, + { + "name":"doSomethingObjects", + "parameters":[ + {"name":"p0"}, + {"name":"p1"}, + {"name":"p2"} + ] + }, + { + "name":"doSomethingPrimitives", + "parameters":[ + {"name":"p0"}, + {"name":"p1"}, + {"name":"p2"}, + {"name":"p3"}, + {"name":"p4"}, + {"name":"p5"}, + {"name":"p6"}, + {"name":"p7"}, + {"name":"p8"} + ] + } + ], + "objectName":null, + "serviceType":"JSON-RPC", + "serviceUrl":"http:\/\/sumeruri", + "version":".1" +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-10.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-10.txt new file mode 100644 index 000000000..89e014aee --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-10.txt @@ -0,0 +1,4 @@ +{ + "method": "getBean", + "id":15 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-11.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-11.txt new file mode 100644 index 000000000..11b9489d3 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-11.txt @@ -0,0 +1,5 @@ +{ + "error":null, + "id":"2", + "result":null +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-12.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-12.txt new file mode 100644 index 000000000..357d0d0ce --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-12.txt @@ -0,0 +1,19 @@ +{ + "error":null, + "id":"15", + "result": { + "bigDecimal": null, + "bigInteger": null, + "booleanField": true, + "byteField": 0, + "charField": "s", + "doubleField": 10.1, + "enumBean": null, + "enumField": null, + "floatField": 1.5, + "intField": 10, + "longField": 100, + "objectField": null, + "stringField": "str" + } +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-13.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-13.txt new file mode 100644 index 000000000..58eca3363 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-13.txt @@ -0,0 +1,10 @@ +{ + "error": { + "code":100, + "message": "'method' is required for JSON RPC", + "name":null, + "stack":null + }, + "id":"1", + "result":null +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-14.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-14.txt new file mode 100644 index 000000000..7c6f6b264 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-14.txt @@ -0,0 +1,5 @@ +{ + "params": [], + "method": "doSomething", + "id":1 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-15.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-15.txt new file mode 100644 index 000000000..033eaf33f --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-15.txt @@ -0,0 +1,5 @@ +{ + "params": [], + "method": "doSomethingElse", + "id":1 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-2.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-2.txt new file mode 100644 index 000000000..1a9d2f558 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-2.txt @@ -0,0 +1,24 @@ +{ + "methods": [ + { + "name": "add", + "parameters": [ + {"name":"a"}, + {"name":"b"} + ] + }, + { + "name": "doSomethingElse", + "parameters":[] + }, + { + "name": "getBean", + "parameters":[] + } + ], + "objectName": "testaction", + "serviceType": "service", + "serviceUrl":"http:\/\/sumeruri", + "version": "10.0" +} + diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-3.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-3.txt new file mode 100644 index 000000000..c143297e1 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-3.txt @@ -0,0 +1,5 @@ +{ + "params": [0, 0], + "method": "add", + "id":1 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-4.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-4.txt new file mode 100644 index 000000000..f1d6fb9df --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-4.txt @@ -0,0 +1,4 @@ +{ + "params": [0, 0], + "id":1 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-5.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-5.txt new file mode 100644 index 000000000..1aa620808 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-5.txt @@ -0,0 +1,5 @@ +{ + "params": [0, 0], + "method": "zzz", + "id":1 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-6.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-6.txt new file mode 100644 index 000000000..9aba76639 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-6.txt @@ -0,0 +1,5 @@ +{ + "params": ["string", 1, true, 'c', 2, 3.3, 4.4, 5, 6], + "method": "doSomethingPrimitives", + "id":"2" +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-7.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-7.txt new file mode 100644 index 000000000..f324fe197 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-7.txt @@ -0,0 +1,19 @@ +{ + "params": [ + { + "booleanField": true, + "stringField" : "test", + "intField" : 10, + "charField": "s", + "doubleField": 10.1, + "byteField": 3 + }, + { + "a": 1, + "c": [1.0, 2.0] + }, + ["str0", "str1"] + ], + "method": "doSomethingObjects", + "id":"2" +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-8.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-8.txt new file mode 100644 index 000000000..e50a6c36a --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-8.txt @@ -0,0 +1,16 @@ +{ + "addWasCalled":false, + "beanParam":null, + "booleanParam":false, + "byteParam":0, + "charParam":"\u0000", + "doubleParam":0.0, + "floatParam":0.0, + "intParam":0, + "listParam":null, + "longParam":0, + "mapParam":null, + "objectParam":null, + "shortParam":0, + "stringParam":null +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd-9.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd-9.txt new file mode 100644 index 000000000..41723f8a1 --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd-9.txt @@ -0,0 +1,5 @@ +{ + "params": [0, 0], + "method": "methodWithoutAnnotation", + "id":1 +} \ No newline at end of file diff --git a/plugins/json/src/test/resources/org/apache/struts2/json/smd.txt b/plugins/json/src/test/resources/org/apache/struts2/json/smd.txt new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/plugins/json/src/test/resources/org/apache/struts2/json/smd.txt @@ -0,0 +1 @@ +{} \ No newline at end of file