/*
* JSmart Framework - Java Web Development Framework
* Copyright (c) 2015, Jeferson Albino da Silva, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
package com.jsmartframework.web.manager;
import static com.jsmartframework.web.config.Config.CONFIG;
import static com.jsmartframework.web.manager.BeanHandler.HANDLER;
import static com.jsmartframework.web.config.Constants.ENCODING;
import static com.jsmartframework.web.manager.BeanHandler.AnnotatedFunction;
import com.google.common.html.HtmlEscapers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.internal.Primitives;
import com.jsmartframework.web.adapter.ListAdapter;
import com.jsmartframework.web.adapter.TableAdapter;
import com.jsmartframework.web.config.Constants;
import com.jsmartframework.web.json.Scroll;
import com.jsmartframework.web.util.WebText;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.joda.time.DateTime;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.el.ELContext;
import javax.el.MethodExpression;
import javax.el.PropertyNotWritableException;
import javax.el.ValueExpression;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
enum ExpressionHandler {
EXPRESSIONS();
private static final Logger LOGGER = Logger.getLogger(ExpressionHandler.class.getPackage().getName());
public static final Pattern EL_PATTERN = Pattern.compile("@\\{[!]*(.[^@\\{\\}]*)\\}");
public static final Pattern JSP_PATTERN = Pattern.compile("\\$\\{[!]*(.[^\\$\\{\\}]*)\\}");
public static final Pattern ID_PATTERN = Pattern.compile("id=\"(.[^\"]*)\"");
public static final String EL_PATTERN_FORMAT = "@{%s.%s}";
public static final String BEAN_METHOD_NAME_FORMAT = "%s.%s";
static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new JsonConverter.LocalDateTimeTypeConverter())
.registerTypeAdapter(DateTime.class, new JsonConverter.DateTimeTypeConverter())
.registerTypeAdapter(Date.class, new JsonConverter.DateTypeConverter())
.create();
Map<String, String> getRequestExpressions(HttpServletRequest request) {
Map<String, String> expressions = new LinkedHashMap<>();
for (String param : request.getParameterMap().keySet()) {
String expr = extractExpression(request, param);
if (expr != null) {
expressions.put(param, expr);
}
}
return expressions;
}
private String extractExpression(HttpServletRequest request, String param) {
Matcher matcher = TagHandler.J_TAG_PATTERN.matcher(param);
if (matcher.find()) {
return TagEncrypter.decrypt(request, matcher.group(2).replace("[]", ""));
}
return null;
}
void handleRequestExpression(String jTag, String expr, String jParam) throws ServletException, IOException {
try {
if (jTag.equals(TagHandler.J_TAG)) {
setExpressionValue(expr, jParam);
} else if (jTag.equals(TagHandler.J_ARRAY)) {
setExpressionValues(expr, jParam);
} else if (jTag.equals(TagHandler.J_SEL)) {
setSelectionValue(expr, jParam);
} else if (jTag.equals(TagHandler.J_FILE)) {
setExpressionFilePart(expr, jParam);
} else if (jTag.equals(TagHandler.J_DATE)) {
setExpressionDate(expr, jParam);
} else if (jTag.equals(TagHandler.J_CAPTCHA)) {
setExpressionCaptcha(expr, jParam);
}
} catch (PropertyNotWritableException e) {
LOGGER.log(Level.SEVERE, "Property " + expr + " is not writable");
throw e;
}
}
String handleSubmitExpression(String expr, String jParam) throws ServletException, IOException {
Object response = null;
Matcher matcher = EL_PATTERN.matcher(expr);
if (matcher.find()) {
String beanMethod = matcher.group(1);
String[] methodSign = beanMethod.split(Constants.EL_SEPARATOR);
HttpServletRequest request = WebContext.getRequest();
if (methodSign.length > 0 && WebContext.containsAttribute(methodSign[0])) {
Object bean = getExpressionBean(methodSign[0]);
String elBeanMethod = String.format(Constants.JSP_EL, beanMethod);
// Check authorization to execute method
if (!HANDLER.checkExecuteAuthorization(bean, elBeanMethod, request)) {
return null;
}
// Call mapped method with @PreSubmit / @PreAction annotation for specific action
boolean preActionValidated = HANDLER.executePreSubmit(bean, methodSign[methodSign.length -1]);
preActionValidated &= HANDLER.executePreAction(bean, methodSign[methodSign.length -1]);
if (preActionValidated) {
Object[] arguments = null;
String[] paramArgs = request.getParameterValues(TagHandler.J_SBMT_ARGS + jParam);
AnnotatedFunction function = HANDLER.beanMethodFunctions.get(beanMethod);
if (paramArgs != null) {
arguments = new Object[paramArgs.length];
boolean unescape = HANDLER.containsUnescapeMethod(methodSign);
for (int i = 0; i < paramArgs.length; i++) {
if (function != null) {
Class<?> argClass = function.getArgumentType(i);
if (argClass != null && !Primitives.isPrimitive(argClass) && !argClass.equals(String.class)) {
arguments[i] = GSON.fromJson(paramArgs[i], argClass);
continue;
}
}
arguments[i] = unescape ? paramArgs[i] : escapeValue(paramArgs[i]);
}
}
// Call submit method
ELContext context = WebContext.getPageContext().getELContext();
MethodExpression methodExpr = WebContext.getExpressionFactory().createMethodExpression(context, elBeanMethod,
null, arguments != null ? new Class<?>[arguments.length] : new Class<?>[]{});
response = methodExpr.invoke(context, arguments);
if (function != null && function.hasProduces()) {
switch (function.getProduces()) {
case JSON:
WebContext.writeResponseAsJson(response);
break;
case XML:
WebContext.writeResponseAsXmlQuietly(response);
break;
case STRING:
WebContext.writeResponseAsString(response.toString());
break;
}
// Reset response to be passed to path analysis
response = null;
}
// Call mapped method with @PostSubmit / @PostAction annotation for specific action
HANDLER.executePostSubmit(bean, methodSign[methodSign.length -1]);
HANDLER.executePostAction(bean, methodSign[methodSign.length -1]);
}
}
}
return response != null ? response.toString() : null;
}
@SuppressWarnings("all")
private void setSelectionValue(String expr, String jParam) {
Matcher matcher = EL_PATTERN.matcher(expr);
if (matcher.find()) {
String beanMethod = matcher.group(1);
String[] methodSign = beanMethod.split(Constants.EL_SEPARATOR);
if (methodSign.length > 0 && WebContext.containsAttribute(methodSign[0])) {
HttpServletRequest request = WebContext.getRequest();
beanMethod = String.format(Constants.JSP_EL, beanMethod);
// Get parameter mapped by TagHandler.J_VALUES
String valuesParam = request.getParameter(TagHandler.J_SEL + jParam);
Matcher valuesMatcher = TagHandler.J_TAG_PATTERN.matcher(valuesParam);
Object object = null;
List<Object> list = null;
Scroll scroll = null;
if (valuesMatcher.find()) {
object = getExpressionValue(TagEncrypter.decrypt(request, valuesMatcher.group(2)));
}
if (object instanceof ListAdapter) {
String scrollParam = request.getParameter(TagHandler.J_SCROLL + jParam);
scroll = GSON.fromJson(scrollParam, Scroll.class);
list = ((ListAdapter) object).load(scroll.getIndex(), scroll.getOffset(), scroll.getSize());
} else if (object instanceof TableAdapter) {
String scrollParam = request.getParameter(TagHandler.J_SCROLL + jParam);
scroll = GSON.fromJson(scrollParam, Scroll.class);
list = ((TableAdapter) object).load(scroll.getIndex(), scroll.getOffset(), scroll.getSize(),
scroll.getSort(), scroll.getOrder(), scroll.getFilters());
} else if (object instanceof List<?>) {
list = (List<Object>) object;
}
if (list != null && !list.isEmpty()) {
ELContext context = WebContext.getPageContext().getELContext();
ValueExpression valueExpr = WebContext.getExpressionFactory().createValueExpression(context, beanMethod, Object.class);
Integer index = Integer.parseInt(request.getParameter(TagHandler.J_SEL_VAL + jParam));
// Case scroll list with adapter need to calculate the difference between
// the first index of the loaded content with the clicked list item index
if (scroll != null) {
index -= scroll.getIndex();
}
valueExpr.setValue(context, list.get(index));
}
}
}
}
void setExpressionValue(String expr, String jParam) {
if (isReadOnlyParameter(jParam)) {
return;
}
Matcher matcher = EL_PATTERN.matcher(expr);
if (matcher.find()) {
String beanMethod = matcher.group(1);
String[] methodSign = beanMethod.split(Constants.EL_SEPARATOR);
if (methodSign.length > 0 && WebContext.containsAttribute(methodSign[0])) {
beanMethod = String.format(Constants.JSP_EL, beanMethod);
ELContext context = WebContext.getPageContext().getELContext();
ValueExpression valueExpr = WebContext.getExpressionFactory().createValueExpression(context, beanMethod, Object.class);
Object value = WebContext.getRequest().getParameter(TagHandler.J_TAG + jParam);
if (!HANDLER.containsUnescapeMethod(methodSign)) {
value = escapeValue((String) value);
}
valueExpr.setValue(context, value);
}
}
}
void setAttributeValue(String expr, Object value) {
if (expr != null) {
Matcher matcher = EL_PATTERN.matcher(expr);
if (matcher.find()) {
String beanMethod = matcher.group(1);
String[] methodSign = beanMethod.split(Constants.EL_SEPARATOR);
if (methodSign.length > 0 && WebContext.containsAttribute(methodSign[0])) {
beanMethod = String.format(Constants.JSP_EL, beanMethod);
ELContext context = WebContext.getPageContext().getELContext();
ValueExpression valueExpr = WebContext.getExpressionFactory().createValueExpression(context, beanMethod, Object.class);
valueExpr.setValue(context, value);
}
}
}
}
private void setExpressionValues(String expr, String jParam) {
if (isReadOnlyParameter(jParam)) {
return;
}
Matcher matcher = EL_PATTERN.matcher(expr);
if (matcher.find()) {
String beanMethod = matcher.group(1);
String[] methodSign = beanMethod.split(Constants.EL_SEPARATOR);
if (methodSign.length > 0 && WebContext.containsAttribute(methodSign[0])) {
beanMethod = String.format(Constants.JSP_EL, beanMethod);
List<Object> list = new ArrayList<Object>();
String[] values = WebContext.getRequest().getParameterValues(TagHandler.J_ARRAY + jParam);
boolean unescape = HANDLER.containsUnescapeMethod(methodSign);
if (values != null) {
for (String val : values) {
try {
list.add(NumberUtils.createNumber(val));
} catch (NumberFormatException e) {
list.add(unescape ? val : escapeValue(val));
}
}
}
// Check for empty value sent on array [false]
if (list.size() == 1 && list.get(0) != null && list.get(0).equals("false")) {
list.clear();
}
ELContext context = WebContext.getPageContext().getELContext();
ValueExpression valueExpr = WebContext.getExpressionFactory().createValueExpression(context, beanMethod, Object.class);
valueExpr.setValue(context, list);
}
}
}
private void setExpressionFilePart(String expr, String jParam) throws ServletException, IOException {
Matcher matcher = EL_PATTERN.matcher(expr);
if (matcher.find()) {
String beanMethod = matcher.group(1);
String[] methodSign = beanMethod.split(Constants.EL_SEPARATOR);
if (methodSign.length > 0 && WebContext.containsAttribute(methodSign[0])) {
beanMethod = String.format(Constants.JSP_EL, beanMethod);
ELContext context = WebContext.getPageContext().getELContext();
ValueExpression valueExpr = WebContext.getExpressionFactory().createValueExpression(context, beanMethod, Object.class);
Object value = WebContext.getRequest().getPart(TagHandler.J_PART + jParam);
valueExpr.setValue(context, value);
}
}
}
private void setExpressionDate(String expr, String jParam) throws ServletException {
if (isReadOnlyParameter(jParam)) {
return;
}
Matcher matcher = EL_PATTERN.matcher(expr);
if (matcher.find()) {
String beanMethod = matcher.group(1);
String[] methodSign = beanMethod.split(Constants.EL_SEPARATOR);
if (methodSign.length > 0 && WebContext.containsAttribute(methodSign[0])) {
beanMethod = String.format(Constants.JSP_EL, beanMethod);
ELContext context = WebContext.getPageContext().getELContext();
ValueExpression valueExpr = WebContext.getExpressionFactory().createValueExpression(context, beanMethod, Object.class);
String value = WebContext.getRequest().getParameter(TagHandler.J_DATE + jParam);
if (StringUtils.isNotBlank(value)) {
Throwable throwable = null;
try {
valueExpr.setValue(context, value);
return;
} catch (Exception ex) {
throwable = ex;
}
Long timeMillis = Long.parseLong(value);
try {
valueExpr.setValue(context, new Date(timeMillis));
return;
} catch (Exception ex) {
throwable = ex;
}
try {
valueExpr.setValue(context, Instant.ofEpochMilli(timeMillis)
.atZone(ZoneId.systemDefault()).toLocalDateTime());
return;
} catch (Exception ex) {
throwable = ex;
}
try {
valueExpr.setValue(context, new DateTime(timeMillis));
return;
} catch (Exception ex) {
throwable = ex;
}
if (throwable != null) {
throw new ServletException(throwable.getMessage());
}
} else {
valueExpr.setValue(context, null);
}
}
}
}
private void setExpressionCaptcha(String expr, String jParam) throws ServletException {
HttpServletRequest request = WebContext.getRequest();
// Just add the ReCaptcha value to mapped values for further validation
if (expr.contains(ReCaptchaHandler.RESPONSE_V1_FIELD_NAME)) {
WebContext.addMappedValue(ReCaptchaHandler.RESPONSE_V1_FIELD_NAME,
request.getParameter(TagHandler.J_CAPTCHA + jParam));
} else {
WebContext.addMappedValue(ReCaptchaHandler.RESPONSE_V2_FIELD_NAME,
request.getParameter(ReCaptchaHandler.RESPONSE_V2_FIELD_NAME));
}
}
public Object getExpressionValue(Object expr) {
if (expr != null) {
String evalExpr = expr.toString();
Matcher matcher = EL_PATTERN.matcher(evalExpr);
if (!matcher.find()) {
return expr;
}
boolean hasMoreGroup = false;
StringBuffer exprBuffer = new StringBuffer();
Object result = evaluateExpression(evalExpr.substring(matcher.start() + 2, matcher.end() - 1));
matcher.appendReplacement(exprBuffer, result != null ? Matcher.quoteReplacement(result.toString()) : "null");
while (matcher.find()) {
hasMoreGroup = true;
Object object = evaluateExpression(evalExpr.substring(matcher.start() + 2, matcher.end() - 1));
matcher.appendReplacement(exprBuffer, object != null ? Matcher.quoteReplacement(object.toString()) : "null");
}
if (hasMoreGroup || result instanceof String) {
return matcher.appendTail(exprBuffer).toString();
} else {
return result;
}
}
return null;
}
private Object evaluateExpression(String expr) {
if (expr == null) {
return expr;
}
String[] exprs = expr.split(Constants.EL_SEPARATOR, 2);
if (exprs.length == 2 && WebText.containsResource(exprs[0])) {
return WebText.getString(exprs[0], exprs[1]);
}
String jspExpr = String.format(Constants.JSP_EL, expr);
ELContext context = WebContext.getPageContext().getELContext();
ValueExpression valueExpr = WebContext.getExpressionFactory().createValueExpression(context, jspExpr, Object.class);
Object obj = valueExpr.getValue(context);
if (obj instanceof String) {
String[] objs = obj.toString().split(Constants.EL_SEPARATOR, 2);
if (objs.length == 2 && WebText.containsResource(objs[0])) {
return WebText.getString(objs[0], objs[1]);
}
}
return obj;
}
private Object getExpressionBean(String name) {
return WebContext.getAttribute(name);
}
private boolean isReadOnlyParameter(String jParam) {
if (jParam != null) {
return jParam.endsWith(Constants.EL_PARAM_READ_ONLY);
}
return false;
}
private Object escapeValue(String value) {
if (value != null && CONFIG.getContent().isEscapeRequest()) {
value = HtmlEscapers.htmlEscaper().escape(value);
}
return value;
}
String decodeUrl(String value) {
try {
return URLDecoder.decode(value, ENCODING);
} catch (UnsupportedEncodingException ex) {
ex.printStackTrace();
}
return value;
}
}