blob: 23c701589adcb07b0113549d9a0a704abadb4b0d [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.sling.scripting.esx;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeType;
import javax.script.Invocable;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import jdk.nashorn.api.scripting.JSObject;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.scripting.SlingScriptHelper;
import org.apache.sling.scripting.esx.plugins.ConsoleLog;
import org.apache.sling.scripting.esx.plugins.SimpleResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Module extends SimpleBindings implements Require {
public static final String CONTEXT_FIELD_SLING = "sling";
public static final String CONTEXT_FIELD_PARENT = "parent";
public static final String CONTEXT_FIELD_ID = "id";
public static final String CONTEXT_FIELD_MAIN = "main";
public static final String CONTEXT_FIELD_IS_LOADED = "isLoaded";
public static final String CONTEXT_FIELD_FILENAME = "filename";
public static final String CONTEXT_FIELD_RESOURCE = "resource";
public static final String CONTEXT_FIELD_CHILDREN = "children";
public static final String CONTEXT_FIELD_MODULE_RESOURCE = "moduleResource";
public static final String CONTEXT_FIELD_EXPORTS = "exports";
public static final String CONTEXT_FIELD_CONSOLE = "console";
public static final String LOADER_TEXT = "text!";
public static final String LOADER_RESOURCE = "resource!";
public static final String LOADER_JS = "js!";
private final Logger log = LoggerFactory.getLogger(getClass());
private SlingScriptHelper scriptHelper;
private final EsxScriptEngineFactory factory;
private ModuleScript moduleScript;
private boolean isLoaded = false;
private final List<Module> children = new ArrayList();
private String loader;
/**
*
* @param factory
* @param resource
* @param moduleScript
* @param id
* @param parent
* @param scriptHelper
* @throws ScriptException
*/
public Module(EsxScriptEngineFactory factory, Resource resource,
ModuleScript moduleScript, String id, Module parent,
SlingScriptHelper scriptHelper,
String loader) throws ScriptException {
this.factory = factory;
this.scriptHelper = scriptHelper;
this.moduleScript = moduleScript;
this.loader = loader;
put(CONTEXT_FIELD_PARENT, parent);
put(CONTEXT_FIELD_SLING, scriptHelper);
put(CONTEXT_FIELD_ID, id);
put(CONTEXT_FIELD_MAIN, (parent == null) ? this : (Module) parent.get(CONTEXT_FIELD_PARENT));
put(CONTEXT_FIELD_IS_LOADED, isLoaded);
put(CONTEXT_FIELD_FILENAME, moduleScript.getResource().getPath());
put(CONTEXT_FIELD_RESOURCE, resource);
put(CONTEXT_FIELD_CHILDREN, children);
put(CONTEXT_FIELD_MODULE_RESOURCE, moduleScript.getResource());
put(CONTEXT_FIELD_EXPORTS, ((JSObject) factory.getNashornEngine().eval("Object")).newObject());
log.debug("this is the main script: " + (get(CONTEXT_FIELD_MAIN) == this));
put(CONTEXT_FIELD_CONSOLE, new ConsoleLog((String) get(CONTEXT_FIELD_FILENAME)));
}
private ScriptObjectMirror decorateScript(String source)
throws ScriptException {
return decorateScript(source, true);
}
/**
*
* @param source
* @return
* @throws ScriptException
*/
private ScriptObjectMirror decorateScript(String source, boolean es6)
throws ScriptException {
String filename = (String) get("filename");
if (filename.indexOf("node_modules") == -1 && es6) {
try {
source = factory.getSandboxService().compileSource(source);
} catch (ScriptException e) {
log.error("Could not transpile script", e);
throw new ScriptException("could not load " + get("filename"));
}
}
// TODO: refactor polyfill for window, global and make require outside the wrapper as function parameter
source = "//@sourceURL=" + (String) get("filename") + "\n"
+ "(function (exports, Require, module, __filename,"
+ " __dirname, currentNode, console, properties, sling, simpleResource) { "
+ "var window = (this.window == 'undefined' || this.window == null) ? this : this.window;"
+ "var global = (global == 'undefined') ? this : global;"
+ "function require(id) { return Require.require(id); } require.resolve = function (id) { return Require.resolve(id, currentNode.resource, 1); };"
+ source
+ "})";
// use load + filenane for older JDK versions, @sourceURL is working for latest JDK version
source = "load( { name : \"" + get("filename") + "\","
+ " script: \""
+ StringEscapeUtils.escapeEcmaScript(source)
+ "\" } )";
ScriptObjectMirror function = null;
try {
function = (ScriptObjectMirror) factory.getNashornEngine().eval(
source
);
if (function == null) {
log.error("Function is null !");
}
} catch (ScriptException ex) {
// todo: better handling in future
throw ex;
}
return function;
}
/**
*
* @return @throws ScriptException
*/
public Object runScript() throws ScriptException {
log.debug("run script with id {}", get("id"));
ScriptObjectMirror function = factory.getModuleCache().get((String) get(CONTEXT_FIELD_FILENAME));
Resource moduleResource = (Resource) get(CONTEXT_FIELD_MODULE_RESOURCE);
Resource resource = (Resource) get(CONTEXT_FIELD_RESOURCE);
if (function == null) {
if (moduleScript.isJsFile()) {
function = Module.this.decorateScript(
//readScript(moduleResource)//
readScript(moduleResource)
);
}
if (moduleScript.isJsonFile()) {
String jsonfile = readScript(moduleResource);
function = decorateScript(
"module.exports = " + jsonfile
, false);
}
if (moduleScript.isResourceFile()) {
Iterator<Resource> resChildren = moduleResource.listChildren();
ArrayList<Resource> children = new ArrayList<Resource>();
resChildren.forEachRemaining(children::add);
put("children", children);
/*ValueMap map = moduleResource.adaptTo(ValueMap.class);
Invocable invocable = (Invocable) factory.getNashornEngine();
Object jsonprop = null;
try {
jsonprop = invocable.invokeMethod(factory.getNashornEngine().eval("JSON"), "stringify", map);
} catch (NoSuchMethodException ex) {
throw new ScriptException(ex);
}
*/
SimpleResource simpleResource = moduleResource.adaptTo(SimpleResource.class);
put("simpleResource", simpleResource);
/* String source = "exports.properties = " + jsonprop + ";"
+ "exports.path = currentNode.resource.path;"
+ "exports.simpleResource = this.simpleResource;"
+ "exports.children = this.children;";*/
String source = "module.exports = this.simpleResource;";
function = decorateScript(source, false);
}
if (!moduleScript.isResourceFile()) {
factory.getModuleCache().put(moduleScript.getResource().getPath(), function);
}
} else {
log.debug("module " + get(CONTEXT_FIELD_ID) + " received from cache");
}
if (moduleScript.isTextFile()) {
log.debug("is textfile loaidng file");
String source = StringEscapeUtils.escapeEcmaScript(readScript(moduleResource));
log.debug("sourcE: ");
source = "module.exports = \"" + source + "\";";
log.debug(source);
function = Module.this.decorateScript(source);
factory.getModuleCache().put(moduleScript.getResource().getPath(), function);
}
JSObject process = (JSObject) factory.getNashornEngine().eval("Object");
process.setMember("domain", log);
if (function != null) {
SimpleBindings currentNode = new SimpleBindings();
if (resource != null) {
currentNode.put("resource", resource);
currentNode.put("properties", resource.adaptTo(ValueMap.class));
} else {
log.debug("module id {} resource is null", get(CONTEXT_FIELD_ID));
}
// changed require to be generated within the wrapper
// TODO: need to be refactored
function.call(this, get(CONTEXT_FIELD_EXPORTS), this, this, get(CONTEXT_FIELD_FILENAME),
((Resource) get(CONTEXT_FIELD_MODULE_RESOURCE)).getParent().getPath(), currentNode,
(ConsoleLog) get(CONTEXT_FIELD_CONSOLE),
null,
(SlingScriptHelper) get(CONTEXT_FIELD_SLING),
resource.adaptTo(SimpleResource.class)
);
} else {
log.warn("function not called because it is null");
}
put(CONTEXT_FIELD_EXPORTS, get(CONTEXT_FIELD_EXPORTS));
return get(CONTEXT_FIELD_EXPORTS);
}
/**
*
* @param script
* @return
*/
public String readScript(Resource script) throws ScriptException {
InputStream is = script.getChild("jcr:content").adaptTo(InputStream.class);
BufferedReader esxScript = new BufferedReader(new InputStreamReader(is));
StringBuilder buffer = new StringBuilder();
String temp;
try {
while ((temp = esxScript.readLine()) != null) {
buffer.append(temp).append("\r\n");
}
return buffer.toString();
} catch (IOException ioex) {
throw new ScriptException(ioex);
}
}
/**
*
* @param path
* @return
*/
private boolean isLocalModule(String path) {
return (path.startsWith("./") == true
|| path.startsWith("/") == true
|| path.startsWith("../") == true);
}
/**
*
* @param path
* @param basePath
* @return
*/
private String normalizePath(String path, String basePath) {
path = StringUtils.removeStart(cleanModulePathFromLoaders(path), basePath);
return ResourceUtil.normalize(basePath + "/" + path);
}
/**
* not implemented yet
*
* @param path
* @return
*/
private boolean isGlobalModule(String path) {
return false;
}
/**
*
* @param file
* @param type
* @return
* @throws ScriptException
*/
private ModuleScript createModuleScript(Resource file, int type) throws ScriptException {
log.debug("module created. " + file.getPath());
Node currentNode = file.adaptTo(Node.class);
if (currentNode != null) {
log.debug("currentNode !) null = " + (currentNode != null));
try {
boolean isFile = currentNode.isNodeType(NodeType.NT_FILE);
log.debug("isFile: " + isFile);
if (isFile) {
return new ModuleScript(type, file);
}
log.debug("not a file " + currentNode.getMixinNodeTypes().toString());
} catch (RepositoryException ex) {
throw new ScriptException("cannot load file " + file.getPath());
}
}
return null;
}
/**
*
* @param module
* @param path
* @param currentResource
* @return
* @throws ScriptException
*/
public ModuleScript loadAsFile(String module, String path,
Resource currentResource, String loader) throws ScriptException {
int type = ModuleScript.JS_FILE;
// this is need to be refactored, it is this way because I followed the
// node.js extension handling at first but switched over to requirejs
// loader notation
Resource file = currentResource.getResourceResolver().getResource(path);
// require.extensions is deprecated, however to implement this might
// be a good way to handle .resource loading or to implemend loader
// like in requirejs e.g. similar to https://github.com/requirejs/text
// "text!some/module.html"
// or require("resource!/content/homepage/jcr:content")
if (LOADER_RESOURCE.equals(loader) && file != null) {
return new ModuleScript(ModuleScript.RESOURCE_FILE, file);
}
if (LOADER_TEXT.equals(loader) && file != null) {
return new ModuleScript(ModuleScript.TEXT_FILE, file);
}
//special handling for json file require
if (path.endsWith(".json") && file != null) {
return new ModuleScript(ModuleScript.JSON_FILE, file);
}
if (path.endsWith(".bin") && file != null) {
log.warn(".bin loder are currently not supported (file requested: " + path);
}
try {
if (file == null || !file.adaptTo(Node.class).isNodeType(NodeType.NT_FILE)) {
file = currentResource.getResourceResolver().getResource(path + ".js");
if (file == null) {
file = currentResource.getResourceResolver().getResource(path + ".json");
if (file == null) {
return null;
}
type = ModuleScript.JSON_FILE;
} else {
type = ModuleScript.JS_FILE;
}
}
} catch (RepositoryException ex) {
log.error(module + "", ex);
}
return createModuleScript(file, type);
}
private String cleanModulePathFromLoaders(String path) {
if (path.startsWith("resource!")) {
return path.substring("resource!".length(), path.length());
}
if (path.startsWith("text!")) {
return path.substring("text!".length(), path.length());
}
return path;
}
/**
*
* @param module
* @param path
* @param currentResource
* @return
* @throws ScriptException
*/
public ModuleScript loadAsDirectory(String module, String path, Resource currentResource, String loader) throws ScriptException {
ResourceResolver resolver = currentResource.getResourceResolver();
Resource packageJson = resolver.getResource(path + "/package.json");
if (packageJson != null) {
Node jsonFile = packageJson.adaptTo(Node.class);
try {
boolean isFile = (jsonFile.isNodeType(NodeType.NT_FILE) || jsonFile.isNodeType(NodeType.NT_RESOURCE));
if (isFile) {
InputStream is = packageJson.getChild("jcr:content").adaptTo(InputStream.class);
try {
String jsonData = IOUtils.toString(is);
Invocable invocable = (Invocable) factory.getNashornEngine();
JSObject jsonprop = null;
try {
jsonprop = (JSObject) invocable.invokeMethod(factory.getNashornEngine().eval("JSON"), "parse", jsonData);
} catch (NoSuchMethodException ex) {
throw new ScriptException(ex);
}
Object main = jsonprop.getMember("main");
if (main != null) {
String packageModule = (String) main;
String mainpath = normalizePath(packageModule,
path);
return loadAsFile(packageModule, mainpath, currentResource, loader);
}
} catch (IOException ex) {
throw new ScriptException(ex);
}
}
} catch (RepositoryException ex) {
throw new ScriptException(ex);
}
}
Resource indexjs = resolver.getResource(path + "/index.js");
if (indexjs != null) {
return createModuleScript(indexjs, ModuleScript.JS_FILE);
}
Resource indexjson = resolver.getResource(path + "/index.json");
if (indexjson != null) {
return createModuleScript(indexjson, ModuleScript.JSON_FILE);
}
Resource indexnode = resolver.getResource(path + "/index.node");
if (indexnode != null) {
throw new ScriptException("Node module .node (binary) loading is currently not supported");
}
return null;
}
/**
*
* @param module
* @param currentResource
* @return
* @throws ScriptException
*/
public ModuleScript loadAsModule(String module, Resource currentResource, String loader) throws ScriptException {
return loadAsModule(module, currentResource, true, loader);
}
/**
*
* @param paths
* @return
*/
private String[] loadModulePaths(String paths) {
String[] parts = paths.split("/");
List<String> dirs = new ArrayList<String>();
for (int i = (parts.length - 1); i > 0;) {
log.debug(parts[i]);
if ("node_modules".equals(parts[i]) || "esx_modules".equals(parts[i])) {
continue;
}
String part = StringUtils.join(parts, "/", 0, i);
String dir = part + "/node_modules";
log.debug("load dir: " + dir);
dirs.add(part + "/esx_modules");
dirs.add(dir);
i = i - 1;
}
// if the regular module resoultion is not finding anything, try and check the
// global paths. needs to be optimized.
dirs.add("/apps/esx/esx_modules");
dirs.add("/apps/esx/node_modules");
dirs.add("/libs/esx/esx_modules");
dirs.add("/libs/esx/node_modules");
return dirs.stream().toArray(String[]::new);
}
/**
*
* @param module
* @param currentResource
* @param isFileResource
* @return
* @throws ScriptException
*/
public ModuleScript loadAsModule(String module, Resource currentResource, boolean isFileResource, String loader) throws ScriptException {
ModuleScript script = null;
String[] dirs = loadModulePaths(currentResource.getPath());
log.debug("loading modules from dir path");
for (String dir : dirs) {
log.debug("trying to resolve. " + dir);
Resource searchPath = currentResource.getResourceResolver().resolve(dir);
log.debug("searchpath = " + searchPath.getPath());
if (searchPath != null && !ResourceUtil.isNonExistingResource(searchPath)) {
log.debug("searchpath loadasmodule: " + searchPath.getPath());
script = loadLocalModule(module, searchPath, false, loader);
if (script != null) {
return script;
}
} else {
log.debug("dir is null = " + dir);
}
}
log.debug("finsihed loading dirs, script is loaded?!");
return script;
}
/**
*
* @param module
* @param currentResource
* @return
* @throws ScriptException
*/
public ModuleScript loadLocalModule(String module, Resource currentResource, String loader) throws ScriptException {
return loadLocalModule(module, currentResource, false, loader);
}
/**
*
* @param resource
* @return
*/
private boolean resourceIsfile(Resource resource) {
try {
return resource.adaptTo(Node.class).isNodeType(NodeType.NT_FILE);
} catch (RepositoryException ex) {
log.error("resourceIsfile", ex);
}
return false;
}
/**
*
* @param module
* @param currentResource
* @param isFile
* @return
* @throws ScriptException
*/
public ModuleScript loadLocalModule(String module, Resource currentResource, boolean isFile, String loader) throws ScriptException {
String basePath = (resourceIsfile(currentResource))
? currentResource.getParent().getPath() : currentResource.getPath();
String path = normalizePath(module, basePath);
if (module.startsWith("/")) {
path = module;
}
ModuleScript script = loadAsFile(module, path, currentResource, loader);
if (script != null) {
return script;
}
return loadAsDirectory(module, path, currentResource, loader); // load as directory
}
/**
* p´
*
* @param module
* @param currentResource
* @return
* @throws javax.script.ScriptException
*/
public ModuleScript resolve(String module, Resource currentResource, String loader) throws ScriptException {
// if x is core module / library return directly the one
log.debug("resolving module: " + module);
ModuleScript script;
if (isGlobalModule(module)) {
// ignore for now
}
if (isLocalModule(module)) {
script = loadLocalModule(module, currentResource, loader);
if (script != null) {
return script;
}
}
// load as module (first split path, then load)
script = loadAsModule(module, currentResource, loader);
if (script != null) {
return script;
}
throw new ScriptException("module not found " + module);
}
@Override
public Object require(String id) throws ScriptException, IOException {
log.debug("Trying to require Module with Id: " + id);
// different behavior if we are the main module, require directly
// run runScript directly on this module
if (get("id").equals(id)
&& get("main") == this) {
return runScript();
}
String loader = LOADER_JS;
if (id.startsWith(LOADER_TEXT)) {
loader = LOADER_TEXT;
}
if (id.startsWith(LOADER_RESOURCE)) {
loader = LOADER_RESOURCE;
}
ModuleScript subModuleScript = resolve(cleanModulePathFromLoaders(id), (Resource) get(CONTEXT_FIELD_MODULE_RESOURCE), loader);
Module subModule = new Module(factory, (Resource) get(CONTEXT_FIELD_RESOURCE),
subModuleScript, id, this, (SlingScriptHelper) get(CONTEXT_FIELD_SLING), loader);
Object result = subModule.runScript();
children.add(subModule);
subModule.put(CONTEXT_FIELD_IS_LOADED, true);
return result;
}
}