/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 *
 *
 *
 */

package com.liferay.portal.action;

import com.liferay.petra.string.StringPool;
import com.liferay.portal.kernel.json.JSONArray;
import com.liferay.portal.kernel.json.JSONException;
import com.liferay.portal.kernel.json.JSONFactoryUtil;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.json.JSONSerializable;
import com.liferay.portal.kernel.json.JSONSerializer;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.security.access.control.AccessControlThreadLocal;
import com.liferay.portal.kernel.service.ServiceContext;
import com.liferay.portal.kernel.service.ServiceContextUtil;
import com.liferay.portal.kernel.util.ArrayUtil;
import com.liferay.portal.kernel.util.ClassUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.LocaleUtil;
import com.liferay.portal.kernel.util.LocalizationUtil;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.PortalUtil;
import com.liferay.portal.kernel.util.SetUtil;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.struts.JSONAction;
import com.liferay.portal.util.PropsValues;

import java.lang.reflect.Method;
import java.lang.reflect.Type;

import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionMapping;

/**
 * @author Brian Wing Shun Chan
 * @author Karthik Sudarshan
 * @author Julio Camarero
 * @author Eduardo Lundgren
 */
public class JSONServiceAction extends JSONAction {

	public JSONServiceAction() {
		_invalidClassNames = SetUtil.fromArray(
			PropsValues.JSON_SERVICE_INVALID_CLASS_NAMES);

		_invalidMethodNames = SetUtil.fromArray(
			PropsValues.JSON_SERVICE_INVALID_METHOD_NAMES);

		if (_log.isDebugEnabled()) {
			for (String invalidClassName : _invalidClassNames) {
				_log.debug("Invalid class name " + invalidClassName);
			}

			for (String invalidMethodName : _invalidMethodNames) {
				_log.debug("Invalid method name " + invalidMethodName);
			}
		}
	}

	@Override
	public String getJSON(
			ActionMapping actionMapping, ActionForm actionForm,
			HttpServletRequest request, HttpServletResponse response)
		throws Exception {

		String className = ParamUtil.getString(request, "serviceClassName");
		String methodName = ParamUtil.getString(request, "serviceMethodName");

		String[] serviceParameters = getStringArrayFromJSON(
			request, "serviceParameters");
		String[] serviceParameterTypes = getStringArrayFromJSON(
			request, "serviceParameterTypes");

		if (!isValidRequest(request)) {
			return null;
		}

		Thread currentThread = Thread.currentThread();

		ClassLoader contextClassLoader = currentThread.getContextClassLoader();

		Class<?> clazz = contextClassLoader.loadClass(className);

		Object[] methodAndParameterTypes = getMethodAndParameterTypes(
			clazz, methodName, serviceParameters, serviceParameterTypes);

		if (methodAndParameterTypes == null) {
			return null;
		}

		Method method = (Method)methodAndParameterTypes[0];

		Type[] parameterTypes = (Type[])methodAndParameterTypes[1];
		Object[] args = new Object[serviceParameters.length];

		for (int i = 0; i < serviceParameters.length; i++) {
			args[i] = getArgValue(
				request, clazz, methodName, serviceParameters[i],
				parameterTypes[i]);
		}

		try {
			if (_log.isDebugEnabled()) {
				_log.debug(
					StringBundler.concat(
						"Invoking ", String.valueOf(clazz), " on method ",
						method.getName(), " with args ",
						Arrays.toString(args)));
			}

			Object returnObj = null;

			boolean remoteAccess = AccessControlThreadLocal.isRemoteAccess();

			try {
				AccessControlThreadLocal.setRemoteAccess(true);

				returnObj = method.invoke(clazz, args);
			}
			finally {
				AccessControlThreadLocal.setRemoteAccess(remoteAccess);
			}

			if (returnObj != null) {
				return getReturnValue(returnObj);
			}

			return JSONFactoryUtil.getNullJSON();
		}
		catch (Exception e) {
			if (_log.isDebugEnabled()) {
				_log.debug(
					StringBundler.concat(
						"Invoked ", String.valueOf(clazz), " on method ",
						method.getName(), " with args ", Arrays.toString(args)),
					e);
			}

			if (PropsValues.JSON_SERVICE_SERIALIZE_THROWABLE) {
				return JSONFactoryUtil.serializeThrowable(e);
			}

			PortalUtil.sendError(
				HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e, request,
				response);

			return JSONFactoryUtil.getNullJSON();
		}
	}

