Example #1
0
 /**
  * Invoke a script from the command line.
  *
  * @param scriptResource the script resource of path
  * @param scriptArgs an array of command line arguments
  * @return the return value
  * @throws IOException an I/O related error occurred
  * @throws JavaScriptException the script threw an error during compilation or execution
  */
 public Object runScript(Object scriptResource, String... scriptArgs)
     throws IOException, JavaScriptException {
   Resource resource;
   if (scriptResource instanceof Resource) {
     resource = (Resource) scriptResource;
   } else if (scriptResource instanceof String) {
     resource = findResource((String) scriptResource, null, null);
   } else {
     throw new IOException("Unsupported script resource: " + scriptResource);
   }
   if (!resource.exists()) {
     throw new FileNotFoundException(scriptResource.toString());
   }
   Context cx = contextFactory.enterContext();
   try {
     Object retval;
     Map<Trackable, ReloadableScript> scripts = getScriptCache(cx);
     commandLineArgs = Arrays.asList(scriptArgs);
     ReloadableScript script = new ReloadableScript(resource, this);
     scripts.put(resource, script);
     mainScope = new ModuleScope(resource.getModuleName(), resource, globalScope, mainWorker);
     retval = mainWorker.evaluateScript(cx, script, mainScope);
     mainScope.updateExports();
     return retval instanceof Wrapper ? ((Wrapper) retval).unwrap() : retval;
   } finally {
     Context.exit();
   }
 }
Example #2
0
/**
 * This class provides methods to create JavaScript objects from JavaScript files.
 *
 * @author Hannes Wallnoefer <*****@*****.**>
 */
public class RhinoEngine implements ScopeProvider {

  private RingoConfig config;
  private List<Repository> repositories;
  private RingoGlobal globalScope;
  private List<String> commandLineArgs;
  private Map<Trackable, ReloadableScript> compiledScripts, interpretedScripts;
  private final Map<Singleton, Singleton> singletons;
  private AppClassLoader loader = new AppClassLoader();
  private WrapFactory wrapFactory;
  private Set<Class> hostClasses;
  private ModuleLoader[] loaders;
  private List<Callback> shutdownHooks;

  private RingoContextFactory contextFactory = null;
  private ModuleScope mainScope = null;

  private final RingoWorker mainWorker;
  private final Deque<RingoWorker> workers;
  private final ThreadLocal<RingoWorker> currentWorker;
  private final AsyncTaskCounter asyncCounter = new AsyncTaskCounter();

  private static Logger log = Logger.getLogger(RhinoEngine.class.getName());

  public static final List<Integer> VERSION = Collections.unmodifiableList(Arrays.asList(0, 9));

  /**
   * Create a RhinoEngine with the given configuration. If <code>globals</code> is not null, its
   * contents are added as properties on the global object.
   *
   * @param config the configuration used to initialize the engine.
   * @param globals an optional map of global properties
   * @throws Exception if the engine can't be created
   */
  public RhinoEngine(RingoConfig config, Map<String, Object> globals) throws Exception {
    this.config = config;
    workers = new LinkedBlockingDeque<RingoWorker>();
    currentWorker = new ThreadLocal<RingoWorker>();
    mainWorker = new RingoWorker(this);
    compiledScripts = new ConcurrentHashMap<Trackable, ReloadableScript>();
    interpretedScripts = new ConcurrentHashMap<Trackable, ReloadableScript>();
    singletons = new HashMap<Singleton, Singleton>();
    contextFactory = new RingoContextFactory(this, config);
    repositories = config.getRepositories();
    wrapFactory = config.getWrapFactory();

    loaders =
        new ModuleLoader[] {new JsModuleLoader(), new JsonModuleLoader(), new ClassModuleLoader()};

    RingoDebugger debugger = null;
    if (config.getDebug()) {
      debugger = new RingoDebugger(config);
      debugger.setScopeProvider(this);
      debugger.attachTo(contextFactory);
      debugger.setBreakOnExceptions(true);
    }

    // create and initialize global scope
    Context cx = contextFactory.enterContext();
    try {
      boolean sealed = config.isSealed();
      globalScope = new RingoGlobal(cx, this, sealed);
      Class<Scriptable>[] classes = config.getHostClasses();
      if (classes != null) {
        for (Class<Scriptable> clazz : classes) {
          defineHostClass(clazz);
        }
      }
      ScriptableList.init(globalScope);
      ScriptableMap.init(globalScope);
      ScriptableObject.defineClass(globalScope, ScriptableWrapper.class);
      ScriptableObject.defineClass(globalScope, ModuleObject.class);
      if (globals != null) {
        for (Map.Entry<String, Object> entry : globals.entrySet()) {
          ScriptableObject.defineProperty(
              globalScope, entry.getKey(), entry.getValue(), ScriptableObject.DONTENUM);
        }
      }
      mainWorker.evaluateScript(cx, getScript("globals"), globalScope);
      evaluateBootstrapScripts(cx);
      if (sealed) {
        globalScope.sealObject();
      }
      if (debugger != null) {
        debugger.setBreak();
      }
      try {
        Runtime.getRuntime()
            .addShutdownHook(
                new Thread() {
                  public void run() {
                    shutdown();
                  }
                });
      } catch (java.security.AccessControlException e) {
        log.log(Level.WARNING, "Could not register shutdown hook due to security exception", e);
      }
    } finally {
      Context.exit();
    }
  }

