| /* |
| * 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; |
| } |
| } |