	protected Object getArgValue(
			HttpServletRequest request, Class<?> clazz, String methodName,
			String parameter, Type parameterType)
		throws Exception {

		String typeNameOrClassDescriptor = getTypeNameOrClassDescriptor(
			parameterType);

		String value = ParamUtil.getString(request, parameter);

		if (Validator.isNull(value) &&
			!typeNameOrClassDescriptor.equals("[Ljava.lang.String;")) {

			return null;
		}
		else if (typeNameOrClassDescriptor.equals("boolean") ||
				 typeNameOrClassDescriptor.equals(Boolean.class.getName())) {

			return Boolean.valueOf(ParamUtil.getBoolean(request, parameter));
		}
		else if (typeNameOrClassDescriptor.equals("double") ||
				 typeNameOrClassDescriptor.equals(Double.class.getName())) {

			return Double.valueOf(ParamUtil.getDouble(request, parameter));
		}
		else if (typeNameOrClassDescriptor.equals("int") ||
				 typeNameOrClassDescriptor.equals(Integer.class.getName())) {

			return Integer.valueOf(ParamUtil.getInteger(request, parameter));
		}
		else if (typeNameOrClassDescriptor.equals("long") ||
				 typeNameOrClassDescriptor.equals(Long.class.getName())) {

			return Long.valueOf(ParamUtil.getLong(request, parameter));
		}
		else if (typeNameOrClassDescriptor.equals("short") ||
				 typeNameOrClassDescriptor.equals(Short.class.getName())) {

			return Short.valueOf(ParamUtil.getShort(request, parameter));
		}
		else if (typeNameOrClassDescriptor.equals(Calendar.class.getName())) {
			Calendar cal = Calendar.getInstance(LocaleUtil.getDefault());

			cal.setTimeInMillis(ParamUtil.getLong(request, parameter));

			return cal;
		}
		else if (typeNameOrClassDescriptor.equals(Date.class.getName())) {
			return new Date(ParamUtil.getLong(request, parameter));
		}
		else if (typeNameOrClassDescriptor.equals(
					ServiceContext.class.getName())) {

			JSONObject jsonObject = JSONFactoryUtil.createJSONObject(value);

			jsonObject.put("javaClass", ServiceContext.class.getName());

			return ServiceContextUtil.deserialize(jsonObject);
		}
		else if (typeNameOrClassDescriptor.equals(String.class.getName())) {
			return value;
		}
		else if (typeNameOrClassDescriptor.equals("[Z")) {
			return ParamUtil.getBooleanValues(request, parameter);
		}
		else if (typeNameOrClassDescriptor.equals("[D")) {
			return ParamUtil.getDoubleValues(request, parameter);
		}
		else if (typeNameOrClassDescriptor.equals("[F")) {
			return ParamUtil.getFloatValues(request, parameter);
		}
		else if (typeNameOrClassDescriptor.equals("[I")) {
			return ParamUtil.getIntegerValues(request, parameter);
		}
		else if (typeNameOrClassDescriptor.equals("[J")) {
			return ParamUtil.getLongValues(request, parameter);
		}
		else if (typeNameOrClassDescriptor.equals("[S")) {
			return ParamUtil.getShortValues(request, parameter);
		}
		else if (typeNameOrClassDescriptor.equals("[Ljava.lang.String;")) {
			return ParamUtil.getParameterValues(request, parameter);
		}
		else if (typeNameOrClassDescriptor.equals("[[Z")) {
			String[] values = request.getParameterValues(parameter);

			if (ArrayUtil.isNotEmpty(values)) {
				String[] values0 = StringUtil.split(values[0]);

				boolean[][] doubleArray =
					new boolean[values.length][values0.length];

				for (int i = 0; i < values.length; i++) {
					String[] curValues = StringUtil.split(values[i]);

					for (int j = 0; j < curValues.length; j++) {
						doubleArray[i][j] = GetterUtil.getBoolean(curValues[j]);
					}
				}

				return doubleArray;
			}

			return new boolean[0][0];
		}
		else if (typeNameOrClassDescriptor.equals("[[D")) {
			String[] values = request.getParameterValues(parameter);

			if (ArrayUtil.isNotEmpty(values)) {
				String[] values0 = StringUtil.split(values[0]);

				double[][] doubleArray =
					new double[values.length][values0.length];

				for (int i = 0; i < values.length; i++) {
					String[] curValues = StringUtil.split(values[i]);

					for (int j = 0; j < curValues.length; j++) {
						doubleArray[i][j] = GetterUtil.getDouble(curValues[j]);
					}
				}

				return doubleArray;
			}

			return new double[0][0];
		}
		else if (typeNameOrClassDescriptor.equals("[[F")) {
			String[] values = request.getParameterValues(parameter);

			if (ArrayUtil.isNotEmpty(values)) {
				String[] values0 = StringUtil.split(values[0]);

				float[][] doubleArray =
					new float[values.length][values0.length];

				for (int i = 0; i < values.length; i++) {
					String[] curValues = StringUtil.split(values[i]);

					for (int j = 0; j < curValues.length; j++) {
						doubleArray[i][j] = GetterUtil.getFloat(curValues[j]);
					}
				}

				return doubleArray;
			}

			return new float[0][0];
		}
		else if (typeNameOrClassDescriptor.equals("[[I")) {
			String[] values = request.getParameterValues(parameter);

			if (ArrayUtil.isNotEmpty(values)) {
				String[] values0 = StringUtil.split(values[0]);

				int[][] doubleArray = new int[values.length][values0.length];

				for (int i = 0; i < values.length; i++) {
					String[] curValues = StringUtil.split(values[i]);

					for (int j = 0; j < curValues.length; j++) {
						doubleArray[i][j] = GetterUtil.getInteger(curValues[j]);
					}
				}

				return doubleArray;
			}

			return new int[0][0];
		}
		else if (typeNameOrClassDescriptor.equals("[[J")) {
			String[] values = request.getParameterValues(parameter);

			if (ArrayUtil.isNotEmpty(values)) {
				String[] values0 = StringUtil.split(values[0]);

				long[][] doubleArray = new long[values.length][values0.length];

				for (int i = 0; i < values.length; i++) {
					String[] curValues = StringUtil.split(values[i]);

					for (int j = 0; j < curValues.length; j++) {
						doubleArray[i][j] = GetterUtil.getLong(curValues[j]);
					}
				}

				return doubleArray;
			}

			return new long[0][0];
		}
		else if (typeNameOrClassDescriptor.equals("[[S")) {
			String[] values = request.getParameterValues(parameter);

			if (ArrayUtil.isNotEmpty(values)) {
				String[] values0 = StringUtil.split(values[0]);

				short[][] doubleArray =
					new short[values.length][values0.length];

				for (int i = 0; i < values.length; i++) {
					String[] curValues = StringUtil.split(values[i]);

					for (int j = 0; j < curValues.length; j++) {
						doubleArray[i][j] = GetterUtil.getShort(curValues[j]);
					}
				}

				return doubleArray;
			}

			return new short[0][0];
		}
		else if (typeNameOrClassDescriptor.equals("[[Ljava.lang.String")) {
			String[] values = request.getParameterValues(parameter);

			if (ArrayUtil.isNotEmpty(values)) {
				String[] values0 = StringUtil.split(values[0]);

				String[][] doubleArray =
					new String[values.length][values0.length];

				for (int i = 0; i < values.length; i++) {
					doubleArray[i] = StringUtil.split(values[i]);
				}

				return doubleArray;
			}

			return new String[0][0];
		}
		else if (typeNameOrClassDescriptor.equals(
					"java.util.Map<java.util.Locale, java.lang.String>")) {

			JSONObject jsonObject = JSONFactoryUtil.createJSONObject(value);

			return LocalizationUtil.deserialize(jsonObject);
		}
		else {
			try {
				return JSONFactoryUtil.looseDeserialize(value);
			}
			catch (Exception e) {
				_log.error(
					StringBundler.concat(
						"Unsupported parameter type for class ",
						String.valueOf(clazz), ", method ", methodName,
						", parameter ", parameter, ", and type ",
						typeNameOrClassDescriptor));

				return null;
			}
		}
	}