  /**
   * Define a Javascript host object implemented by the given class.
   *
   * @param clazz The Java class implementing the host object.
   * @exception IllegalAccessException if access is not available to a reflected class member
   * @exception InstantiationException if unable to instantiate the named class
   * @exception InvocationTargetException if an exception is thrown during execution of methods of
   *     the named class
   */
  public void defineHostClass(Class<Scriptable> clazz)
      throws InvocationTargetException, InstantiationException, IllegalAccessException {
    if (hostClasses != null && hostClasses.contains(clazz)) {
      return;
    }
    synchronized (this) {
      if (hostClasses == null) {
        hostClasses = new HashSet<Class>();
      }
      hostClasses.add(clazz);
      ScriptableObject.defineClass(globalScope, clazz);
    }
  }

  /**
   * Invoke a script from the command line.
   *
   * @param scriptResource the script resource of path
   * @param scriptArgs an array of command line arguments
   * @return the return value
   * @throws IOException an I/O related error occurred
   * @throws JavaScriptException the script threw an error during compilation or execution
   */
  public Object runScript(Object scriptResource, String... scriptArgs)
      throws IOException, JavaScriptException {
    Resource resource;
    if (scriptResource instanceof Resource) {
      resource = (Resource) scriptResource;
    } else if (scriptResource instanceof String) {
      resource = findResource((String) scriptResource, null, null);
    } else {
      throw new IOException("Unsupported script resource: " + scriptResource);
    }
    if (!resource.exists()) {
      throw new FileNotFoundException(scriptResource.toString());
    }
    Context cx = contextFactory.enterContext();
    try {
      Object retval;
      Map<Trackable, ReloadableScript> scripts = getScriptCache(cx);
      commandLineArgs = Arrays.asList(scriptArgs);
      ReloadableScript script = new ReloadableScript(resource, this);
      scripts.put(resource, script);
      mainScope = new ModuleScope(resource.getModuleName(), resource, globalScope, mainWorker);
      retval = mainWorker.evaluateScript(cx, script, mainScope);
      mainScope.updateExports();
      return retval instanceof Wrapper ? ((Wrapper) retval).unwrap() : retval;
    } finally {
      Context.exit();
    }
  }

  /**
   * Evaluate an expression from the command line.
   *
   * @param expr the JavaScript expression to evaluate
   * @return the return value
   * @throws IOException an I/O related error occurred
   * @throws JavaScriptException the script threw an error during compilation or execution
   */
  public Object evaluateExpression(String expr) throws IOException, JavaScriptException {
    Context cx = contextFactory.enterContext();
    cx.setOptimizationLevel(-1);
    try {
      Object retval;
      Repository repository = repositories.get(0);
      Scriptable parentScope = mainScope != null ? mainScope : globalScope;
      ModuleScope scope = new ModuleScope("<expr>", repository, parentScope, mainWorker);
      Resource res = new StringResource("<expr>", expr, 1);
      ReloadableScript script = new ReloadableScript(res, this);
      retval = mainWorker.evaluateScript(cx, script, scope);
      return retval instanceof Wrapper ? ((Wrapper) retval).unwrap() : retval;
    } finally {
      Context.exit();
    }
  }

