/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.rhino;
import android.support.annotation.NonNull;
import android.util.Log;
import com.facebook.stetho.common.LogUtil;
import com.facebook.stetho.inspector.console.CLog;
import com.facebook.stetho.inspector.console.RuntimeRepl;
import com.facebook.stetho.inspector.console.RuntimeReplFactory;
import com.facebook.stetho.inspector.protocol.module.Console;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* <p>Builder used to setup the javascript runtime to be used by stetho.</p>
*
* <p>You can use this builder to configure the javacript environment by preloading:
* <ul>
* <li>Java classes</li>
* <li>Java packages (with all their java classes)</li>
* <li>Variables</li>
* <li>Functions</li>
* </ul>
* </p>
*
* <p>Your application context package is automatically visible with this builder.</p>
*/
public class JsRuntimeReplFactoryBuilder {
/**
* Name of the "source" file used for reporting JavaScript compilation errors (or runtime errors).
* Since this is evaluated from a chrome inspector window we pass "chrome".
*/
private static final String SOURCE_NAME = "chrome";
/**
* Android application context.
*/
private final android.content.Context mContext;
/**
* Java classes to import into the javascript environment.
*/
private final Set<Class<?>> mClasses = new HashSet<>();
/**
* Java packages to import into the javascript environment.
* All classes inside the package will be imported.
*/
private final Set<String> mPackages = new HashSet<>();
/**
* Variables to bind to the javascript environment.
*/
private final Map<String, Object> mVariables = new HashMap<>();
/**
* Global mFunctions to add to the javascript environment.
*/
private final Map<String, Function> mFunctions = new HashMap<>();
public static RuntimeReplFactory defaultFactory(@NonNull android.content.Context context) {
return new JsRuntimeReplFactoryBuilder(context).build();
}
public JsRuntimeReplFactoryBuilder(@NonNull android.content.Context context) {
mContext = context;
// We import the app's package name by default
mPackages.add(context.getPackageName());
// Predefine $_ which holds the value of the last expression evaluated
mVariables.put("$_", Context.getUndefinedValue());
}
/**
* Request that the given java class be imported in the javascript runtime.
* @param aClass the java class to import
* @return the builder
*/
public @NonNull
JsRuntimeReplFactoryBuilder importClass(@NonNull Class<?> aClass) {
mClasses.add(aClass);
return this;
}
/**
* Request that the given package name will be imported in the javascript runtime.
* This means that all classes (enums and interfaces) will be imported.
* @param packageName the java package name to import
* @return the builder
*/
public @NonNull
JsRuntimeReplFactoryBuilder importPackage(@NonNull String packageName) {
mPackages.add(packageName);
return this;
}
/**
* Add a variable (binding) to the javascript environment.
* @param name the javascript variable name
* @param value the value to add
* @return the builder
*/
public JsRuntimeReplFactoryBuilder addVariable(@NonNull String name, Object value) {
mVariables.put(name, value);
return this;
}
/**
* Adds a function to the javascript environment.
* @param name the javascript function name
* @param function the function
* @return the builder
*/
public @NonNull
JsRuntimeReplFactoryBuilder addFunction(@NonNull String name, @NonNull Function function) {
mFunctions.put(name, function);
return this;
}
/**
* Build the runtime REPL instance to be supplied to the Stetho {@code Runtime} module.
*/
public RuntimeReplFactory build() {
return new RuntimeReplFactory() {
@Override
public RuntimeRepl newInstance() {
return new JsRuntimeRepl(initJsScope());
}
};
}
/**
* Initializes a proper javascript scope (runtime environment holding variables).
* @return a javascript scope
*/
private @NonNull ScriptableObject initJsScope() {
final Context jsContext = JsRuntimeRepl.enterJsContext();
try {
ScriptableObject scope = initJsScope(jsContext);
return scope;
} finally {
Context.exit();
}
}
private @NonNull ScriptableObject initJsScope(@NonNull Context jsContext) {
// Set the main Rhino goodies
ImporterTopLevel importerTopLevel = new ImporterTopLevel(jsContext);
ScriptableObject scope = jsContext.initStandardObjects(importerTopLevel, false);
ScriptableObject.putProperty(scope, "context", Context.javaToJS(mContext, scope));
try {
importClasses(jsContext, scope);
importPackages(jsContext, scope);
importConsole(scope);
importVariables(scope);
importFunctions(scope);
} catch (StethoJsException e) {
String message = String.format("%s\n%s", e.getMessage(), Log.getStackTraceString(e));
LogUtil.e(e, message);
CLog.writeToConsole(Console.MessageLevel.ERROR, Console.MessageSource.JAVASCRIPT, message);
}
return scope;
}
private void importClasses(@NonNull Context jsContext, @NonNull ScriptableObject scope) throws StethoJsException {
// Import the classes that the caller requested
for (Class<?> aClass : mClasses) {
String className = aClass.getName();
try {
// import from default classes
String expression = String.format("importClass(%s)", className);
jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null);
} catch (Exception e) {
try {
// import from application classes
String expression = String.format("importClass(Packages.%s)", className);
jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null);
} catch (Exception e1) {
throw new StethoJsException(e1, "Failed to import class: %s", className);
}
}
}
}
private void importPackages(@NonNull Context jsContext, @NonNull ScriptableObject scope) throws StethoJsException {
// Import the packages that the caller requested
for (String packageName : mPackages) {
try {
// import from default packages
String expression = String.format("importPackage(%s)", packageName);
jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null);
} catch (Exception e) {
try {
// import from application packages
String expression = String.format("importPackage(Packages.%s)", packageName);
jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null);
} catch (Exception e1) {
throw new StethoJsException(e, "Failed to import package: %s", packageName);
}
}
}
}
private void importConsole(@NonNull ScriptableObject scope) throws StethoJsException {
// Set the `console` object
try {
ScriptableObject.defineClass(scope, JsConsole.class);
JsConsole console = new JsConsole(scope);
scope.defineProperty("console", console, ScriptableObject.DONTENUM);
} catch (Exception e) {
throw new StethoJsException(e, "Failed to setup javascript console");
}
}
private void importVariables(@NonNull ScriptableObject scope) throws StethoJsException {
// Define the variables
for (Map.Entry<String, Object> entrySet : mVariables.entrySet()) {
String varName = entrySet.getKey();
Object varValue = entrySet.getValue();
try {
Object jsValue;
if (varValue instanceof Scriptable || varValue instanceof Undefined) {
jsValue = varValue;
} else {
jsValue = Context.javaToJS(varValue, scope);
}
ScriptableObject.putProperty(scope, varName, jsValue);
} catch (Exception e) {
throw new StethoJsException(e, "Failed to setup variable: %s", varName);
}
}
}
private void importFunctions(@NonNull ScriptableObject scope) throws StethoJsException {
// Define the functions
for (Map.Entry<String, Function> entrySet : mFunctions.entrySet()) {
String functionName = entrySet.getKey();
Function function = entrySet.getValue();
try {
ScriptableObject.putProperty(scope, functionName, function);
} catch (Exception e) {
throw new StethoJsException(e, "Failed to setup function: %s", functionName);
}
}
}
private static class StethoJsException extends Exception {
StethoJsException(Throwable rootCause, String format, Object...args) {
super(args.length == 0 ? format : String.format(format, args), rootCause);
}
}
}