blob: 47ef3deaf8d5763fb496812af799e9000ed9c848 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pig.scripting.js;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pig.FuncSpec;
import org.apache.pig.impl.PigContext;
import org.apache.pig.impl.util.UDFContext;
import org.apache.pig.scripting.ScriptEngine;
import org.apache.pig.tools.pigstats.PigStats;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.NativeFunction;
import org.mozilla.javascript.Scriptable;
/**
* {@link ScriptEngine} implementation for JavaScript
*/
public class JsScriptEngine extends ScriptEngine {
private static final Log LOG = LogFactory.getLog(JsScriptEngine.class);
/** holds the instance created on client side to parse the js */
private static JsScriptEngine clientInstance;
/** holds the singleton for UDFs */
private static final class Holder {
static JsScriptEngine instance;
static {
if (clientInstance!=null) {
/** on client side UDFs re-use the same instance */
instance = clientInstance;
} else {
/** on the hadoop slave a new instance is created using the UDFContext for initialization */
instance = new JsScriptEngine();
String scriptPath = (String) UDFContext.getUDFContext().getUDFProperties(JsFunction.class).get(JsScriptEngine.class.getName()+".scriptFile");
instance.scriptPath = scriptPath;
if (scriptPath == null) {
throw new IllegalStateException("could not get script path from UDFContext");
}
InputStream is = getScriptAsStream(scriptPath);
try {
instance.load(scriptPath, is);
} finally {
try {
is.close();
} catch (IOException e) {
LOG.warn("Could not close stream for file "+scriptPath, e);
}
}
}
}
}
public static JsScriptEngine getInstance() {
JsScriptEngine instance = Holder.instance;
/** add script path information in the UDFContext */
UDFContext.getUDFContext().getUDFProperties(JsFunction.class).put(JsScriptEngine.class.getName()+".scriptFile", instance.scriptPath);
return instance;
}
private ThreadLocal<Context> cx = new ThreadLocal<Context>();
private Scriptable scope;
private String scriptPath;
/** print functions to help with debugging */
private static final String printSource =
"function print(str) { \n" +
" if (typeof(str) == 'undefined') { \n" +
" str = 'undefined'; \n" +
" } else if (str == null) { \n" +
" str = 'null'; \n" +
" }\n" +
" java.lang.System.out.print(String(str));\n" +
"}\n" +
"function println(str) { \n" +
" if (typeof(str) == 'undefined') { \n" +
" str = 'undefined'; \n" +
" } else if (str == null) { \n" +
" str = 'null'; \n" +
" }\n" +
" java.lang.System.out.println(String(str));\n" +
"}";
/**
* @return the javascript context for the current thread
*/
private Context getContext() {
Context context = cx.get();
if (context == null) {
context = Context.enter();
cx.set(context);
}
return context;
}
/**
* evaluate a javascript String
* @param name the name of the script (for error messages)
* @param script the content of the script
* @return the result
*/
public Object jsEval(String name, String script) {
try {
return getContext().evaluateString(scope, script, name, 1, null);
} catch (EcmaError e) {
throw new RuntimeException("can't evaluate "+name+": "+script,e);
}
}
/**
* evaluate javascript from a reader
* @param name the name of the script (for error messages)
* @param scriptReader the content of the script
* @return the result
*/
public Object jsEval(String name, Reader scriptReader) {
try {
return getContext().evaluateReader(scope, scriptReader, name, 1, null);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* put a value in the current scope
* @param name the name of the variable
* @param value its value
*/
public void jsPut(String name, Object value) {
scope.put(name, scope, value);
}
/**
* call a javascript function
* @param functionName the name of the function
* @param passedParams the parameters to pass
* @return the result of the function
*/
public Object jsCall(String functionName, Object[] passedParams) {
Function f = (Function)scope.get(functionName, scope);
Object result = f.call(getContext(), scope, scope, passedParams);
return result;
}
Scriptable getScope() {
return scope;
}
/**
* creates a new JavaScript object
* @return an empty object
*/
public Scriptable jsNewObject() {
return getContext().newObject(scope);
}
/**
* creates a new javascript array
* @param size the size of the array
* @return an empty array of the given size
*/
public Scriptable jsNewArray(long size) {
return getContext().newArray(scope, (int)size);
}
public JsScriptEngine() {
Context context = getContext();
scope = new ImporterTopLevel(context); // so that we can use importPackage()
context.evaluateString(scope, printSource, "print", 1, null); // so that we can print on std out
}
protected Object eval(String expr) {
return jsEval(expr, "new String("+expr+")");
}
protected void load(String name, InputStream script) {
jsEval(name, new InputStreamReader(script));
}
@Override
protected Map<String, List<PigStats>> main(PigContext pigContext,
String scriptFile) throws IOException {
File f = new File(scriptFile);
if (!f.canRead()) {
throw new IOException("Can't read file: " + scriptFile);
}
registerFunctions(scriptFile, null, pigContext);
// run main
jsEval("main", "main();");
return getPigStatsMap();
}
@Override
public void registerFunctions(String path, String namespace,
PigContext pigContext) throws IOException {
// to enable passing of information to the slave
this.scriptPath = path;
JsScriptEngine.clientInstance = this;
pigContext.addScriptJar(getJarPath(Context.class));
namespace = (namespace == null) ? "" : namespace + NAMESPACE_SEPARATOR;
FileInputStream fis = new FileInputStream(path);
try {
load(path, fis);
} finally {
fis.close();
}
Object[] ids = scope.getIds();
for (Object id : ids) {
if (id instanceof String) {
String name = (String) id;
Object value = scope.get(name, scope);
if (value instanceof Function
&& !name.equals("print") // ignoring the print functions that were added by this class in the initialization
&& !name.equals("println")) {
FuncSpec funcspec = new FuncSpec(JsFunction.class.getCanonicalName() + "('" + name + "')");
LOG.info("Register scripting UDF: " + name);
pigContext.registerFunction(namespace + name, funcspec);
}
}
}
pigContext.addScriptFile(path);
}
@Override
protected Map<String, Object> getParamsFromVariables() throws IOException {
Map<String, Object> params = new HashMap<String, Object>();
Object[] ids = scope.getIds();
for (Object id : ids) {
if (id instanceof String) {
String name = (String) id;
Object value = scope.get(name, scope);
if (!(value instanceof NativeFunction)) {
LOG.debug("Registering parameter "+name+" => "+scope.get(name, scope));
params.put(name, eval(name).toString());
}
}
}
return params;
}
@Override
protected String getScriptingLang() {
return "javascript";
}
}