  /**
   * Invoke a javascript function. This enters a JavaScript context, creates a new per-thread scope,
   * calls the function, exits the context and returns the return value of the invocation.
   *
   * @param module the module name or object, or null for the main module
   * @param method the method name to call in the script
   * @param args the arguments to pass to the method
   * @return the return value of the invocation
   * @throws NoSuchMethodException the method is not defined
   * @throws IOException an I/O related error occurred
   */
  public Object invoke(Object module, String method, Object... args)
      throws IOException, NoSuchMethodException, ExecutionException, InterruptedException {
    return mainWorker.invoke(module, method, args);
  }

  /**
   * Associate a worker with the current worker and return the worker that was previously associated
   * with it, or null.
   *
   * @param worker the new worker associated with the current thread
   * @return the worker previously associated with the current thread, or null
   */
  protected RingoWorker setCurrentWorker(RingoWorker worker) {
    RingoWorker previousWorker = currentWorker.get();
    currentWorker.set(worker);
    return previousWorker;
  }

  /**
   * Get the worker associated with the given scope, or null.
   *
   * @return the worker associated with the current thread, or null.
   */
  public RingoWorker getCurrentWorker() {
    return currentWorker.get();
  }

  /**
   * Get the main worker running the main script.
   *
   * @return the main worker
   */
  public RingoWorker getMainWorker() {
    return mainWorker;
  }

  /**
   * Get a new {@link RingoWorker}.
   *
   * @return a worker instance.
   */
  public RingoWorker getWorker() {
    RingoWorker worker = workers.pollFirst();
    if (worker == null) {
      worker = new RingoWorker(this);
    }
    return worker;
  }

  /**
   * Return a worker, returning it to the worker pool.
   *
   * @param worker the worker to be released
   */
  void returnWorker(RingoWorker worker) {
    if (!workers.offerFirst(worker)) {
      worker.shutdown();
    }
  }

  synchronized void shutdown() {
    List<Callback> hooks = shutdownHooks;
    if (hooks != null) {
      for (Callback callback : hooks) {
        try {
          Object result = callback.invoke();
          if (!callback.sync && result instanceof Future) {
            ((Future) result).get();
          }
        } catch (Exception x) {
          log.log(Level.WARNING, "Error in shutdown hook", x);
        }
      }
      shutdownHooks = null;
    }
  }

  /**
   * Add a callback to be invoked on shutdown.
   *
   * @param callback a callback function wrapper
   * @param sync whether to invoke the callback synchronously (on the main shutdown thread) or
   *     asynchronously (on the worker's event loop thread)
   */
  public synchronized void addShutdownHook(Scriptable callback, boolean sync) {
    List<Callback> hooks = shutdownHooks;
    if (hooks == null) {
      hooks = shutdownHooks = new ArrayList<Callback>();
    }
    hooks.add(new Callback(callback, this, sync));
  }

  /**
   * Get the list of errors encountered by the main worker.
   *
   * @return a list of errors, may be null.
   */
  public List<ScriptError> getMainErrors() {
    return mainWorker.getErrors();
  }

  /**
   * Return a shell scope for interactive evaluation
   *
   * @return a shell scope
   * @throws IOException an I/O related exception occurred
   */
  public Scriptable getShellScope(RingoWorker worker) throws IOException {
    Repository repository = new FileRepository("");
    repository.setAbsolute(true);
    Scriptable protoScope = mainScope != null ? mainScope : globalScope;
    return new ModuleScope("<shell>", repository, protoScope, worker);
  }

  /**
   * Get the engine's global shared scope
   *
   * @return the global scope
   */
  public Scriptable getScope() {
    return globalScope;
  }

  /**
   * Initialize and normalize the global variables and arguments on a thread scope.
   *
   * @param args the arguments
   */
  protected void initArguments(Object[] args) {
    if (args != null) {
      for (int i = 0; i < args.length; i++) {
        args[i] = wrapArgument(args[i], globalScope);
      }
    }
  }

  /**
   * Prepare a single property or argument value for use within rhino.
   *
   * @param value the property or argument value
   * @param scope the scope
   * @return the object wrapped and wired for rhino
   */
  public static Object wrapArgument(Object value, Scriptable scope) {
    if (value instanceof ScriptableObject) {
      ScriptableObject scriptable = ((ScriptableObject) value);
      // Avoid overwriting prototype and parent scope as this would break closures
      if (scriptable.getPrototype() == null) {
        scriptable.setPrototype(
            ScriptableObject.getClassPrototype(scope, scriptable.getClassName()));
      }
      if (scriptable.getParentScope() == null) {
        scriptable.setParentScope(scope);
      }
      return scriptable;
    } else {
      return Context.javaToJS(value, scope);
    }
  }

