/* * Copyright 2012 Jason Miller * * Licensed 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 jj.script.module; import static jj.server.ServerLocation.*; import java.io.IOException; import javax.inject.Inject; import jj.resource.Location; import jj.resource.ResourceThread; import jj.resource.NoSuchResourceException; import jj.script.AbstractScriptEnvironment; import jj.script.ChildScriptEnvironment; import jj.script.PendingKey; import jj.script.RhinoContext; import jj.script.ScriptEnvironment; import org.mozilla.javascript.Script; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; /** * <p> * Provides the notion of a script module, which is the standard unit of execution * in the JibbrJabbr system. A module must belong to a parent script environment of * some sort, which provides the top-level execution semantics, but in principle, all * modules are interchangeable. * * <p> * Modules are identified by a tuple of identifier and parent, so the same script loaded * from two different parents will have independent scopes. If the same modules is loaded * multiple times within the context of a single parent, the same instance will be returned. * Modules cannot be parents of modules, this is a function of the * {@link RootScriptEnvironment} * * <p> * Modules can be either executable scripts, which can assign to the provided module.exports * or exports variables within their scopes, or they can be a single serialized JSON object, * which will be exported directly. * * <p> * Modules that are provided by the server are prepending with jj/ to identify them. This means if * you have a directory in your application named jj, you will have weird times getting to modules * in that directory, so don't do it. * * <p> * Otherwise, name resolution works like directory resolution, relative paths are resolved * against the current module identifier much like changing directories in a shell. paths that * start with / are resolved from the application root. it is possible to navigate "below" the * root with enough .. units in the module path, but that may not stick around so don't get * too excited about it. * * @author jason * */ public class ModuleScriptEnvironment extends AbstractScriptEnvironment<RequiredModule> implements ChildScriptEnvironment<RequiredModule> { public static final String API_PREFIX = "jj/"; private final RequiredModule requiredModule; // the key to restarting whatever included this. gets removed on first read and is null forever after // maybe not a good spot? it's not necessarily the same as the overall root environment // does not need to be volatile because this interaction is guaranteed to be from one thread private PendingKey pendingKey; private final ScriptableObject scope; private final Script script; private final String sha1; @Inject ModuleScriptEnvironment( final Dependencies dependencies, final RequiredModule requiredModule ) { super(dependencies); String moduleIdentifier = name(); Location base = requiredModule.parent().moduleLocation(); if (name().startsWith(API_PREFIX)) { base = APIModules; moduleIdentifier = name().substring(API_PREFIX.length()); } this.requiredModule = requiredModule; pendingKey = requiredModule.pendingKey(); assert requiredModule.parent().alive(): "cannot require a module for a dead parent"; // we look for a script and a JSON file, with script taking precedence ScriptResource scriptResource = resourceFinder.loadResource(ScriptResource.class, base, moduleIdentifier + ".js"); JSONResource jsonResource = resourceFinder.loadResource(JSONResource.class, base, moduleIdentifier + ".json"); if (scriptResource == null && jsonResource == null) { throw new NoSuchResourceException(getClass(), moduleIdentifier); } else if (scriptResource == null) { jsonResource.addDependent(requiredModule.parent()); sha1 = jsonResource.sha1(); scope = configureModuleObjects(moduleIdentifier, createChainedScope(requiredModule.parent().global())); script = null; try (RhinoContext context = contextProvider.get()) { Scriptable module = (Scriptable)context.evaluateString(scope, "module", ""); module.put("exports", module, jsonResource.contents()); } } else { scriptResource.addDependent(requiredModule.parent()); sha1 = scriptResource.sha1(); scope = configureTimers(configureModuleObjects(moduleIdentifier, createChainedScope(requiredModule.parent().global()))); script = scriptResource.script(); if (scriptResource.base() == APIModules) { configureInjectFunction(scope); } } // we need to reload each other on changes requiredModule.parent().addDependent(this); } @Override public Scriptable scope() { return scope; } @Override public Script script() { return script; } @Override public String sha1() { return sha1; } @Override public String scriptName() { return name() + ".js"; } @Override public ScriptEnvironment<?> parent() { return requiredModule.parent(); } @Override public PendingKey initializationContinuationPendingKey() { PendingKey result = pendingKey; pendingKey = null; return result; } @Override @ResourceThread public boolean needsReplacing() throws IOException { // we are obselete when our script is // but we don't listen as a dependent, our parent // does. we'll get reloaded anyway return false; } }