/**
* 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 static java.util.Objects.requireNonNull;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.jooby.Route;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.eclipsesource.v8.NodeJS;
import com.eclipsesource.v8.V8;
import com.eclipsesource.v8.utils.MemoryManager;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import javaslang.control.Try;
import javaslang.control.Try.CheckedConsumer;
/**
* Helper class that allows you extract and unpack a nodejs library from classpath.
*
* It provides a nice and safe nodejs execution environment.
*
* @author edgar
*
*/
public class Nodejs {
private static class Library implements Closeable {
private Closeable closer;
private Path root;
private Library(final Path path) {
this.closer = null;
this.root = path;
}
private Library(final FileSystem fs) throws IOException {
this.closer = fs;
this.root = fs.getPath("/");
}
@Override
public void close() throws IOException {
if (closer != null) {
closer.close();
}
}
public Stream<Path> stream() throws IOException {
return Files.walk(root);
}
}
/** The logging system. */
private final Logger log = LoggerFactory.getLogger(getClass());
private ClassLoader loader;
private File basedir;
private NodeJS node;
private MemoryManager scope;
private Set<StandardCopyOption> coptions = Collections.emptySet();
/**
* Creates a new {@link Nodejs}.
*
* @param basedir Base dir where to deploy a library.
* @param loader Class loader to use.
*/
public Nodejs(final File basedir, final ClassLoader loader) {
this.basedir = requireNonNull(basedir, "Basedir required.");
this.loader = requireNonNull(loader, "ClassLoader required.");
this.node = NodeJS.createNodeJS();
V8 v8 = node.getRuntime();
this.scope = new MemoryManager(v8);
}
/**
* Creates a new {@link Nodejs}.
*
* @param basedir Base dir where to deploy a library.
*/
public Nodejs(final File basedir) {
this(basedir, Nodejs.class.getClassLoader());
}
/**
* Creates a new {@link Nodejs} and use the <code>java.io.tmpdir</code> as base dir.
*/
public Nodejs() {
this(new File(System.getProperty("java.io.tmpdir")));
}
/**
* Force to unpack library files every {@link #exec(String)} call. This is useful for development
* and testing. By default files are unpacked just once (overwrite = false).
*
* @param overwrite True, to force overwrite.
* @return
*/
public Nodejs overwrite(final boolean overwrite) {
if (overwrite) {
this.coptions = EnumSet.of(StandardCopyOption.REPLACE_EXISTING);
} else {
this.coptions = Collections.emptySet();
}
return this;
}
/**
* Unpack and execute a nodejs library. Once unpack this method will execute one of these scripts
* in the following order: 1) [library].js; 2) main.js; 3) index.js.
*
* The first file found will be executed.
*
* @param library Library to unpack and execute this library.
* @throws Throwable If something goes wrong.
*/
public void exec(final String library) throws Throwable {
exec(library, v8 -> {
});
}
/**
* Unpack and execute a nodejs library. Once unpack this method will execute one of these scripts
* in the following order: 1) [library].js; 2) main.js; 3) index.js.
*
* The first file found will be executed.
*
* @param library Library to unpack and execute this library.
* @throws Throwable If something goes wrong.
*/
public void exec(final String library, final CheckedConsumer<V8> callback) throws Throwable {
Path basedir = deploy(library);
List<String> candidates = Arrays.asList(
basedir.getFileName().toString() + ".js",
"main.js",
"index.js");
Path main = candidates.stream()
.map(it -> basedir.resolve(it))
.filter(it -> it.toFile().exists())
.findFirst()
.orElseThrow(() -> new FileNotFoundException(candidates.toString()));
callback.accept(node.getRuntime());
node.exec(main.toFile());
while (node.isRunning()) {
node.handleMessage();
}
}
/**
* Release nodejs and v8 resources.
*/
public void release() {
Try.run(scope::release);
node.release();
}
public Path deploy(final String library) throws Exception {
URL url = loader.getResource(library);
if (url == null) {
throw new FileNotFoundException(library);
}
URI uri = url.toURI();
log.debug("{}", uri);
Path outdir = this.basedir.toPath().resolve("node_modules").resolve(library.replace("/", "."));
Optional<Path> basedir = Try.of(() -> Paths.get(uri)).toJavaOptional();
String libroot = Route.normalize("/" + library);
try (Library lib = loadLibrary(uri)) {
try (Stream<Path> stream = lib.stream()) {
stream.filter(it -> !Files.isDirectory(it))
.forEach(it -> {
String relative = basedir.map(d -> d.relativize(it).toString())
.orElseGet(() -> {
String fname = it.toString();
if (fname.startsWith(libroot)) {
fname = fname.substring(libroot.length());
}
return fname.substring(1);
});
Path output = outdir.resolve(relative);
File fout = output.toFile();
boolean copy = !fout.exists()
|| coptions.contains(StandardCopyOption.REPLACE_EXISTING);
if (copy) {
log.debug("copying {} to {}", it, fout);
fout.getParentFile().mkdirs();
StandardCopyOption[] coptions = this.coptions
.toArray(new StandardCopyOption[this.coptions.size()]);
Try.run(() -> Files.copy(it, output, coptions))
.onFailure(x -> log.error("can't copy {}", it, x));
}
});
}
}
return outdir;
}
private Library loadLibrary(final URI lib) {
return Try.of(() -> FileSystems.newFileSystem(lib, Maps.newHashMap()))
.map(it -> Try.of(() -> new Library(it)).get())
.recoverWith(x -> Try.of(() -> new Library(Paths.get(lib))))
.get();
}
/**
* Execute the given nodejs callback and automatically release v8 and nodejs resources.
*
* @param callback Nodejs callback.
*/
public static void run(final CheckedConsumer<Nodejs> callback) {
run(new File(System.getProperty("java.io.tmpdir")), callback);
}
/**
* Execute the given nodejs callback and automatically release v8 and nodejs resources.
*
* @param basedir Base dir where to deploy a library.
* @param callback Nodejs callback.
*/
public static void run(final File basedir, final CheckedConsumer<Nodejs> callback) {
Nodejs node = new Nodejs(basedir);
try {
callback.accept(node);
} catch (Throwable x) {
throw Throwables.propagate(x);
} finally {
node.release();
}
}
}