  /**
   * Get the current Rhino optimization level
   *
   * @return the current optimization level
   */
  public int getOptimizationLevel() {
    Context cx = Context.getCurrentContext();
    if (cx != null) {
      return cx.getOptimizationLevel();
    }
    return 0;
  }

  /**
   * Set Rhino optimization level
   *
   * @param level the new optimization level
   */
  public void setOptimizationLevel(int level) {
    Context cx = Context.getCurrentContext();
    if (cx != null && cx.getOptimizationLevel() != level) {
      cx.setOptimizationLevel(level);
    }
  }

  /**
   * Resolves a type name to a script file within our script directory and returns a Scriptable
   * evaluated to the file.
   *
   * @param moduleName the name of the module to load
   * @return The raw compiled script for the module
   * @throws JavaScriptException if an error occurred evaluating the script file
   * @throws IOException if an error occurred reading the script file
   */
  public ReloadableScript getScript(String moduleName) throws JavaScriptException, IOException {
    return getScript(moduleName, null);
  }

  /**
   * Resolves a type name to a script file within our script directory and returns a Scriptable
   * evaluated to the file.
   *
   * @param moduleName the name of the module to load
   * @param localPath the path of the resource issuing this call
   * @return The raw compiled script for the module
   * @throws JavaScriptException if an error occurred evaluating the script file
   * @throws IOException if an error occurred reading the script file
   */
  public ReloadableScript getScript(String moduleName, Repository localPath)
      throws JavaScriptException, IOException {
    ReloadableScript script;
    Resource source = findResource(moduleName, loaders, localPath);
    if (!source.exists()) {
      source = loadPackage(moduleName, localPath);
      if (!source.exists()) {
        source = findResource(moduleName, null, localPath);
      }
    }
    Context cx = Context.getCurrentContext();
    Map<Trackable, ReloadableScript> scripts = getScriptCache(cx);
    if (scripts.containsKey(source)) {
      script = scripts.get(source);
    } else {
      script = new ReloadableScript(source, this);
      if (source.exists()) {
        scripts.put(source, script);
      }
    }
    return script;
  }

  /**
   * Resolves a module id to a package resource. If module id consists of just one term and resolves
   * to a package directory, the main module of the package is returned. If the module id consists
   * of several terms and the first term resolves to a package directory, the remaining part of the
   * module id is resolved against the "lib" directory of the package.
   *
   * @link http://nodejs.org/docs/v0.4.4/api/modules.html#folders_as_Modules
   * @param moduleName the name of the package to load
   * @param localPath the path of the resource issuing this call
   * @return the location of the package's main module
   * @throws IOException an unrecoverable I/O exception occurred while reading the package
   */
  protected Resource loadPackage(String moduleName, Repository localPath) throws IOException {

    int slash = 0;
    String packageName, remainingName;

    do {
      slash = moduleName.indexOf('/', slash + 1);
      if (slash == -1) {
        packageName = moduleName;
        remainingName = null;
      } else {
        packageName = moduleName.substring(0, slash);
        if (".".equals(packageName) || "..".equals(packageName)) continue;
        remainingName = moduleName.substring(slash + 1);
      }

      Resource json = findResource(packageName + "/package.json", null, localPath);

      if (json != null && json.exists()) {

        Scriptable obj = parseJsonResource(json);
        Repository parent = json.getParentRepository();
        String moduleId;
        Resource res;

        if (remainingName == null) {
          // get the main module of this package
          moduleId = getStringProperty(obj, "main", null);
          if (moduleId != null) {
            // optimize for the common case where main module
            // property links to the exact file name
            res = parent.getResource(moduleId);
            if (res != null && res.exists()) return res;
          }
        } else {
          // map remaining name to libs directory
          String lib = "lib";
          Object dirs = ScriptableObject.getProperty(obj, "directories");
          if (dirs instanceof Scriptable) {
            lib = getStringProperty((Scriptable) dirs, "lib", "lib");
          }
          moduleId = lib + "/" + remainingName;
        }

        if (moduleId != null) {
          for (ModuleLoader loader : loaders) {
            res = parent.getResource(moduleId + loader.getExtension());
            if (res != null && res.exists()) return res;
          }
          if (remainingName != null) {
            res = parent.getResource(moduleId);
            if (res != null && res.exists()) return res;
          }
        }
      }

    } while (slash != -1);

    return findResource(moduleName + "/index", loaders, localPath);
  }