	/**
	 * @see com.liferay.portal.jsonwebservice.JSONWebServiceServiceAction#getCSRFOrigin(
	 *      HttpServletRequest)
	 */
	@Override
	protected String getCSRFOrigin(HttpServletRequest request) {
		StringBundler sb = new StringBundler(6);

		sb.append(ClassUtil.getClassName(this));
		sb.append(StringPool.COLON);
		sb.append(StringPool.SLASH);

		String serviceClassName = ParamUtil.getString(
			request, "serviceClassName");

		sb.append(serviceClassName);

		sb.append(StringPool.POUND);

		String serviceMethodName = ParamUtil.getString(
			request, "serviceMethodName");

		sb.append(serviceMethodName);

		return sb.toString();
	}

	protected Object[] getMethodAndParameterTypes(
			Class<?> clazz, String methodName, String[] parameters,
			String[] parameterTypes)
		throws Exception {

		StringBundler sb = new StringBundler(5);

		sb.append(clazz.getName());
		sb.append("_METHOD_NAME_");
		sb.append(methodName);
		sb.append("_PARAMETERS_");

		String parameterTypesNames = StringUtil.merge(parameterTypes);

		if (Validator.isNull(parameterTypesNames)) {
			sb.append(parameters.length);
		}
		else {
			sb.append(parameterTypesNames);
		}

		String key = sb.toString();

		Object[] methodAndParameterTypes = _methodCache.get(key);

		if (methodAndParameterTypes != null) {
			return methodAndParameterTypes;
		}

		Method method = null;
		Type[] methodParameterTypes = null;

		Method[] methods = clazz.getMethods();

		for (Method curMethod : methods) {
			String curMethodName = curMethod.getName();

			if (curMethodName.equals(methodName)) {
				Type[] curParameterTypes = curMethod.getGenericParameterTypes();

				if (curParameterTypes.length == parameters.length) {
					if ((parameterTypes.length > 0) &&
						(parameterTypes.length == curParameterTypes.length)) {

						boolean match = true;

						for (int j = 0; j < parameterTypes.length; j++) {
							String t1 = parameterTypes[j];
							String t2 = getTypeNameOrClassDescriptor(
								curParameterTypes[j]);

							if (!t1.equals(t2)) {
								match = false;
							}
						}

						if (match) {
							method = curMethod;
							methodParameterTypes = curParameterTypes;

							break;
						}
					}
					else if (method != null) {
						String parametersString = StringUtil.merge(parameters);

						_log.error(
							StringBundler.concat(
								"Obscure method name for class ",
								String.valueOf(clazz), ", method ", methodName,
								", and parameters ", parametersString));

						return null;
					}
					else {
						method = curMethod;
						methodParameterTypes = curParameterTypes;
					}
				}
			}
		}

		if (method != null) {
			methodAndParameterTypes = new Object[] {
				method, methodParameterTypes
			};

			_methodCache.put(key, methodAndParameterTypes);

			return methodAndParameterTypes;
		}

		String parametersString = StringUtil.merge(parameters);

		_log.error(
			StringBundler.concat(
				"No method found for class ", String.valueOf(clazz),
				", method ", methodName, ", and parameters ",
				parametersString));

		return null;
	}

