diff --git a/fizz-core/src/main/java/we/fizz/function/CodecFunc.java b/fizz-core/src/main/java/we/fizz/function/CodecFunc.java
new file mode 100644
index 0000000..ca57020
--- /dev/null
+++ b/fizz-core/src/main/java/we/fizz/function/CodecFunc.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2021 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package we.fizz.function;
+
+import java.io.UnsupportedEncodingException;
+import java.security.Key;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESKeySpec;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import we.util.DigestUtils;
+
+/**
+ * Codec Functions
+ *
+ * @author Francis Dong
+ *
+ */
+public class CodecFunc implements IFunc {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CodecFunc.class);
+
+ private static final String CHARSET_UTF8 = "UTF-8";
+
+ private static final String IV = "12345678";
+
+ private static CodecFunc singleton;
+
+ public static CodecFunc getInstance() {
+ if (singleton == null) {
+ synchronized (CodecFunc.class) {
+ if (singleton == null) {
+ CodecFunc instance = new CodecFunc();
+ instance.init();
+ singleton = instance;
+ }
+ }
+ }
+ return singleton;
+ }
+
+ private CodecFunc() {
+ }
+
+ public void init() {
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.md5", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.sha1", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.sha256", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.sha384", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.sha512", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.base64Encode", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.base64Decode", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.aesEncrypt", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.aesDecrypt", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.desEncrypt", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "codec.desDecrypt", this);
+ }
+
+ public String md5(String data) {
+ return DigestUtils.md5Hex(data);
+ }
+
+ public String sha1(String data) {
+ return DigestUtils.sha1Hex(data);
+ }
+
+ public String sha256(String data) {
+ return DigestUtils.sha256Hex(data);
+ }
+
+ public String sha384(String data) {
+ return DigestUtils.sha384Hex(data);
+ }
+
+ public String sha512(String data) {
+ return DigestUtils.sha512Hex(data);
+ }
+
+ public String base64Encode(String data) throws Exception {
+ try {
+ return Base64.getEncoder().encodeToString(data.getBytes(CHARSET_UTF8));
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.error("Base64 encode error, data={}", data, e);
+ throw e;
+ }
+ }
+
+ public String base64Decode(String data) throws Exception {
+ return new String(Base64.getDecoder().decode(data));
+ }
+
+ public String aesEncrypt(String data, String key) throws Exception {
+ if (StringUtils.isBlank(data) || StringUtils.isBlank(key)) {
+ return null;
+ }
+ try {
+ Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+ SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(CHARSET_UTF8), "AES");
+ cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
+ byte[] result = cipher.doFinal(data.getBytes(CHARSET_UTF8));
+ return Base64.getEncoder().encodeToString(result);
+ } catch (Exception e) {
+ LOGGER.error("AES encrypt error, data={}", data, e);
+ throw e;
+ }
+ }
+
+ public String aesDecrypt(String data, String key) throws Exception {
+ if (StringUtils.isBlank(data) || StringUtils.isBlank(key)) {
+ return null;
+ }
+ try {
+ Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+ SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(CHARSET_UTF8), "AES");
+ cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
+ byte[] result = Base64.getDecoder().decode(data);
+ return new String(cipher.doFinal(result));
+ } catch (Exception e) {
+ LOGGER.error("AES decrypt error, data={}", data, e);
+ throw e;
+ }
+ }
+
+ public String desEncrypt(String data, String key) throws Exception {
+ if (StringUtils.isBlank(data) || StringUtils.isBlank(key)) {
+ return null;
+ }
+ try {
+ DESKeySpec dks = new DESKeySpec(key.getBytes(CHARSET_UTF8));
+ SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+ Key secretKey = keyFactory.generateSecret(dks);
+ Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
+ IvParameterSpec iv = new IvParameterSpec(IV.getBytes(CHARSET_UTF8));
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
+ byte[] bytes = cipher.doFinal(data.getBytes(CHARSET_UTF8));
+ return new String(Base64.getEncoder().encode(bytes));
+ } catch (Exception e) {
+ LOGGER.error("DES eecrypt error, data={}", data, e);
+ throw e;
+ }
+ }
+
+ public String desDecrypt(String data, String key) throws Exception {
+ if (StringUtils.isBlank(data) || StringUtils.isBlank(key)) {
+ return null;
+ }
+ try {
+ DESKeySpec dks = new DESKeySpec(key.getBytes(CHARSET_UTF8));
+ SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+ Key secretKey = keyFactory.generateSecret(dks);
+ Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
+ IvParameterSpec iv = new IvParameterSpec(IV.getBytes(CHARSET_UTF8));
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
+ return new String(cipher.doFinal(Base64.getDecoder().decode(data.getBytes(CHARSET_UTF8))), CHARSET_UTF8);
+ } catch (Exception e) {
+ LOGGER.error("DES decrypt error, data={}", data, e);
+ throw e;
+ }
+ }
+
+}
diff --git a/fizz-core/src/main/java/we/fizz/function/DateFunc.java b/fizz-core/src/main/java/we/fizz/function/DateFunc.java
new file mode 100644
index 0000000..2514166
--- /dev/null
+++ b/fizz-core/src/main/java/we/fizz/function/DateFunc.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2021 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package we.fizz.function;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import we.fizz.exception.FizzRuntimeException;
+
+/**
+ * Date Functions
+ *
+ * @author Francis Dong
+ *
+ */
+public class DateFunc implements IFunc {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DateFunc.class);
+
+ private static DateFunc singleton;
+
+ public static DateFunc getInstance() {
+ if (singleton == null) {
+ synchronized (DateFunc.class) {
+ if (singleton == null) {
+ DateFunc instance = new DateFunc();
+ instance.init();
+ singleton = instance;
+ }
+ }
+ }
+ return singleton;
+ }
+
+ private DateFunc() {
+ }
+
+ public void init() {
+ FuncExecutor.register(NAME_SPACE_PREFIX + "date.timestamp", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "date.now", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "date.add", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "date.formatTs", this);
+ FuncExecutor.register(NAME_SPACE_PREFIX + "date.changePattern", this);
+ }
+
+ /**
+ * Date pattern
+ * yyyy-MM-dd
+ */
+ public final static String DATE_FORMAT = "yyyy-MM-dd";
+
+ /**
+ * Time pattren
+ * HH:mm:ss
+ */
+ public final static String TIME_FORMAT = "HH:mm:ss";
+
+ /**
+ * Short time pattren
+ * HH:mm
+ */
+ public final static String SHORT_TIME_FORMAT = "HH:mm";
+
+ /**
+ * Date time pattern
+ * yyyy-MM-dd HH:mm:ss
+ */
+ public final static String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+
+ /**
+ * Returns current timestamp (Milliseconds)
+ *
+ * @return
+ */
+ public long timestamp() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * Returns current time with the given pattern
+ * Frequently-used pattern:
+ * yyyy-MM-dd HH:mm:ss
+ * yyyy-MM-dd
+ * HH:mm:ss
+ * HH:mm
+ * yyyy-MM-dd HH:mm:ss Z
+ *
+ * @param pattern [optional] the pattern describing the date and time format,
+ * dafault yyyy-MM-dd HH:mm:ss
+ * @return
+ */
+ public String now(String pattern) {
+ return formatDate(new Date(), pattern);
+ }
+
+ /**
+ * Adds or subtracts the specified amount of time to the given calendar field,
+ * based on the calendar's rules. For example, to subtract 5 hours from the
+ * current time of the calendar, you can achieve it by calling:
+ *
+ * add("2021-08-04 14:23:12", "yyyy-MM-dd HH:mm:ss", 4, -5).
+ *
+ * @param date date string
+ * @param pattern date pattern of the given date string
+ * @param field the calendar field,
+ * 1 for millisecond
+ * 2 for second
+ * 3 for minute
+ * 4 for hour
+ * 5 for date
+ * 6 for month
+ * 7 for year
+ * @param amount the amount of date or time to be added to the field
+ * @return
+ */
+ public String add(String date, String pattern, int field, int amount) {
+ Date d = parse(date, pattern);
+ if (d != null) {
+ // convert to calendar field
+ int calField = 0;
+ switch (field) {
+ case 1:
+ calField = Calendar.MILLISECOND;
+ break;
+ case 2:
+ calField = Calendar.SECOND;
+ break;
+ case 3:
+ calField = Calendar.MINUTE;
+ break;
+ case 4:
+ calField = Calendar.HOUR;
+ break;
+ case 5:
+ calField = Calendar.DATE;
+ break;
+ case 6:
+ calField = Calendar.MONTH;
+ break;
+ case 7:
+ calField = Calendar.YEAR;
+ break;
+ default:
+ LOGGER.error("invalid field, date={} pattern={} filed={}", date, pattern, field);
+ throw new FizzRuntimeException(
+ "invalid field, date=" + date + "pattern=" + pattern + " filed=" + field);
+ }
+ return formatDate(addToFiled(d, calField, amount), pattern);
+ }
+ return null;
+ }
+
+ /**
+ * Format the a timestamp to the given pattern
+ *
+ * @param timestamp
+ * @param pattern
+ * @return
+ */
+ public String formatTs(long timestamp, String pattern) {
+ return formatDate(new Date(timestamp), pattern);
+ }
+
+ /**
+ * Format the a time with source pattern to the target pattern
+ *
+ * @param dateStr date
+ * @param sourcePattern source pattern
+ * @param targetPattern target pattern
+ * @return
+ */
+ public String changePattern(String dateStr, String sourcePattern, String targetPattern) {
+ return formatDate(parse(dateStr, sourcePattern), targetPattern);
+ }
+
+ /**
+ * Adds or subtracts the specified amount of time to the given calendar field
+ *
+ * @param date a Date
+ * @param field field that the times to be add to, such as: Calendar.SECOND,
+ * Calendar.YEAR
+ * @param amount the amount of date or time to be added to the field
+ * @return
+ */
+ private Date addToFiled(Date date, int field, int amount) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ cal.add(field, amount);
+ return cal.getTime();
+ }
+
+ /**
+ * Parse string to Date
+ *
+ * @param dateStr String to be parsed
+ * @param pattern pattern of dateStr
+ * @return
+ */
+ private Date parse(String dateStr, String pattern) {
+ SimpleDateFormat df = new SimpleDateFormat(pattern == null ? DATE_TIME_FORMAT : pattern);
+ try {
+ return df.parse(dateStr);
+ } catch (ParseException e) {
+ LOGGER.error("Parse date error, dateStr={} pattern={}", dateStr, pattern, e);
+ throw new FizzRuntimeException("Parse date error, dateStr=" + dateStr + " pattern=" + pattern, e);
+ }
+ }
+
+ /**
+ * Format date with the given pattern
+ * Frequently-used pattern:
+ * yyyy-MM-dd HH:mm:ss
+ * yyyy-MM-dd
+ * HH:mm:ss
+ * HH:mm
+ * yyyy-MM-dd HH:mm:ss Z
+ *
+ * @param pattern [optional] the pattern describing the date and time format,
+ * dafault yyyy-MM-dd HH:mm:ss
+ * @return
+ */
+ private String formatDate(Date date, String pattern) {
+ SimpleDateFormat sdf = new SimpleDateFormat(pattern == null ? DATE_TIME_FORMAT : pattern);
+ return sdf.format(date);
+ }
+
+}
diff --git a/fizz-core/src/main/java/we/fizz/function/FuncExecutor.java b/fizz-core/src/main/java/we/fizz/function/FuncExecutor.java
new file mode 100644
index 0000000..e81709a
--- /dev/null
+++ b/fizz-core/src/main/java/we/fizz/function/FuncExecutor.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2021 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package we.fizz.function;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.beanutils.ConvertUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.noear.snack.ONode;
+import org.reflections.Reflections;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import we.fizz.exception.FizzRuntimeException;
+import we.fizz.input.Input;
+import we.fizz.input.PathMapping;
+
+/**
+ * Function Register
+ *
+ * @author Francis Dong
+ *
+ */
+public class FuncExecutor {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FuncExecutor.class);
+
+ private static final Map funcMap = new HashMap<>();
+
+ private static Pattern NUMBER_PATTERN = Pattern
+ .compile("^[-\\+]?[\\d]+\\s*[,\\)]{1}|^[-\\+]?[\\d]+\\.[\\d]+\\s*[,\\)]{1}");
+
+ private static FuncExecutor singleton;
+
+ public static FuncExecutor getInstance() {
+ if (singleton == null) {
+ synchronized (FuncExecutor.class) {
+ if (singleton == null) {
+ singleton = new FuncExecutor();
+ init();
+ }
+ }
+ }
+ return singleton;
+ }
+
+ private FuncExecutor() {
+ }
+
+ public static void init() {
+ try {
+ Reflections reflections = new Reflections("we.fizz.function");
+ Set> types = reflections.getSubTypesOf(IFunc.class);
+ for (Class extends IFunc> fnType : types) {
+ Method method = fnType.getMethod("getInstance");
+ method.invoke(fnType);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Register a function instance
+ *
+ * @param namespace a name to identify the given function instance
+ * @param func
+ */
+ public static void register(String namespace, IFunc funcInstance) {
+ if (StringUtils.isBlank(namespace)) {
+ LOGGER.warn("namespace is required");
+ return;
+ }
+ if (!namespace.startsWith(IFunc.NAME_SPACE_PREFIX)) {
+ LOGGER.warn("namespace must start with fn.");
+ return;
+ }
+ if (funcInstance == null) {
+ LOGGER.warn("function instance is required");
+ return;
+ }
+ funcMap.put(namespace, funcInstance);
+ }
+
+ /**
+ * Execute function
+ *
+ * @param funcExpression
+ * @return
+ */
+ public Object exec(ONode ctxNode, String funcExpression) {
+ RecursionContext ctx = new RecursionContext();
+ ctx.setFuncExpression(funcExpression);
+ return doExec(ctxNode, ctx);
+ }
+
+ private Object doExec(ONode ctxNode, RecursionContext ctx) {
+ String funcExpression = ctx.funcExpression;
+ if (StringUtils.isBlank(funcExpression)) {
+ return null;
+ }
+ funcExpression = StringUtils.trim(funcExpression);
+ int pos1 = funcExpression.indexOf("(");
+ if (pos1 == -1) {
+ LOGGER.warn("func expression is invalid, expression: {}", funcExpression);
+ return null;
+ }
+ if (!funcExpression.endsWith(")")) {
+ LOGGER.warn("func expression is invalid, expression: {}", funcExpression);
+ return null;
+ }
+
+ String path = funcExpression.substring(0, pos1);
+ int lastDotPos = path.lastIndexOf(".");
+ if (pos1 == -1) {
+ LOGGER.warn("func expression is invalid, expression: {}", funcExpression);
+ return null;
+ }
+ String namespace = path.substring(0, lastDotPos);
+ String methodName = path.substring(lastDotPos + 1);
+
+ Object funcInstance = funcMap.get(path);
+ if (funcInstance == null) {
+ String msg = String.format("function not found: %s, expression: %s", path, funcExpression);
+ LOGGER.warn(msg);
+ throw new FizzRuntimeException(msg);
+ }
+
+ try {
+ Method method = findMethod(funcInstance.getClass(), methodName);
+ Class[] paramTypes = method.getParameterTypes();
+ ctx.funcExpression = funcExpression;
+ Object[] args = parseArgs(ctxNode, ctx, funcExpression, paramTypes, method.isVarArgs());
+ if (args == null) {
+ return method.invoke(funcInstance);
+ }
+ return method.invoke(funcInstance, args);
+ } catch (FizzRuntimeException e) {
+ throw e;
+ } catch (InvocationTargetException e) {
+ Throwable targetEx = e.getTargetException();
+ if (targetEx instanceof FizzRuntimeException) {
+ throw (FizzRuntimeException) targetEx;
+ }
+ String msg = targetEx.getMessage();
+ if (msg == null) {
+ msg = String.format("execute function error: %s", funcExpression);
+ }
+ LOGGER.error(msg, targetEx);
+ throw new FizzRuntimeException(msg, targetEx);
+ } catch (Exception e) {
+ String msg = String.format("execute function error: %s", funcExpression);
+ LOGGER.error(msg, e);
+ throw new FizzRuntimeException(msg, e);
+ }
+ }
+
+ private Method findMethod(Class funcClass, String methodName) {
+ Method[] methods = funcClass.getDeclaredMethods();
+ for (Method method : methods) {
+ if (method.getName().equals(methodName)) {
+ return method;
+ }
+ }
+ String msg = String.format("method not found: %s, class: %s", methodName, funcClass);
+ LOGGER.warn(msg);
+ throw new FizzRuntimeException(msg);
+ }
+
+ /**
+ * funcExpression sample:
+ * fn.date.add({step1.request1.response.body.date}, "yyyy-MM-dd HH:mm:ss", 1,
+ * 1000)
+ * fn.date.add(true, fn.date.add({step1.request1.response.body.date},
+ * "yyyy-MM-dd HH:mm:ss", 1, 1000), "yyyy-MM-dd HH:mm:ss\"))}}", 1, 1000)
+ *
+ * @param funcExpression
+ * @param paramTypes
+ * @return
+ */
+ private Object[] parseArgs(ONode ctxNode, RecursionContext ctx, String funcExpression, Class[] paramTypes,
+ boolean isVarArgs) {
+ int pos1 = funcExpression.indexOf("(");
+ // int pos2 = funcExpression.lastIndexOf(")");
+ String argsStr = funcExpression.substring(pos1 + 1);
+ argsStr = StringUtils.trim(argsStr);
+ // check if there is any argument
+ if (StringUtils.isBlank(argsStr)) {
+ if (paramTypes == null || paramTypes.length == 0) {
+ return null;
+ } else if (paramTypes.length == 1 && isVarArgs) {
+ // check if variable arguments
+ return null;
+ } else {
+ throw new FizzRuntimeException(
+ String.format("missing argument, Function Expression: %s", funcExpression));
+ }
+ }
+ Object[] args = new Object[paramTypes.length];
+ List