  private Scriptable parseJsonResource(Resource resource) throws IOException {
    JsonParser parser = new JsonParser(Context.getCurrentContext(), globalScope);
    try {
      Object result = parser.parseValue(resource.getContent());
      if (!(result instanceof Scriptable)) {
        throw new RuntimeException("Expected Object from package.json, got " + result);
      }
      return (Scriptable) result;
    } catch (JsonParser.ParseException px) {
      throw new RuntimeException(px);
    }
  }

  private String getStringProperty(Scriptable obj, String name, String defaultValue) {
    Object value = ScriptableObject.getProperty(obj, name);
    if (value != null && value != ScriptableObject.NOT_FOUND) {
      return ScriptRuntime.toString(value);
    }
    return defaultValue;
  }

  /**
   * Load a Javascript module into a module scope. This checks if the module has already been loaded
   * in the current context and if so returns the existing module scope.
   *
   * @param cx the current context
   * @param moduleName the module name
   * @param loadingScope the scope requesting the module
   * @return the loaded module's scope
   * @throws IOException indicates that in input/output related error occurred
   */
  public Scriptable loadModule(Context cx, String moduleName, Scriptable loadingScope)
      throws IOException {
    return mainWorker.loadModule(cx, moduleName, loadingScope);
  }

  /**
   * Get the name of the main script as module name, if any
   *
   * @return the main module name, or null
   */
  public String getMainModule() {
    return config.getMainModule();
  }

  /**
   * Get the main scrip's module scope, if any
   *
   * @return the main module scope, or null
   */
  public ModuleScope getMainModuleScope() {
    return mainScope;
  }

  /**
   * Get the script arguments as object array suitable for use with Context.newArray().
   *
   * @return the script arguments
   */
  public Object[] getArguments() {
    String[] args = config.getArguments();
    if (args == null) {
      return ScriptRuntime.emptyArgs;
    } else {
      Object[] array = new Object[args.length];
      System.arraycopy(args, 0, array, 0, args.length);
      return array;
    }
  }

  public String getCharset() {
    return config.getCharset();
  }

  /**
   * Get the currently active RhinoEngine instance.
   *
   * @param scope the global scope or a top level module scope
   * @return the current RhinoEngine
   */
  public static RhinoEngine getEngine(Scriptable scope) {
    if (scope instanceof ModuleScope) {
      scope = scope.getPrototype();
    }
    if (scope instanceof RingoGlobal) {
      return ((RingoGlobal) scope).getEngine();
    }
    throw new IllegalArgumentException("Unsupported scope");
  }

  /**
   * Create a sandboxed scripting engine with the same install directory as this and the given
   * module paths, global properties, class shutter and sealing
   *
   * @param config the sandbox configuration
   * @param globals a map of predefined global properties, may be null
   * @return a sandboxed RhinoEngine instance
   * @throws FileNotFoundException if any part of the module paths does not exist
   */
  public RhinoEngine createSandbox(RingoConfig config, Map<String, Object> globals)
      throws Exception {
    config.setPolicyEnabled(this.config.isPolicyEnabled());
    return new RhinoEngine(config, globals);
  }

  protected boolean isPolicyEnabled() {
    // only use security when ringo runs standalone with default security manager,
    // not with google app engine
    return config.isPolicyEnabled();
  }

  /**
   * Wait until all daemon threads running in this engine have terminated.
   *
   * @throws InterruptedException if the current thread has been interrupted
   */
  public void waitForAsyncTasks() throws InterruptedException {
    asyncCounter.waitTillDone();
  }

  protected void enterAsyncTask() {
    asyncCounter.increase();
  }

  protected void exitAsyncTask() {
    asyncCounter.decrease();
  }

  private Map<Trackable, ReloadableScript> getScriptCache(Context cx) {
    return cx.getOptimizationLevel() == -1 ? interpretedScripts : compiledScripts;
  }