	@Override
	protected String getReroutePath() {
		return _REROUTE_PATH;
	}

	protected String getReturnValue(Object returnObj) throws Exception {
		if (returnObj instanceof JSONSerializable) {
			JSONSerializable jsonSerializable = (JSONSerializable)returnObj;

			return jsonSerializable.toJSONString();
		}

		JSONSerializer jsonSerializer = JSONFactoryUtil.createJSONSerializer();

		jsonSerializer.exclude("*.class");

		return jsonSerializer.serializeDeep(returnObj);
	}

	protected String[] getStringArrayFromJSON(
			HttpServletRequest request, String param)
		throws JSONException {

		String json = ParamUtil.getString(request, param, "[]");

		JSONArray jsonArray = JSONFactoryUtil.createJSONArray(json);

		return ArrayUtil.toStringArray(jsonArray);
	}

	protected String getTypeNameOrClassDescriptor(Type type) {
		String typeName = type.toString();

		if (typeName.contains("class ")) {
			return typeName.substring(6);
		}

		Matcher matcher = _fieldDescriptorPattern.matcher(typeName);

		while (matcher.find()) {
			String dimensions = matcher.group(2);
			String fieldDescriptor = matcher.group(1);

			if (Validator.isNull(dimensions)) {
				return fieldDescriptor;
			}

			dimensions = dimensions.replace(
				StringPool.CLOSE_BRACKET, StringPool.BLANK);

			if (fieldDescriptor.equals("boolean")) {
				fieldDescriptor = "Z";
			}
			else if (fieldDescriptor.equals("byte")) {
				fieldDescriptor = "B";
			}
			else if (fieldDescriptor.equals("char")) {
				fieldDescriptor = "C";
			}
			else if (fieldDescriptor.equals("double")) {
				fieldDescriptor = "D";
			}
			else if (fieldDescriptor.equals("float")) {
				fieldDescriptor = "F";
			}
			else if (fieldDescriptor.equals("int")) {
				fieldDescriptor = "I";
			}
			else if (fieldDescriptor.equals("long")) {
				fieldDescriptor = "J";
			}
			else if (fieldDescriptor.equals("short")) {
				fieldDescriptor = "S";
			}
			else {
				fieldDescriptor = "L".concat(
					fieldDescriptor
				).concat(
					StringPool.SEMICOLON
				);
			}

			return dimensions.concat(fieldDescriptor);
		}

		throw new IllegalArgumentException(type.toString() + " is invalid");
	}

	protected boolean isValidRequest(HttpServletRequest request) {
		String className = ParamUtil.getString(request, "serviceClassName");
		String methodName = ParamUtil.getString(request, "serviceMethodName");

		if (className.contains(".service.") &&
			className.endsWith("ServiceUtil") &&
			!className.endsWith("LocalServiceUtil") &&
			!_invalidClassNames.contains(className) &&
			!_invalidMethodNames.contains(methodName)) {

			return true;
		}

		return false;
	}

	private static final String _REROUTE_PATH = "/api/json";

	private static final Log _log = LogFactoryUtil.getLog(
		JSONServiceAction.class);

	private static final Pattern _fieldDescriptorPattern = Pattern.compile(
		"^(.*?)((\\[\\])*)$", Pattern.DOTALL);

	private final Set<String> _invalidClassNames;
	private final Set<String> _invalidMethodNames;
	private final Map<String, Object[]> _methodCache = new HashMap<>();

}