/**
* 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.jooby.assets;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.eclipsesource.v8.JavaCallback;
import com.eclipsesource.v8.JavaVoidCallback;
import com.eclipsesource.v8.Releasable;
import com.eclipsesource.v8.V8;
import com.eclipsesource.v8.V8Array;
import com.eclipsesource.v8.V8Function;
import com.eclipsesource.v8.V8Object;
import com.eclipsesource.v8.utils.V8ObjectUtils;
import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import javaslang.control.Try;
public class V8Context {
public interface Callback {
String call(V8Context ctx) throws Exception;
}
public final V8 v8;
private String id;
private V8Context(final String global, final String id) {
this(V8.createV8Runtime(global), id);
}
private V8Context(final V8 v8, final String id) {
this.v8 = v8;
this.id = id;
console(id);
assets(v8);
b64(v8);
}
public V8Object hash() {
return register(new V8Object(v8));
}
public V8Function function(final JavaCallback callback) {
return register(new V8Function(v8, callback));
}
public V8Object hash(final Map<String, Object> hash) {
return register(V8ObjectUtils.toV8Object(v8, hash));
}
public V8Array array() {
return register(new V8Array(v8));
}
public V8Array array(final List<? extends Object> value) {
return register(V8ObjectUtils.toV8Array(v8, value));
}
public Object load(final String path) throws Exception {
return register(v8.executeScript(readFile(path), path, 0));
}
public String invoke(final String path, final Object... args) throws Exception {
V8Function fn = register((V8Function) load(path));
Object value = register(fn.call(v8, array(Arrays.asList(args))));
if (value instanceof String) {
return value.toString();
}
List<AssetProblem> problems = problems(value);
if (problems.size() > 0) {
throw new AssetException(id, problems);
}
return ((V8Object) value).getString("output");
}
private List<AssetProblem> problems(final Object value) {
if (value instanceof V8Array) {
return problems((V8Array) value);
}
V8Object hash = (V8Object) value;
if (hash.contains("errors")) {
return problems(register(hash.getArray("errors")));
}
if (hash.contains("message")) {
return ImmutableList.of(problem(hash));
}
return Collections.emptyList();
}
private List<AssetProblem> problems(final V8Array array) {
ImmutableList.Builder<AssetProblem> result = ImmutableList.builder();
for (int i = 0; i < array.length(); i++) {
result.add(problem(register(array.getObject(i))));
}
return result.build();
}
private <T> Optional<T> get(final String name, final Function<String, T> provider) {
return Try.of(() -> Optional.of(register(provider.apply(name)))).getOrElse(Optional.empty());
}
private AssetProblem problem(final V8Object js) {
Optional<Integer> line = get("line", name -> ((Number) js.get(name)).intValue());
Optional<Integer> column = get("column", name -> ((Number) js.get(name)).intValue());
Optional<String> filename = get("filename", js::getString);
Optional<String> evidence = get("evidence", js::getString);
Optional<String> message = get("message", js::getString);
return new AssetProblem(filename.orElse("file.js"), line.orElse(-1), column.orElse(-1),
message.orElse(""), evidence.orElse(null));
}
private URL resolve(final String path) {
URL resource = getClass().getResource(path.startsWith("/") ? path : "/" + path);
return resource;
}
private boolean exists(final String path) {
return resolve(path) != null;
}
private String readFile(final String path) throws IOException {
URL resource = resolve(path);
if (resource == null) {
throw new FileNotFoundException(path);
}
try (InputStream stream = resource.openStream()) {
return new String(ByteStreams.toByteArray(stream), "UTF-8");
}
}
public static String run(final Callback callback) throws Exception {
return run(null, callback);
}
public static String run(final String global, final Callback callback) throws Exception {
V8Context ctx = new V8Context(global, classname(callback));
try {
return callback.call(ctx);
} finally {
ctx.v8.release();
}
}
private static String classname(final Callback callback) {
String logname = callback.getClass().getSimpleName();
logname = logname.substring(0, logname.indexOf("$"));
return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, logname);
}
private <T> T register(final T value) {
if (value instanceof Releasable) {
v8.registerResource((Releasable) value);
}
return value;
}
private JavaVoidCallback console(final Consumer<String> log) {
return (self, args) -> {
StringBuilder buff = new StringBuilder();
for (int i = 0; i < args.length(); i++) {
buff.append(register(args.get(i)));
}
log.accept(buff.toString());
};
}
private void console(final String logname) {
V8Object console = hash();
Logger log = LoggerFactory.getLogger(logname);
v8.add("console", console);
console.registerJavaMethod(console(log::info), "log");
console.registerJavaMethod(console(log::info), "info");
console.registerJavaMethod(console(log::error), "error");
console.registerJavaMethod(console(log::debug), "debug");
console.registerJavaMethod(console(log::warn), "warn");
}
private void b64(final V8 v8) {
v8.registerJavaMethod((JavaCallback) (receiver, args) -> {
byte[] bytes = args.get(0).toString().getBytes(StandardCharsets.UTF_8);
return BaseEncoding.base64().encode(bytes);
}, "btoa");
v8.registerJavaMethod((JavaCallback) (receiver, args) -> {
byte[] atob = BaseEncoding.base64().decode(args.get(0).toString());
return new String(atob, StandardCharsets.UTF_8);
}, "atob");
}
private void assets(final V8 v8) {
V8Object assets = hash();
v8.add("assets", assets);
assets.registerJavaMethod((JavaCallback) (receiver, args) -> {
try {
return readFile(args.get(0).toString());
} catch (IOException ex) {
// we can't fire exceptions from Java :S
return V8.getUndefined();
}
}, "readFile");
assets.registerJavaMethod((JavaCallback) (receiver, args) -> exists(args.get(0).toString()),
"exists");
assets.registerJavaMethod((JavaCallback) (receiver, args) -> {
try {
return load(args.get(0).toString());
} catch (Exception ex) {
// we can't fire exceptions from Java :S
return V8.getUndefined();
}
}, "load");
}
}