/*
* Created by Angel Leon (@gubatron), Alden Torres (aldenml)
* Copyright (c) 2011-2014, FrostWire(R). All rights reserved.
*
* 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
* (at your option) 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 <http://www.gnu.org/licenses/>.
*/
package com.frostwire.search.extractors.js;
import static com.frostwire.search.extractors.js.JavaFunctions.escape;
import static com.frostwire.search.extractors.js.JavaFunctions.isalpha;
import static com.frostwire.search.extractors.js.JavaFunctions.isdigit;
import static com.frostwire.search.extractors.js.JavaFunctions.join;
import static com.frostwire.search.extractors.js.JavaFunctions.json_loads;
import static com.frostwire.search.extractors.js.JavaFunctions.len;
import static com.frostwire.search.extractors.js.JavaFunctions.list;
import static com.frostwire.search.extractors.js.JavaFunctions.mscpy;
import static com.frostwire.search.extractors.js.JavaFunctions.reverse;
import static com.frostwire.search.extractors.js.JavaFunctions.slice;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.code.regexp.Matcher;
import com.google.code.regexp.Pattern;
/**
* @author gubatron
* @author aldenml
*
*/
public final class JsFunction<T> {
private final JsContext ctx;
private final LambdaN initial_function;
public JsFunction(String jscode, String funcname) {
this.ctx = new JsContext(jscode);
this.initial_function = extract_function(ctx, funcname);
}
@SuppressWarnings("unchecked")
public T eval(Object[] args) {
try {
return (T) initial_function.eval(args);
} finally {
// at this point we know that jscode is no longer necessary
ctx.free();
}
}
public T eval(Object s) {
return eval(new Object[] { s });
}
private static Object interpret_statement(final JsContext ctx, String stmt, final Map<String, Object> local_vars, final int allow_recursion) {
if (allow_recursion < 0) {
throw new JsError("Recursion limit reached");
}
if (stmt.startsWith("var ")) {
stmt = stmt.substring("var ".length());
}
final Matcher ass_m = Pattern.compile("^(?<out>[a-z]+)(\\[(?<index>.+?)\\])?=(?<expr>.*)$").matcher(stmt);
Lambda1 assign;
String expr;
if (ass_m.find()) {
if (ass_m.group("index") != null) {
final Object lvar = local_vars.get(ass_m.group("out"));
final Object idx = interpret_expression(ctx, ass_m.group("index"), local_vars, allow_recursion);
assert idx instanceof Integer;
assign = new Lambda1() {
@SuppressWarnings("unchecked")
@Override
public Object eval(Object val) {
((List<Object>) lvar).set((Integer) idx, val);
return val;
}
};
expr = ass_m.group("expr");
} else {
final String var = ass_m.group("out");
assign = new Lambda1() {
@Override
public Object eval(Object val) {
local_vars.put(var, val);
return val;
}
};
expr = ass_m.group("expr");
}
} else if (stmt.startsWith("return ")) {
assign = new Lambda1() {
@Override
public Object eval(Object v) {
return v;
}
};
expr = stmt.substring("return ".length());
} else {
// Try interpreting it as an expression
expr = stmt;
assign = new Lambda1() {
@Override
public Object eval(Object v) {
return v;
}
};
}
Object v = interpret_expression(ctx, expr, local_vars, allow_recursion);
return assign.eval(v);
}
@SuppressWarnings("unchecked")
private static Object interpret_expression(final JsContext ctx, String expr, Map<String, Object> local_vars, int allow_recursion) {
if (isdigit(expr)) {
return Integer.valueOf(expr);
}
if (isalpha(expr)) {
return local_vars.get(expr);
}
// try:
// return json.loads(expr)
// except ValueError:
// pass
Object jsl = json_loads(expr);
if (jsl != null) {
return jsl;
}
Matcher m = Pattern.compile("^(?<var>[a-zA-Z0-9_]+)\\.(?<member>[^\\(]+)(\\((?<args>[^\\(\\)]*)\\))?$").matcher(expr);
if (m.find()) {
String variable = m.group("var");
String member = m.group("member");
String arg_str = m.group("args");
Object obj = null;
if (local_vars.containsKey(variable)) {
obj = local_vars.get(variable);
} else {
if (!ctx.objects.containsKey(variable)) {
ctx.objects.put(variable, extract_object(ctx, variable));
}
obj = ctx.objects.get(variable);
}
if (arg_str == null) {
// Member access
if (member.equals("length")) {
return len(obj);
}
return ((JsObject) obj).functions.get(member).eval(new Object[] {});
}
if (!expr.endsWith(")")) {
throw new JsError("Error parsing js code");
}
List<Object> argvals = null;
if (arg_str.equals("")) {
argvals = new ArrayList<Object>();
} else {
argvals = new ArrayList<Object>();
for (String v : arg_str.split(",")) {
argvals.add(interpret_expression(ctx, v, local_vars, 20));
}
}
if (member.equals("split")) {
//assert argvals == ('',)
return list(obj);
}
if (member.equals("join")) {
//assert len(argvals) == 1
return join((List<Object>) obj, argvals.get(0));
}
if (member.equals("reverse")) {
//assert len(argvals) == 0
reverse(obj);
return obj;
}
if (member.equals("slice")) {
//assert len(argvals) == 1
return slice(obj, (Integer) argvals.get(0));
}
if (member.equals("splice")) {
//assert isinstance(obj, list)
int index = (Integer) argvals.get(0);
int howMany = (Integer) argvals.get(1);
List<Object> res = new ArrayList<Object>();
List<Object> list = (List<Object>) obj;
for (int i = index; i < Math.min(index + howMany, len(obj)); i++) {
res.add(list.remove(index));
}
return res.toArray();
}
return ((JsObject) obj).functions.get(member).eval(argvals.toArray());
}
m = Pattern.compile("^(?<in>[a-z]+)\\[(?<idx>.+)\\]$").matcher(expr);
if (m.find()) {
Object val = local_vars.get(m.group("in"));
Object idx = interpret_expression(ctx, m.group("idx"), local_vars, allow_recursion - 1);
return ((List<?>) val).get((Integer) idx);
}
m = Pattern.compile("^(?<a>.+?)(?<op>[%])(?<b>.+?)$").matcher(expr);
if (m.find()) {
Object a = interpret_expression(ctx, m.group("a"), local_vars, allow_recursion);
Object b = interpret_expression(ctx, m.group("b"), local_vars, allow_recursion);
return (Integer) a % (Integer) b;
}
m = Pattern.compile("^(?<func>[a-zA-Z]+)\\((?<args>[a-z0-9,]+)\\)$").matcher(expr);
if (m.find()) {
String fname = m.group("func");
if (!ctx.functions.containsKey(fname) && ctx.jscode.length() > 0) {
ctx.functions.put(fname, extract_function(ctx, fname));
}
List<Object> argvals = new ArrayList<Object>();
for (String v : m.group("args").split(",")) {
if (isdigit(v)) {
argvals.add(Integer.valueOf(v));
} else {
argvals.add(local_vars.get(v));
}
}
return ctx.functions.get(fname).eval(argvals.toArray());
}
throw new JsError(String.format("Unsupported JS expression %s", expr));
}
private static JsObject extract_object(final JsContext ctx, String objname) {
JsObject obj = new JsObject();
String obj_mRegex = String.format("(var\\p{Space}+)?%1$s\\p{Space}*=\\p{Space}*\\{", escape(objname)) + "\\p{Space}*(?<fields>([a-zA-Z$0-9]+\\p{Space}*:\\p{Space}*function\\(.*?\\)\\p{Space}*\\{.*?\\})*)\\}\\p{Space}*;";
final Matcher obj_m = Pattern.compile(obj_mRegex).matcher(ctx.jscode);
obj_m.find();
String fields = obj_m.group("fields");
// Currently, it only supports function definitions
final Matcher fields_m = Pattern.compile("(?<key>[a-zA-Z$0-9]+)\\p{Space}*:\\p{Space}*function\\((?<args>[a-z,]+)\\)\\{(?<code>[^\\}]+)\\}").matcher(fields);
while (fields_m.find()) {
final String[] argnames = mscpy(fields_m.group("args").split(","));
LambdaN f = build_function(ctx, argnames, fields_m.group("code"));
obj.functions.put(fields_m.group("key"), f);
}
return obj;
}
private static LambdaN extract_function(final JsContext ctx, String funcname) {
String func_mRegex = String.format("(function %1$s|[\\{;]%1$s\\p{Space}*=\\p{Space}*function)", escape(funcname)) + "\\((?<args>[a-z,]+)\\)\\{(?<code>[^\\}]+)\\}";
final Matcher func_m = Pattern.compile(func_mRegex).matcher(ctx.jscode);
if (!func_m.find()) {
throw new JsError("Could not find JS function " + funcname);
}
final String[] argnames = mscpy(func_m.group("args").split(","));
return build_function(ctx, argnames, func_m.group("code"));
}
private static LambdaN build_function(final JsContext ctx, final String[] argnames, String code) {
final String[] stmts = mscpy(code.split(";"));
return new LambdaN() {
@Override
public Object eval(Object[] args) {
Map<String, Object> local_vars = new HashMap<String, Object>();
for (int i = 0; i < argnames.length; i++) {
local_vars.put(argnames[i], args[i]);
}
Object res = null;
for (String stmt : stmts) {
res = interpret_statement(ctx, stmt, local_vars, 20);
}
return res;
}
};
}
}