  private void evaluateBootstrapScripts(Context cx) throws IOException {
    List<String> bootstrapScripts = config.getBootstrapScripts();
    if (bootstrapScripts != null) {
      for (String script : bootstrapScripts) {
        Resource resource = new FileResource(script);
        // not found, attempt to resolve the file relative to ringo home
        if (!resource.exists()) {
          resource = getRingoHome().getResource(script);
        }
        if (resource == null || !resource.exists()) {
          throw new FileNotFoundException("Bootstrap script " + script + " not found");
        }
        mainWorker.evaluateScript(cx, new ReloadableScript(resource, this), globalScope);
      }
    }
  }

  /**
   * Get the list of command line arguments
   *
   * @return the command line arguments passed to this engine
   */
  public List<String> getCommandLineArguments() {
    if (commandLineArgs == null) {
      commandLineArgs = Collections.emptyList();
    }
    return Collections.unmodifiableList(commandLineArgs);
  }

  /**
   * Get the engine's module search path as a list of repositories
   *
   * @return the module repositories
   */
  public List<Repository> getRepositories() {
    return repositories;
  }

  /**
   * Get the our installation directory.
   *
   * @return the RingoJS installation directory
   */
  public Repository getRingoHome() {
    return config.getRingoHome();
  }

  /**
   * Get the repository associated with the scope or one of its prototypes
   *
   * @param scope the scope to get the repository from
   * @return the repository, or null
   */
  public Repository getParentRepository(Scriptable scope) {
    while (scope != null) {
      if (scope instanceof ModuleScope) {
        return ((ModuleScope) scope).getRepository();
      }
      scope = scope.getPrototype();
    }
    return null;
  }

  /**
   * Get a list of all child resources for the given path relative to our script repository.
   *
   * @param path the repository path
   * @param recursive whether to include nested resources
   * @return a list of all contained child resources
   */
  public List<Resource> findResources(String path, boolean recursive) throws IOException {
    return config.getResources(path, recursive);
  }

  /**
   * Try to resolve path to a resource or repository relative to a local path, or the engine's
   * repository path.
   *
   * @param path the resource name
   * @param localRoot a repository to look first
   * @return the resource or repository
   * @throws IOException if an I/O error occurred
   */
  public Trackable resolve(String path, Repository localRoot) throws IOException {
    Trackable t = findResource(path, null, localRoot);
    if (t == null || !t.exists()) {
      t = findRepository(path, localRoot);
    }
    return t;
  }

  /**
   * Search for a resource in a local path, or the main repository path.
   *
   * @param path the resource name
   * @param loaders optional list of module loaders
   * @param localRoot a repository to look first
   * @return the resource
   * @throws IOException if an I/O error occurred
   */
  public Resource findResource(String path, ModuleLoader[] loaders, Repository localRoot)
      throws IOException {
    // Note: as an extension to the CommonJS modules API
    // we allow absolute module paths for resources
    File file = new File(path);
    if (file.isAbsolute()) {
      Resource res;
      outer:
      if (loaders != null) {
        // loaders must contain at least one loader
        assert loaders.length > 0 && loaders[0] != null;
        for (ModuleLoader loader : loaders) {
          res = new FileResource(path + loader.getExtension());
          if (res.exists()) {
            break outer;
          }
        }
        res = new FileResource(path + loaders[0].getExtension());
      } else {
        res = new FileResource(file);
      }
      res.setAbsolute(true);
      return res;
    } else if (localRoot != null && (path.startsWith("./") || path.startsWith("../"))) {
      String newpath = localRoot.getRelativePath() + path;
      return findResource(newpath, loaders, null);
    } else {
      return config.getResource(normalizePath(path), loaders);
    }
  }

  /**
   * Search for a repository in the local path, or the main repository path.
   *
   * @param path the repository name
   * @param localPath a repository to look first
   * @return the repository
   * @throws IOException if an I/O error occurred
   */
  public Repository findRepository(String path, Repository localPath) throws IOException {
    // To be consistent, always return absolute repository if path is absolute
    // if we make this dependent on whether files exist we introduce a lot of
    // vague and undetermined behaviour.
    File file = new File(path);
    if (file.isAbsolute()) {
      return new FileRepository(file);
    }
    if (localPath != null) {
      Repository repository = localPath.getChildRepository(path);
      if (repository != null && repository.exists()) {
        return repository;
      }
    }
    return config.getRepository(normalizePath(path));
  }

