// Copyright 2016 Michel Kraemer // // Licensed 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 de.undercouch.citeproc.script; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.eclipsesource.v8.V8; import com.eclipsesource.v8.V8Array; import com.eclipsesource.v8.V8Object; import com.eclipsesource.v8.V8RuntimeException; import com.eclipsesource.v8.V8Value; import de.undercouch.citeproc.AbbreviationProvider; import de.undercouch.citeproc.ItemDataProvider; import de.undercouch.citeproc.LocaleProvider; import de.undercouch.citeproc.VariableWrapper; import de.undercouch.citeproc.VariableWrapperParams; import de.undercouch.citeproc.csl.CSLAbbreviationList; import de.undercouch.citeproc.csl.CSLItemData; import de.undercouch.citeproc.helper.json.JsonBuilder; import de.undercouch.citeproc.helper.json.JsonObject; import de.undercouch.citeproc.helper.json.StringJsonBuilder; /** * Executes JavaScript using the V8 runtime * @author Michel Kraemer */ public class V8ScriptRunner extends AbstractScriptRunner { /** * The V8 runtime */ private final V8 runtime; public V8ScriptRunner() { runtime = V8.createV8Runtime(); } public void release() { runtime.release(); } @Override public String getName() { return "V8"; } @Override public String getVersion() { return String.valueOf(runtime.getBuildID()); } @Override public void eval(Reader reader) throws ScriptRunnerException, IOException { //read whole script into memory StringBuilder sb = new StringBuilder(); char[] buf = new char[1024 * 10]; int read; while ((read = reader.read(buf)) >= 0) { sb.append(buf, 0, read); } //execute script runtime.executeVoidScript(sb.toString()); } @Override @SuppressWarnings("unchecked") public <T> T callMethod(String name, Class<T> resultType, Object... args) throws ScriptRunnerException { Set<V8Value> newValues = new HashSet<>(); V8Array parameters = convertArguments(args, newValues); try { if (String.class.isAssignableFrom(resultType)) { return (T)runtime.executeStringFunction(name, parameters); } return convert(runtime.executeObjectFunction(name, parameters), resultType); } catch (V8RuntimeException e) { throw new ScriptRunnerException("Could not call method", e); } finally { newValues.forEach(V8Value::release); } } @Override public void callMethod(String name, Object... args) throws ScriptRunnerException { Set<V8Value> newValues = new HashSet<>(); V8Array parameters = convertArguments(args, newValues); try { runtime.executeVoidFunction(name, parameters); } catch (V8RuntimeException e) { throw new ScriptRunnerException("Could not call method", e); } finally { newValues.forEach(V8Value::release); } } @Override @SuppressWarnings("unchecked") public <T> T callMethod(Object obj, String name, Class<T> resultType, Object... args) throws ScriptRunnerException { Set<V8Value> newValues = new HashSet<>(); V8Array parameters = convertArguments(args, newValues); try { V8Object vo = (V8Object)obj; if (String.class.isAssignableFrom(resultType)) { return (T)vo.executeStringFunction(name, parameters); } return convert(vo.executeObjectFunction(name, parameters), resultType); } catch (V8RuntimeException e) { throw new ScriptRunnerException("Could not call method", e); } finally { newValues.forEach(V8Value::release); } } @Override public void callMethod(Object obj, String name, Object... args) throws ScriptRunnerException { Set<V8Value> newValues = new HashSet<>(); V8Array parameters = convertArguments(args, newValues); try { ((V8Object)obj).executeVoidFunction(name, parameters); } catch (V8RuntimeException e) { throw new ScriptRunnerException("Could not call method", e); } finally { newValues.forEach(V8Value::release); } } @Override @SuppressWarnings("unchecked") public <T> T convert(Object r, Class<T> resultType) { if (List.class.isAssignableFrom(resultType) && r instanceof V8Array) { V8Array arr = (V8Array)r; r = convertArray(arr); arr.release(); } else if (Map.class.isAssignableFrom(resultType) && r instanceof V8Object) { V8Object obj = (V8Object)r; r = convertObject(obj); obj.release(); } return (T)r; } /** * Recursively convert a V8 array to a list and release it * @param arr the array to convert * @return the list */ private List<Object> convertArray(V8Array arr) { List<Object> l = new ArrayList<>(); for (int i = 0; i < arr.length(); ++i) { Object o = arr.get(i); if (o instanceof V8Array) { o = convert((V8Array)o, List.class); } else if (o instanceof V8Object) { o = convert((V8Object)o, Map.class); } l.add(o); } return l; } /** * Recursively convert a V8 object to a map and release it * @param obj the object to convert * @return the map */ private Map<String, Object> convertObject(V8Object obj) { if (obj.isUndefined()) { return null; } Map<String, Object> r = new LinkedHashMap<>(); for (String k : obj.getKeys()) { Object o = obj.get(k); if (o instanceof V8Array) { o = convert((V8Array)o, List.class); } else if (o instanceof V8Object) { o = convert((V8Object)o, Map.class); } r.put(k, o); } return r; } @Override public JsonBuilder createJsonBuilder() { return new StringJsonBuilder(this); } /** * Convert an array of object to a V8 array * @param args the array to convert * @param newValues a set that will be filled with all V8 values created * during the operation * @return the V8 array */ private V8Array convertArguments(Object[] args, Set<V8Value> newValues) { //create the array V8Array result = new V8Array(runtime); newValues.add(result); //convert the values for (int i = 0; i < args.length; ++i) { Object o = args[i]; if (o == null) { result.push(V8Value.NULL); } else if (o instanceof JsonObject || o instanceof Collection || o.getClass().isArray() || o instanceof Map) { V8Object v = runtime.executeObjectScript("(" + createJsonBuilder().toJson(o).toString() + ")"); newValues.add(v); result.push(v); } else if (o instanceof String) { result.push((String)o); } else if (o instanceof Integer) { result.push((Integer)o); } else if (o instanceof Boolean) { result.push((Boolean)o); } else if (o instanceof Double) { result.push((Double)o); } else if (o instanceof ItemDataProvider) { o = new ItemDataProviderWrapper((ItemDataProvider)o); V8Object v8o = convertJavaObject(o); newValues.add(v8o); result.push(v8o); } else if (o instanceof AbbreviationProvider) { o = new AbbreviationProviderWrapper((AbbreviationProvider)o); V8Object v8o = convertJavaObject(o); newValues.add(v8o); result.push(v8o); } else if (o instanceof VariableWrapper) { o = new VariableWrapperWrapper((VariableWrapper)o); V8Object v8o = convertJavaObject(o); newValues.add(v8o); result.push(v8o); } else if (o instanceof V8ScriptRunner || o instanceof LocaleProvider) { V8Object v8o = convertJavaObject(o); newValues.add(v8o); result.push(v8o); } else if (o instanceof V8Value) { //already converted V8Value v = (V8Value)o; result.push(v); } else { throw new IllegalArgumentException("Unsupported argument: " + o.getClass()); } } return result; } /** * Convert a Java object to a V8 object. Register all methods of the * Java object as functions in the created V8 object. * @param o the Java object * @return the V8 object */ private V8Object convertJavaObject(Object o) { V8Object v8o = new V8Object(runtime); Method[] methods = o.getClass().getMethods(); for (Method m : methods) { v8o.registerJavaMethod(o, m.getName(), m.getName(), m.getParameterTypes()); } return v8o; } /** * Wraps around {@link ItemDataProvider} and converts all retrieved * items to JSON objects * @author Michel Kraemer */ private class ItemDataProviderWrapper { private final ItemDataProvider provider; public ItemDataProviderWrapper(ItemDataProvider provider) { this.provider = provider; } /** * Retrieve an item from the {@link ItemDataProvider} and convert * it to a JSON object * @param id the ID of the item to retrieve * @return the JSON object */ @SuppressWarnings("unused") public Object retrieveItem(String id) { CSLItemData item = provider.retrieveItem(id); if (item == null) { return null; } return item.toJson(createJsonBuilder()); } } /** * Wraps around {@link AbbreviationProvider} and converts all retrieved * abbreviation lists to JSON objects * @author Michel Kraemer */ private class AbbreviationProviderWrapper { private final AbbreviationProvider provider; public AbbreviationProviderWrapper(AbbreviationProvider provider) { this.provider = provider; } /** * Retrieve an abbreviation list from the {@link AbbreviationProvider} * and convert it to a JSON object * @param id the name of the list to retrieve * @return the JSON object */ @SuppressWarnings("unused") public Object getAbbreviations(String name) { CSLAbbreviationList a = provider.getAbbreviations(name); if (a == null) { return null; } return a.toJson(createJsonBuilder()); } } /** * Wraps around {@link VariableWrapper} and converts * {@link VariableWrapperParams} objects to JSON objects * @author Michel Kraemer */ private class VariableWrapperWrapper { private final VariableWrapper wrapper; public VariableWrapperWrapper(VariableWrapper wrapper) { this.wrapper = wrapper; } /** * Call the {@link VariableWrapper} with the given parameters * @param params the context in which an item should be rendered * @param prePunct the text that precedes the item to render * @param str the item to render * @param postPunct the text that follows the item to render * @return the string to be rendered */ @SuppressWarnings("unused") public String wrap(Object params, String prePunct, String str, String postPunct) { Map<String, Object> m = convertObject((V8Object)params); VariableWrapperParams p = VariableWrapperParams.fromJson(m); return wrapper.wrap(p, prePunct, str, postPunct); } } }