package jaci.openrio.toast.core.script;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import jaci.openrio.toast.core.Toast;
import jaci.openrio.toast.core.ToastBootstrap;
import jaci.openrio.toast.core.io.Storage;
import jaci.openrio.toast.core.io.usb.MassStorageDevice;
import jaci.openrio.toast.core.io.usb.USBMassStorage;
import jaci.openrio.toast.core.script.js.JavaScript;
import jaci.openrio.toast.lib.profiler.Profiler;
import jaci.openrio.toast.lib.profiler.ProfilerEntity;
import jaci.openrio.toast.lib.profiler.ProfilerSection;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import java.io.*;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* The ScriptLoader is used for loading Files from the FileSystem into a ScriptEngine instance. This automatically manages
* retrieving of Files and evaluating them into the engine. USB Mass Storage rules are also in effect inside of the Script Loader.
*
* @author Jaci
*/
public class ScriptLoader {
/**
* Get the directory for the script root. This is based from toast/script/. This does NOT mkdir(), you must call
* that yourself if you intend to use the file.
*/
public static File getScriptDirByType(String type) {
return new File(ToastBootstrap.toastHome, "script/" + type);
}
/**
* Load all the scripts in the given directory into the provided ScriptEngine. Only files with the defined filenames
* will be accepted into the loader. This function will recursively search in each Sub-Directory of the parent directory
*/
public static List<String> loadAll(String homedir, String... filenames) throws FileNotFoundException {
List<String> list = new ArrayList<String>();
if (!USBMassStorage.overridingModules()) {
searchModules(new File(getScriptDirByType(homedir), "modules"));
}
for (MassStorageDevice device : USBMassStorage.connectedDevices) {
if (device.concurrent_modules || device.override_modules)
searchModules(new File(new File(device.toast_directory, "script/" + homedir), "modules"));
}
Profiler.INSTANCE.section("JavaScript").start("Load");
if (!USBMassStorage.overridingModules())
search(getScriptDirByType(homedir), list, filenames);
for (MassStorageDevice device : USBMassStorage.connectedDevices) {
if (device.concurrent_modules || device.override_modules)
search(new File(device.toast_directory, "script/" + homedir), list, filenames);
}
Profiler.INSTANCE.section("JavaScript").stop("Load");
return list;
}
/**
* Recursively search a directory (and its subdirectory) for candidate files and load them into the ScriptEngine.
*/
private static void search(File dir, List<String> list, String... filenames) throws FileNotFoundException {
dir.mkdirs();
File[] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (new File(dir, name).isDirectory()) return true;
for (String ext : filenames)
if (name.equalsIgnoreCase(ext)) return true;
return false;
}
});
if (files != null && files.length > 0) {
for (File file : files) {
if (!file.isDirectory()) {
try {
load(file, JavaScript.getEngine());
} catch (ScriptException e) {
Toast.log().error("Could not load Script: " + file);
Toast.log().exception(e);
}
}
}
}
}
/**
* Search for modules in the given directory. This will extract the modules into a subfolder
* named .extraction_cache and prepare the modules for mapping, as well as launch any init-scripts
* required.
*/
private static void searchModules(File dir) {
dir.mkdirs();
HashMap<String, File> map = new HashMap<>();
File extraction = new File(dir, ".extraction_cache");
if (extraction.exists())
extraction.delete();
extraction.mkdirs();
File[] modules = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jsm");
}
});
if (modules != null && modules.length > 0) {
for (File file : modules) {
try {
File extract = new File(extraction, file.getName().replace(".jsm", ""));
ProfilerEntity unz = new ProfilerEntity("unzip").start();
unzip(file, extract);
unz.stop();
ProfilerEntity mape = new ProfilerEntity("init").start();
String n = mapModule(extract, map);
mape.stop();
ProfilerSection section = Profiler.INSTANCE.section("JavaScript").section("Module").section(n);
section.pushEntity(unz);
section.pushEntity(mape);
} catch (Exception e) {
e.printStackTrace();
}
}
JavaScript.getEngine().put("__MODULES", map);
}
}
/**
* Map a module with the given (extracted) directory into the JavaScript engine.
*/
public static String mapModule(String directory) throws FileNotFoundException, JsonParserException {
ScriptEngine engine = JavaScript.getEngine();
if (engine.get("__MODULES") == null)
engine.put("__MODULES", new HashMap<String, File>());
return mapModule(new File(directory), (HashMap<String, File>) engine.get("__MODULES"));
}
/**
* Map a module with the given (extracted) directory into the JavaScript engine.
*/
private static String mapModule(File directory, HashMap<String, File> map) throws FileNotFoundException, JsonParserException {
File metadata = new File(directory, "module.json");
JsonObject obj = JsonParser.object().from(new FileReader(metadata));
map.put(obj.getString("name"), new File(directory, obj.getString("script")).getAbsoluteFile());
String name = obj.getString("name");
if (obj.has("initscript")) {
try {
load(new File(directory, obj.getString("initscript")), JavaScript.getEngine());
} catch (Exception e) {
Toast.log().info("Error in loading InitScript: " + e);
Toast.log().exception(e);
}
}
return name;
}
/**
* Unzip a .jsm module into the given target directory. Usually, this is inside of the .extraction_cache folder.
*/
private static void unzip(File source, File target) throws IOException {
ZipFile zf = new ZipFile(source);
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements()) {
ZipEntry ze = entries.nextElement();
if (ze.isDirectory()) {
new File(target, ze.getName()).mkdirs();
} else {
File out = new File(target, ze.getName());
out.getParentFile().mkdirs();
InputStream is = zf.getInputStream(ze);
FileOutputStream fos = new FileOutputStream(out);
byte[] bytes = new byte[1024];
int length;
while ((length = is.read(bytes)) >= 0) {
fos.write(bytes, 0, length);
}
is.close();
fos.close();
}
}
zf.close();
}
/**
* Load a single File and return the value it returns. This is used by the require keyword among others. This occurs across all USB devices
*/
public static Object loadSingle(String homedir, ScriptEngine engine, String file) throws FileNotFoundException, ScriptException {
final Object[] returnVal = {null};
Storage.USB_Module(new File("script/" + homedir, file).toString(), new Storage.StorageCallback() {
@Override
public void call(File file, boolean isUSB, MassStorageDevice device) {
if (file.exists()) {
try {
returnVal[0] = load(file, engine);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
return returnVal[0];
}
/**
* Load a module into the javascript engine
*/
public static Object loadModule(String homedir, ScriptEngine engine, String name) throws FileNotFoundException, ScriptException {
HashMap<String, File> mp = (HashMap<String, File>) engine.get("__MODULES");
Object ret = load(mp.get(name), engine);
return ret;
}
/**
* Load a relative file into the JavaScript engine. This will work across USB Mass Storage devices.
*/
public static Object loadRelative(String basedir, ScriptEngine engine, String loadtarget) {
String rel = USBMassStorage.relativize(new File(basedir));
final Object[] load = {null};
Storage.USB_Module(rel, new Storage.StorageCallback() {
@Override
public void call(File file, boolean isUSB, MassStorageDevice device) {
File loadPath = new File((file.isDirectory() ? file : file.getParentFile()), loadtarget);
if (loadPath.exists()) {
try {
load[0] = load(loadPath, engine);
} catch (FileNotFoundException | ScriptException e) { }
}
}
});
return load[0];
}
/**
* LOad a file relative here. This is relative, but will not work across USB Mass Storage devices. This should
* be used by Module-Makers for including files in their own modules.
*/
public static Object loadHere(String basedir, ScriptEngine engine, String loadtarget) throws FileNotFoundException, ScriptException {
File file = new File(basedir);
file = (file.isDirectory() ? file : file.getParentFile());
return load(new File(file, loadtarget), engine);
}
/**
* Load a file into the script engine. This uses the load() keyword in nashorn to ensure the __FILE__ and __DIR__ attributes are
* added correctly
*/
public static Object load(File file, ScriptEngine engine) throws FileNotFoundException, ScriptException {
try {
engine.put("__LOAD_TARGET", file.getAbsolutePath());
return engine.eval("load(__LOAD_TARGET)");
} finally {
engine.eval("delete __LOAD_TARGET;");
}
}
}