  public ModuleLoader getModuleLoader(Resource resource) {
    String name = resource.getName();
    for (ModuleLoader loader : loaders) {
      if (name.endsWith(loader.getExtension())) {
        return loader;
      }
    }
    return loaders[0];
  }

  public synchronized void addModuleLoader(String extension, Object value) {
    if (value == null || value == Undefined.instance) {
      removeModuleLoader(extension);
    } else if (!(value instanceof Function)) {
      throw Context.reportRuntimeError("Module loader must be a function");
    }
    Function function = (Function) value;
    int length = loaders.length;
    for (int i = 0; i < length; i++) {
      if (extension.equals(loaders[i].getExtension())) {
        // replace existing loader
        loaders[i] = new ScriptedModuleLoader(extension, function);
        return;
      }
    }
    ModuleLoader[] newLoaders = new ModuleLoader[length + 1];
    System.arraycopy(loaders, 0, newLoaders, 0, length);
    newLoaders[length] = new ScriptedModuleLoader(extension, function);
    loaders = newLoaders;
  }

  public synchronized void removeModuleLoader(String extension) {
    int length = loaders.length;
    for (int i = 0; i < length; i++) {
      if (loaders[i] instanceof ScriptedModuleLoader
          && extension.equals(loaders[i].getExtension())) {
        ModuleLoader[] newLoaders = new ModuleLoader[length - 1];
        if (i > 0) System.arraycopy(loaders, 0, newLoaders, 0, i);
        if (i < length - 1) System.arraycopy(loaders, i + 1, newLoaders, i, length - i - 1);
        loaders = newLoaders;
        return;
      }
    }
  }

  public static String normalizePath(String path) {
    if (!path.contains("./")) {
      return path;
    }
    boolean absolute = path.startsWith("/");
    String[] elements = path.split(Repository.SEPARATOR);
    LinkedList<String> list = new LinkedList<String>();
    for (String e : elements) {
      if ("..".equals(e)) {
        if (list.isEmpty() || "..".equals(list.getLast())) {
          list.add(e);
        } else {
          list.removeLast();
        }
      } else if (!".".equals(e) && e.length() > 0) {
        list.add(e);
      }
    }
    StringBuilder sb = new StringBuilder(path.length());
    if (absolute) {
      sb.append("/");
    }
    int count = 0, last = list.size() - 1;
    for (String e : list) {
      sb.append(e);
      if (count++ < last) sb.append("/");
    }
    return sb.toString();
  }

  public void addToClasspath(Trackable path) throws MalformedURLException {
    loader.addURL(path.getUrl());
  }

  public RingoContextFactory getContextFactory() {
    return contextFactory;
  }

  public RingoClassLoader getClassLoader() {
    return loader;
  }

  public RingoConfig getConfig() {
    return config;
  }

  Singleton getSingleton(Singleton singleton) {
    synchronized (singletons) {
      Singleton st = singletons.get(singleton);
      if (st == null) {
        st = singleton;
        singletons.put(singleton, singleton);
      }
      return st;
    }
  }

  /**
   * Get a wrapper for a string that exposes the java.lang.String methods to JavaScript This is
   * useful for accessing strings as java.lang.String without the cost of creating a new instance.
   *
   * @param object an object
   * @return the object converted to a string and wrapped as native java object
   */
  public Object asJavaString(Object object) {
    if (!(object instanceof String)) {
      object = object.toString();
    }
    Context cx = Context.getCurrentContext();
    return wrapFactory.wrapAsJavaObject(cx, globalScope, object, null);
  }

  /**
   * Get a wrapper for an object that exposes it as Java object to JavaScript.
   *
   * @param object an object
   * @return the object wrapped as native java object
   */
  public Object asJavaObject(Object object) {
    if (object instanceof Wrapper) {
      object = ((Wrapper) object).unwrap();
    }
    Context cx = Context.getCurrentContext();
    return wrapFactory.wrapAsJavaObject(cx, globalScope, object, null);
  }

  /**
   * Get the engine's WrapFactory.
   *
   * @return the engine's WrapFactory instance
   */
  public WrapFactory getWrapFactory() {
    return wrapFactory;
  }

  static class AsyncTaskCounter {

    int count = 0;

    synchronized void waitTillDone() throws InterruptedException {
      while (count > 0) {
        wait();
      }
    }

    synchronized void increase() {
      ++count;
    }

    synchronized void decrease() {
      if (--count <= 0) {
        notifyAll();
      }
    }
  }
}