diff options
Diffstat (limited to 'lisp/org/ob-java.el')
-rw-r--r-- | lisp/org/ob-java.el | 482 |
1 files changed, 444 insertions, 38 deletions
diff --git a/lisp/org/ob-java.el b/lisp/org/ob-java.el index b1d517e94aa..dd3538743db 100644 --- a/lisp/org/ob-java.el +++ b/lisp/org/ob-java.el @@ -1,8 +1,10 @@ -;;; ob-java.el --- Babel Functions for Java -*- lexical-binding: t; -*- +;;; ob-java.el --- org-babel functions for java evaluation -*- lexical-binding: t -*- ;; Copyright (C) 2011-2021 Free Software Foundation, Inc. -;; Author: Eric Schulte +;; Authors: Eric Schulte +;; Dan Davison +;; Maintainer: Ian Martins <ianxm@jhu.edu> ;; Keywords: literate programming, reproducible research ;; Homepage: https://orgmode.org @@ -23,8 +25,7 @@ ;;; Commentary: -;; Currently this only supports the external compilation and execution -;; of java code blocks (i.e., no session support). +;; Org-Babel support for evaluating java source code. ;;; Code: (require 'ob) @@ -32,52 +33,457 @@ (defvar org-babel-tangle-lang-exts) (add-to-list 'org-babel-tangle-lang-exts '("java" . "java")) +(defvar org-babel-temporary-directory) ; from ob-core + +(defvar org-babel-default-header-args:java '((:results . "output") + (:dir . ".")) + "Default header args for java source blocks. +The docs say functional mode should be the default [1], but +ob-java didn't originally support functional mode, so we keep +scripting mode as the default for now to maintain previous +behavior. + +Most languages write tempfiles to babel's temporary directory, +but ob-java originally had to write them to the current +directory, so we keep that as the default behavior. + +[1] https://orgmode.org/manual/Results-of-Evaluation.html") + +(defconst org-babel-header-args:java '((imports . :any)) + "Java-specific header arguments.") + (defcustom org-babel-java-command "java" "Name of the java command. -May be either a command in the path, like java -or an absolute path name, like /usr/local/bin/java -parameters may be used, like java -verbose" +May be either a command in the path, like java or an absolute +path name, like /usr/local/bin/java. Parameters may be used, +like java -verbose." :group 'org-babel - :version "24.3" + :package-version '(Org . "9.5") :type 'string) (defcustom org-babel-java-compiler "javac" "Name of the java compiler. -May be either a command in the path, like javac -or an absolute path name, like /usr/local/bin/javac -parameters may be used, like javac -verbose" +May be either a command in the path, like javac or an absolute +path name, like /usr/local/bin/javac. Parameters may be used, +like javac -verbose." + :group 'org-babel + :package-version '(Org . "9.5") + :type 'string) + +(defcustom org-babel-java-hline-to "null" + "Replace hlines in incoming tables with this when translating to java." :group 'org-babel - :version "24.3" + :package-version '(Org . "9.5") :type 'string) +(defcustom org-babel-java-null-to 'hline + "Replace `null' in java tables with this before returning." + :group 'org-babel + :package-version '(Org . "9.5") + :type 'symbol) + +(defconst org-babel-java--package-re (rx line-start (0+ space) "package" + (1+ space) (group (1+ (in alnum ?_ ?.))) ; capture the package name + (0+ space) ?\; line-end) + "Regexp for the package statement.") +(defconst org-babel-java--imports-re (rx line-start (0+ space) "import" + (opt (1+ space) "static") + (1+ space) (group (1+ (in alnum ?_ ?. ?*))) ; capture the fully qualified class name + (0+ space) ?\; line-end) + "Regexp for import statements.") +(defconst org-babel-java--class-re (rx line-start (0+ space) (opt (seq "public" (1+ space))) + "class" (1+ space) + (group (1+ (in alnum ?_))) ; capture the class name + (0+ space) ?{) + "Regexp for the class declaration.") +(defconst org-babel-java--main-re + (rx line-start (0+ space) "public" + (1+ space) "static" + (1+ space) "void" + (1+ space) "main" + (0+ space) ?\( + (0+ space) "String" + (1+ (in alnum ?_ ?\[ ?\] space)) ; "[] args" or "args[]" + ?\) + (0+ space) (opt "throws" (1+ (in alnum ?_ ?, ?. space))) + ?{) + "Regexp for the main method declaration.") +(defconst org-babel-java--any-method-re + (rx line-start + (0+ space) (opt (seq (1+ alnum) (1+ space))) ; visibility + (opt (seq "static" (1+ space))) ; binding + (1+ (in alnum ?_ ?\[ ?\])) ; return type + (1+ space) (1+ (in alnum ?_)) ; method name + (0+ space) ?\( + (0+ (in alnum ?_ ?\[ ?\] ?, space)) ; params + ?\) + (0+ space) (opt "throws" (1+ (in alnum ?_ ?, ?. space))) + ?{) + "Regexp for any method.") +(defconst org-babel-java--result-wrapper "\n public static String __toString(Object val) { + if (val instanceof String) { + return \"\\\"\" + val + \"\\\"\"; + } else if (val == null) { + return \"null\"; + } else if (val.getClass().isArray()) { + StringBuffer sb = new StringBuffer(); + Object[] vals = (Object[])val; + sb.append(\"[\"); + for (int ii=0; ii<vals.length; ii++) { + sb.append(__toString(vals[ii])); + if (ii<vals.length-1) + sb.append(\",\"); + } + sb.append(\"]\"); + return sb.toString(); + } else if (val instanceof List) { + StringBuffer sb = new StringBuffer(); + List vals = (List)val; + sb.append(\"[\"); + for (int ii=0; ii<vals.size(); ii++) { + sb.append(__toString(vals.get(ii))); + if (ii<vals.size()-1) + sb.append(\",\"); + } + sb.append(\"]\"); + return sb.toString(); + } else { + return String.valueOf(val); + } + } + + public static void main(String[] args) throws IOException { + BufferedWriter output = new BufferedWriter(new FileWriter(\"%s\")); + output.write(__toString(_main(args))); + output.close(); + }" + "Code to inject into a class so that we can capture the value it returns. +This implementation was inspired by ob-python, although not as +elegant. This modified the source block to write out the value +it wants to return to a temporary file so that ob-java can read +it back. The name of the temporary file to write must be +replaced in this string.") + (defun org-babel-execute:java (body params) - (let* ((classname (or (cdr (assq :classname params)) - (error - "Can't compile a java block without a classname"))) - (packagename (file-name-directory classname)) - (src-file (concat classname ".java")) - (cmpflag (or (cdr (assq :cmpflag params)) "")) - (cmdline (or (cdr (assq :cmdline params)) "")) - (cmdargs (or (cdr (assq :cmdargs params)) "")) - (full-body (org-babel-expand-body:generic body params))) - (with-temp-file src-file (insert full-body)) - (org-babel-eval - (concat org-babel-java-compiler " " cmpflag " " src-file) "") + "Execute a java source block with BODY code and PARAMS params." + (let* (;; allow header overrides + (org-babel-java-compiler + (or (cdr (assq :javac params)) + org-babel-java-compiler)) + (org-babel-java-command + (or (cdr (assq :java params)) + org-babel-java-command)) + ;; if true, run from babel temp directory + (run-from-temp (not (cdr (assq :dir params)))) + ;; class and package + (fullclassname (or (cdr (assq :classname params)) + (org-babel-java-find-classname body))) + ;; just the class name + (classname (car (last (split-string fullclassname "\\.")))) + ;; just the package name + (packagename (if (string-match-p "\\." fullclassname) + (file-name-base fullclassname))) + ;; the base dir that contains the top level package dir + (basedir (file-name-as-directory (if run-from-temp + (if (file-remote-p default-directory) + (concat + (file-remote-p default-directory) + org-babel-remote-temporary-directory) + org-babel-temporary-directory) + default-directory))) + ;; the dir to write the source file + (packagedir (if (and (not run-from-temp) packagename) + (file-name-as-directory + (concat basedir (replace-regexp-in-string "\\." "/" packagename))) + basedir)) + ;; the filename of the source file + (src-file (concat packagedir classname ".java")) + ;; compiler flags + (cmpflag (or (cdr (assq :cmpflag params)) "")) + ;; runtime flags + (cmdline (or (cdr (assq :cmdline params)) "")) + ;; command line args + (cmdargs (or (cdr (assq :cmdargs params)) "")) + ;; the command to compile and run + (cmd (concat org-babel-java-compiler " " cmpflag " " + (org-babel-process-file-name src-file 'noquote) + " && " org-babel-java-command + " -cp " (org-babel-process-file-name basedir 'noquote) + " " cmdline " " (if run-from-temp classname fullclassname) + " " cmdargs)) + ;; header args for result processing + (result-type (cdr (assq :result-type params))) + (result-params (cdr (assq :result-params params))) + (result-file (and (eq result-type 'value) + (org-babel-temp-file "java-"))) + ;; the expanded body of the source block + (full-body (org-babel-expand-body:java body params))) + ;; created package-name directories if missing - (unless (or (not packagename) (file-exists-p packagename)) - (make-directory packagename 'parents)) - (let ((results (org-babel-eval (concat org-babel-java-command - " " cmdline " " classname " " cmdargs) ""))) - (org-babel-reassemble-table - (org-babel-result-cond (cdr (assq :result-params params)) - (org-babel-read results t) - (let ((tmp-file (org-babel-temp-file "c-"))) - (with-temp-file tmp-file (insert results)) - (org-babel-import-elisp-from-file tmp-file))) - (org-babel-pick-name - (cdr (assq :colname-names params)) (cdr (assq :colnames params))) - (org-babel-pick-name - (cdr (assq :rowname-names params)) (cdr (assq :rownames params))))))) + (unless (or (not packagedir) (file-exists-p packagedir)) + (make-directory packagedir 'parents)) + + ;; write the source file + (setq full-body (org-babel-java--expand-for-evaluation + full-body run-from-temp result-type result-file)) + (with-temp-file src-file (insert full-body)) + + ;; compile, run, process result + (org-babel-reassemble-table + (org-babel-java-evaluate cmd result-type result-params result-file) + (org-babel-pick-name + (cdr (assoc :colname-names params)) (cdr (assoc :colnames params))) + (org-babel-pick-name + (cdr (assoc :rowname-names params)) (cdr (assoc :rownames params)))))) + +;; helper functions + +(defun org-babel-java-find-classname (body) + "Try to find fully qualified class name in BODY. +Look through BODY for the package and class. If found, put them +together into a fully qualified class name and return. Else just +return class name. If that isn't found either, default to Main." + (let ((package (if (string-match org-babel-java--package-re body) + (match-string 1 body))) + (class (if (string-match org-babel-java--class-re body) + (match-string 1 body)))) + (or (and package class (concat package "." class)) + (and class class) + (and package (concat package ".Main")) + "Main"))) + +(defun org-babel-java--expand-for-evaluation (body suppress-package-p result-type result-file) + "Expand source block for evaluation. +In order to return a value we have to add a __toString method. +In order to prevent classes without main methods from erroring we +add a dummy main method if one is not provided. These +manipulations are done outside of `org-babel--expand-body' so +that they are hidden from tangles. + +BODY is the file content before instrumentation. + +SUPPRESS-PACKAGE-P if true, suppress the package statement. + +RESULT-TYPE is taken from params. + +RESULT-FILE is the temp file to write the result." + (with-temp-buffer + (insert body) + + ;; suppress package statement + (goto-char (point-min)) + (when (and suppress-package-p + (re-search-forward org-babel-java--package-re nil t)) + (replace-match "")) + + ;; add a dummy main method if needed + (goto-char (point-min)) + (when (not (re-search-forward org-babel-java--main-re nil t)) + (org-babel-java--move-past org-babel-java--class-re) + (insert "\n public static void main(String[] args) { + System.out.print(\"success\"); + }\n\n")) + + ;; special handling to return value + (when (eq result-type 'value) + (goto-char (point-min)) + (org-babel-java--move-past org-babel-java--class-re) + (insert (format org-babel-java--result-wrapper + (org-babel-process-file-name result-file 'noquote))) + (search-forward "public static void main(") ; rename existing main + (replace-match "public static Object _main(")) + + ;; add imports + (org-babel-java--import-maybe "java.util" "List") + (org-babel-java--import-maybe "java.util" "Arrays") + (org-babel-java--import-maybe "java.io" "BufferedWriter") + (org-babel-java--import-maybe "java.io" "FileWriter") + (org-babel-java--import-maybe "java.io" "IOException") + + (buffer-string))) + +(defun org-babel-java--move-past (re) + "Move point past the first occurrence of the given regexp RE." + (while (re-search-forward re nil t) + (goto-char (1+ (match-end 0))))) + +(defun org-babel-java--import-maybe (package class) + "Import from PACKAGE the given CLASS if it is used and not already imported." + (let (class-found import-found) + (goto-char (point-min)) + (setq class-found (re-search-forward class nil t)) + (goto-char (point-min)) + (setq import-found + (re-search-forward (concat "^import .*" package ".*\\(?:\\*\\|" class "\\);") nil t)) + (when (and class-found (not import-found)) + (org-babel-java--move-past org-babel-java--package-re) + (insert (concat "import " package "." class ";\n"))))) + +(defun org-babel-expand-body:java (body params) + "Expand BODY with PARAMS. +BODY could be a few statements, or could include a full class +definition specifying package, imports, and class. Because we +allow this flexibility in what the source block can contain, it +is simplest to expand the code block from the inside out." + (let* ((fullclassname (or (cdr (assq :classname params)) ; class and package + (org-babel-java-find-classname body))) + (classname (car (last (split-string fullclassname "\\.")))) ; just class name + (packagename (if (string-match-p "\\." fullclassname) ; just package name + (file-name-base fullclassname))) + (var-lines (org-babel-variable-assignments:java params)) + (imports-val (assq :imports params)) + (imports (if imports-val + (split-string (org-babel-read (cdr imports-val) nil) " ") + nil))) + (with-temp-buffer + (insert body) + + ;; wrap main. If there are methods defined, but no main method + ;; and no class, wrap everything in a generic main method. + (goto-char (point-min)) + (when (and (not (re-search-forward org-babel-java--main-re nil t)) + (not (re-search-forward org-babel-java--any-method-re nil t))) + (org-babel-java--move-past org-babel-java--package-re) ; if package is defined, move past it + (org-babel-java--move-past org-babel-java--imports-re) ; if imports are defined, move past them + (insert "public static void main(String[] args) {\n") + (indent-code-rigidly (point) (point-max) 4) + (goto-char (point-max)) + (insert "\n}")) + + ;; wrap class. If there's no class, wrap everything in a + ;; generic class. + (goto-char (point-min)) + (when (not (re-search-forward org-babel-java--class-re nil t)) + (org-babel-java--move-past org-babel-java--package-re) ; if package is defined, move past it + (org-babel-java--move-past org-babel-java--imports-re) ; if imports are defined, move past them + (insert (concat "\npublic class " (file-name-base classname) " {\n")) + (indent-code-rigidly (point) (point-max) 4) + (goto-char (point-max)) + (insert "\n}")) + (goto-char (point-min)) + + ;; insert variables from source block headers + (when var-lines + (goto-char (point-min)) + (org-babel-java--move-past org-babel-java--class-re) ; move inside class + (insert (mapconcat 'identity var-lines "\n")) + (insert "\n")) + + ;; add imports from source block headers + (when imports + (goto-char (point-min)) + (org-babel-java--move-past org-babel-java--package-re) ; if package is defined, move past it + (insert (mapconcat (lambda (package) (concat "import " package ";")) imports "\n") "\n")) + + ;; add package at the top + (goto-char (point-min)) + (when (and packagename (not (re-search-forward org-babel-java--package-re nil t))) + (insert (concat "package " packagename ";\n"))) + + ;; return expanded body + (buffer-string)))) + +(defun org-babel-variable-assignments:java (params) + "Return a list of java statements assigning the block's variables. +variables are contained in PARAMS." + (mapcar + (lambda (pair) + (let* ((type-data (org-babel-java-val-to-type (cdr pair))) + (basetype (car type-data)) + (var-to-java (lambda (var) (funcall #'org-babel-java-var-to-java var basetype)))) + (format " static %s %s = %s;" + (cdr type-data) ; type + (car pair) ; name + (funcall var-to-java (cdr pair))))) ; value + (org-babel--get-vars params))) + +(defun org-babel-java-var-to-java (var basetype) + "Convert an elisp value to a java variable. +Convert an elisp value, VAR, of type BASETYPE into a string of +java source code specifying a variable of the same value." + (cond ((and (sequencep var) (not (stringp var))) + (let ((var-to-java (lambda (var) (funcall #'org-babel-java-var-to-java var basetype)))) + (concat "Arrays.asList(" (mapconcat var-to-java var ", ") ")"))) + ((eq var 'hline) org-babel-java-hline-to) + ((eq basetype 'integerp) (format "%d" var)) + ((eq basetype 'floatp) (format "%f" var)) + ((eq basetype 'stringp) (if (and (stringp var) (string-match-p ".\n+." var)) + (error "Java does not support multiline string literals") + (format "\"%s\"" var))))) + +(defun org-babel-java-val-to-type (val) + "Determine the type of VAL. +Return (BASETYPE . LISTTYPE), where BASETYPE is a symbol +representing the type of the individual items in VAL, and +LISTTYPE is a string name of the type parameter for a container +for BASETYPE items." + (let* ((basetype (org-babel-java-val-to-base-type val)) + (basetype-str (pcase basetype + (`integerp "Integer") + (`floatp "Double") + (`stringp "String") + (_ (error "Unknown type %S" basetype))))) + (cond + ((and (listp val) (listp (car val))) ; a table + (cons basetype (format "List<List<%s>>" basetype-str))) + ((or (listp val) (vectorp val)) ; a list declared in the source block header + (cons basetype (format "List<%s>" basetype-str))) + (t ; return base type + (cons basetype basetype-str))))) + +(defun org-babel-java-val-to-base-type (val) + "Determine the base type of VAL. +VAL may be +`integerp' if all base values are integers +`floatp' if all base values are either floating points or integers +`stringp' otherwise." + (cond + ((integerp val) 'integerp) + ((floatp val) 'floatp) + ((or (listp val) (vectorp val)) + (let ((type nil)) + (mapc (lambda (v) + (pcase (org-babel-java-val-to-base-type v) + (`stringp (setq type 'stringp)) + (`floatp + (when (or (not type) (eq type 'integerp)) + (setq type 'floatp))) + (`integerp + (unless type (setq type 'integerp))))) + val) + type)) + (t 'stringp))) + +(defun org-babel-java-table-or-string (results) + "Convert RESULTS into an appropriate elisp value. +If the results look like a list or vector, then convert them into an +Emacs-lisp table, otherwise return the results as a string." + (let ((res (org-babel-script-escape results))) + (if (listp res) + (mapcar (lambda (el) (if (eq 'null el) + org-babel-java-null-to + el)) + res) + res))) + +(defun org-babel-java-evaluate (cmd result-type result-params result-file) + "Evaluate using an external java process. +CMD the command to execute. + +If RESULT-TYPE equals `output' then return standard output as a +string. If RESULT-TYPE equals `value' then return the value +returned by the source block, as elisp. + +RESULT-PARAMS input params used to format the response. + +RESULT-FILE filename of the tempfile to store the returned value in +for `value' RESULT-TYPE. Not used for `output' RESULT-TYPE." + (let ((raw (pcase result-type + (`output (org-babel-eval cmd "")) + (`value (org-babel-eval cmd "") + (org-babel-eval-read-file result-file))))) + (org-babel-result-cond result-params raw + (org-babel-java-table-or-string raw)))) (provide 'ob-java) |