package nodebox.function; import clojure.java.api.Clojure; import clojure.lang.*; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import nodebox.util.FileUtils; import nodebox.util.LoadException; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; final class ClojureLibrary extends FunctionLibrary { /** * Run the Clojure register-nodes function in the library. * * @param fileName The file name. * @return The new Clojure library. * @throws LoadException If the script could not be loaded. */ public static ClojureLibrary loadScript(String fileName) throws LoadException { return loadScript(null, fileName); } /** * Run the Clojure register-nodes function in the library. * * @param baseFile The file to which the path of this library is relative to. * @param fileName The file name. * @return The new Clojure library. * @throws LoadException If the script could not be loaded. */ public static ClojureLibrary loadScript(File baseFile, String fileName) throws LoadException { File file; if (baseFile != null) { file = new File(baseFile.getAbsoluteFile(), fileName); } else { file = new File(fileName); } return loadScript(file); } private static ClojureLibrary loadScript(File file) { Object returnValue; try { IFn loadFile = Clojure.var("clojure.core", "load-file"); returnValue = loadFile.invoke(file.getCanonicalPath()); } catch (IOException e) { throw new LoadException(file, e); } // We need a Var as the last statement, because we need to retrieve the current namespace. if (!(returnValue instanceof Var)) { throw new LoadException(file, String.format("The last statement does not define a var, but %s.\n", returnValue)); } Var nodesVar = (Var) returnValue; Namespace ns = nodesVar.ns; String namespace = ns.name.getName(); ImmutableMap.Builder<String, Function> builder = ImmutableMap.builder(); for (Object item : ns.getMappings()) { MapEntry entry = (MapEntry) item; if (entry.getValue() instanceof Var) { Var var = (Var) entry.getValue(); if (var.ns.toString().equals(namespace)) { String name = entry.getKey().toString(); if (var.deref() instanceof IFn) { Function f = new ClojureFunction(name, var.fn()); builder.put(name, f); } } } } return new ClojureLibrary(namespace, file, builder.build()); } private final String namespace; private final File file; private ImmutableMap<String, Function> functionMap; private ClojureLibrary(String namespace, File file, ImmutableMap<String, Function> functionMap) { this.namespace = namespace; this.file = file; this.functionMap = functionMap; } @Override public String getLink(File baseFile) { File parentFile = baseFile != null ? baseFile.getParentFile() : null; return "clojure:" + FileUtils.getRelativeLink(file, parentFile); } public String getSimpleIdentifier() { return file.getName(); } public String getNamespace() { return namespace; } public String getLanguage() { return "clojure"; } public File getFile() { return file; } public Function getFunction(String name) { return functionMap.get(name); } public boolean hasFunction(String name) { return functionMap.containsKey(name); } /** * Reloads the clojure module. */ @Override public void reload() { ClojureLibrary reloadedLibrary = loadScript(file); if (!reloadedLibrary.namespace.equals(namespace)) throw new RuntimeException("The namespace of a function library should not be changed."); this.functionMap = reloadedLibrary.functionMap; } private static final class ClojureFunction implements Function { private final String name; private final IFn fn; private final ImmutableList<Argument> arguments; public ClojureFunction(String name, IFn fn) { this.name = name; this.fn = fn; this.arguments = introspect(fn); } public String getName() { return name; } public Object invoke(Object... args) throws Exception { return fn.applyTo(RT.arrayToList(args)); } public ImmutableList<Argument> getArguments() { return arguments; } private static ImmutableList<Argument> introspect(IFn fn) { // Each function is a separate class. Class functionClass = fn.getClass(); Method m = Functions.findMethod(functionClass, "invoke"); return Functions.introspect(m); } } }