emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
From: ian martins <ianxm@jhu.edu>
To: Org-Mode mailing list <emacs-orgmode@gnu.org>
Subject: Re: [PATCH] ob-java
Date: Fri, 9 Oct 2020 07:15:16 -0400	[thread overview]
Message-ID: <CAC=rjb5ed68AK0umWwjphDoc2ETdcp=kqptEXun=nV+STUC9Qg@mail.gmail.com> (raw)
In-Reply-To: <CAC=rjb5py_-uNBikLNZarYfTSXMxaOBeJsF8LW+C-3KX6cA5mg@mail.gmail.com>


[-- Attachment #1.1: Type: text/plain, Size: 5995 bytes --]

I noticed that the tests didn't run with "make test." This updates the
patch so that they can. I didn't add java to the list of default languages
because the java tests are slow.

On Mon, Oct 5, 2020 at 9:23 AM ian martins <ianxm@jhu.edu> wrote:

> I wrote those examples in an org file so I could test as I wrote them, and
> then exported it to make it more readable, but the export resulted in
> source block headers being lost.  Here is the same without export:
> ----
> * Changes
>
> - support for functional mode (~:results value~)
> - accept variables
> - don't require package, class, and main definitions
> - write source and result tempfiles to ~org-babel-temporary-directory~,
> but respects the ~:dir~ header
> - work with tramp
>
> * Examples
> ** Example 1
> This outputs "hello."  If class and main definitions aren't given the
> code block will be wrapped in generic ones.
>
> #+begin_src java :results output silent
>   System.out.print("hello");
> #+end_src
>
> This is exactly equivalent:
>
> #+begin_src java :results output silent
>   public class Main {
>       public static void main(String[] args) {
>           System.out.print("hello");
>       }
>   }
> #+end_src
>
> ** Example 2
> This also outputs "hello."
>
> #+begin_src java :results value silent
>   return "hello";
> #+end_src
>
> ** Example 3
> This generates the class "Example" in the package "org.orgmode" in the
> current directory.
>
> #+begin_src java :results output silent :classname org.orgmode.Example
> :dir .
>   System.out.print("hello, org-mode");
> #+end_src
>
> ** Example 4
> The "Hey" class defines a static method but no main. C-c C-c on the
> "Hey" source block will write "./org/orgmode/Hey.java" and compile it.
>
> The "Main" class calls the "Hey" class. C-c C-c on the "Main" source
> block will write "./org/orgmode/Main.java" and compile and run it.
>
> #+begin_src java :results output silent :dir .
>   package org.orgmode;
>
>   public class Hey {
>       public static String say() {
>           return "hey";
>       }
>   }
> #+end_src
>
> #+begin_src java :results output silent :dir .
>   package org.orgmode;
>
>   public class Main {
>       public static void main(String[] args) {
>           System.out.print(Hey.say());
>       }
>   }
> #+end_src
>
> Instead of C-c C-c, we could have added tangle headers and written the
> source files out by tangling.
>
> ** Example 5
> This prints the variable from the header
>
> #+begin_src java :var msg="hello, org-mode" :results output silent
>   System.out.print(msg);
> #+end_src
>
> ** Example 6
> This prints "hello, org-mode." The table is provided to the method as a
> list of lists.
>
> #+name: table
> | message | hello, org-mode  |
>
> #+begin_src java :var tbl=table :results output silent
>   System.out.print(tbl.get(0).get(1));
> #+end_src
>
> ** Example 7
> This example returns a list.
>
> Note that you're allowed to specify imports without defining the class
> or main methods.
>
> #+begin_src java :results value :exports both
>   import java.util.Arrays;
>
>   return Arrays.asList("message", "hello, org-mode");
> #+end_src
>
> #+RESULTS:
> | message | hello, org-mode |
>
> On Mon, Oct 5, 2020 at 8:35 AM ian martins <ianxm@jhu.edu> wrote:
>
>> 1 Changes
>> =========
>>
>>   - support for functional mode (`:results value')
>>   - accept variables
>>   - don't require package, class, and main definitions
>>   - write source and result tempfiles to
>>     `org-babel-temporary-directory', but respects the `:dir' header
>>   - work with tramp
>>
>>
>> 2 Examples
>> ==========
>> Some examples follow.  See the tests for more examples.  I'll write
>> proper docs after review.
>>
>> 2.1 Example 1
>> ~~~~~~~~~~~~~
>>
>>   This outputs "hello."  If class and main definitions aren't given the
>>   code block will be wrapped in generic ones.
>>
>>   ,----
>>   | System.out.print("hello");
>>   `----
>>
>>   This is exactly equivalent:
>>
>>   ,----
>>   | public class Main {
>>   |     public static void main(String[] args) {
>>   |         System.out.print("hello");
>>   |     }
>>   | }
>>   `----
>>
>>
>> 2.2 Example 2
>> ~~~~~~~~~~~~~
>>
>>   This also outputs "hello."
>>
>>   ,----
>>   | return "hello";
>>   `----
>>
>>
>> 2.3 Example 3
>> ~~~~~~~~~~~~~
>>
>>   This generates the class "Example" in the package "org.orgmode" in the
>>   current directory.
>>
>>   ,----
>>   | System.out.print("hello, org-mode");
>>   `----
>>
>>
>> 2.4 Example 4
>> ~~~~~~~~~~~~~
>>
>>   The "Hey" class defines a static method but no main. C-c C-c on the
>>   "Hey" source block will write "./org/orgmode/Hey.java" and compile it.
>>
>>   The "Main" class calls the "Hey" class. C-c C-c on the "Main" source
>>   block will write "./org/orgmode/Main.java" and compile and run it.
>>
>>   ,----
>>   | package org.orgmode;
>>   |
>>   | public class Hey {
>>   |     public static String say() {
>>   |         return "hey";
>>   |     }
>>   | }
>>   `----
>>
>>   ,----
>>   | package org.orgmode;
>>   |
>>   | public class Main {
>>   |     public static void main(String[] args) {
>>   |         System.out.print(Hey.say());
>>   |     }
>>   | }
>>   `----
>>
>>   Instead of C-c C-c, we could have added tangle headers and written the
>>   source files out by tangling.
>>
>>
>> 2.5 Example 5
>> ~~~~~~~~~~~~~
>>
>>   This prints the variable from the header
>>
>>   ,----
>>   | System.out.print(msg);
>>   `----
>>
>>
>> 2.6 Example 6
>> ~~~~~~~~~~~~~
>>
>>   This prints "hello, org-mode." The table is provided to the method as
>>   a list of lists.
>>
>>    message  hello, org-mode
>>
>>   ,----
>>   | System.out.print(tbl.get(0).get(1));
>>   `----
>>
>>
>> 2.7 Example 7
>> ~~~~~~~~~~~~~
>>
>>   This example returns a list.
>>
>>   Note that you're allowed to specify imports without defining the class
>>   or main methods.
>>
>>   ,----
>>   | import java.util.Arrays;
>>   |
>>   | return Arrays.asList("message", "hello, org-mode");
>>   `----
>>
>>    message  hello, org-mode
>>
>

[-- Attachment #1.2: Type: text/html, Size: 7390 bytes --]

[-- Attachment #2: 0001-ob-java.el-Add-support-for-variables-return-values-t.patch --]
[-- Type: text/x-patch, Size: 39756 bytes --]

From 6307c528d1d8aebc0200555dea6855f401132aa5 Mon Sep 17 00:00:00 2001
From: Ian Martins <ianxm@jhu.edu>
Date: Mon, 5 Oct 2020 08:07:25 -0400
Subject: [PATCH] ob-java.el: Add support for variables, return values, tramp

* lisp/ob-java.el: Add support for variables and return values.  Write
tempfiles to the org-babel-temporary-directory.  Make package, class,
and main method definitions optional.

* testing/lisp/test-ob-java.el: Add tests.
---
 lisp/ob-java.el              | 422 ++++++++++++++++++++++---
 testing/lisp/test-ob-java.el | 583 +++++++++++++++++++++++++++++++++++
 2 files changed, 964 insertions(+), 41 deletions(-)
 create mode 100644 testing/lisp/test-ob-java.el

diff --git a/lisp/ob-java.el b/lisp/ob-java.el
index fee695bb9..e704c5552 100644
--- a/lisp/ob-java.el
+++ b/lisp/ob-java.el
@@ -1,9 +1,8 @@
-;;; 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-2020 Free Software Foundation, Inc.
 
-;; Author: Eric Schulte
-;; Maintainer: Ian Martins <ianxm@jhu.edu>
+;; Author: Ian Martins <ianxm@jhu.edu>
 ;; Keywords: literate programming, reproducible research
 ;; Homepage: https://orgmode.org
 
@@ -24,8 +23,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)
@@ -33,52 +31,394 @@
 (defvar org-babel-tangle-lang-exts)
 (add-to-list 'org-babel-tangle-lang-exts '("java" . "java"))
 
-(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"
+(defvar org-babel-default-header-args:java '()
+  "Default header args for java source blocks.")
+
+(defconst org-babel-header-args:java '((imports . :any))
+  "Java-specific header arguments.")
+
+(defvar org-babel-java-compiler-command "javac"
+  "Name of the command to execute the java compiler.")
+
+(defvar org-babel-java-runtime-command "java"
+  "Name of the command to run the java runtime.")
+
+(defcustom org-babel-java-hline-to "null"
+  "Replace hlines in incoming tables with this when translating to java."
   :group 'org-babel
-  :version "24.3"
+  :version "25.2"
+  :package-version '(Org . "9.3")
   :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"
+(defcustom org-babel-java-null-to 'hline
+  "Replace `null' in java tables with this before returning."
   :group 'org-babel
-  :version "24.3"
-  :type 'string)
+  :version "25.2"
+  :package-version '(Org . "9.3")
+  :type 'symbol)
 
 (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"))
+  "Execute a java source block with BODY code and PARAMS params."
+  (let* (;; if true, run from babel temp directory
+         (run-from-temp (not (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 (seq-contains fullclassname ?.)
+                          (file-name-base fullclassname)))
+         ;; the base dir that contains the top level package dir
+         (basedir (file-name-as-directory (if run-from-temp
+                                              org-babel-temporary-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)) ""))
-	 (cmdline (or (cdr (assq :cmdline params)) ""))
+	 ;; runtime flags
+         (cmdline (or (cdr (assq :cmdline params)) ""))
+         ;; command line args
 	 (cmdargs (or (cdr (assq :cmdargs params)) ""))
-	 (full-body (org-babel-expand-body:generic body params)))
+         ;; the command to compile and run
+         (cmd (concat org-babel-java-compiler-command " " cmpflag " "
+                      (org-babel-process-file-name src-file 'noquote)
+                      " && " org-babel-java-runtime-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))
+    (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))
-    (org-babel-eval
-     (concat org-babel-java-compiler " " cmpflag " " src-file) "")
-    (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)))))))
+
+    ;; 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 "package \\\([^ ]*\\\);" body)
+                     (match-string 1 body)))
+        (class (if (string-match "public class \\\([^ \n]*\\\)" body)
+                   (match-string 1 body))))
+    (or (and package class (concat package "." class))
+        (and class class)
+        (and package (concat package ".Main"))
+        "Main")))
+
+(defconst org-babel-java--package-re "^[[:space:]]*package .*;$"
+  "Regexp for the package statement.")
+(defconst org-babel-java--imports-re "^[[:space:]]*import .*;$"
+  "Regexp for import statements.")
+(defconst org-babel-java--class-re "^public class [[:alnum:]_]+[[:space:]]*\n?[[:space:]]*{"
+  "Regexp for the class declaration.")
+(defconst org-babel-java--main-re "public static void main(String\\(?:\\[]\\)? args\\(?:\\[]\\)?).*\n?[[:space:]]*{"
+  "Regexp for the main method declaration.")
+(defconst org-babel-java--any-method-re "public .*(.*).*\n?[[: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-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 (seq-contains 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--class-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 reponse.
+
+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)
 
diff --git a/testing/lisp/test-ob-java.el b/testing/lisp/test-ob-java.el
new file mode 100644
index 000000000..090c40084
--- /dev/null
+++ b/testing/lisp/test-ob-java.el
@@ -0,0 +1,583 @@
+;;; test-ob-java.el --- tests for ob-java.el
+
+;; Copyright (c) 2020 Ian Martins
+;; Authors: Ian Martins
+
+;; This file is not part of GNU Emacs.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Code:
+(require 'org-test)
+
+(require 'ob-core)
+(defvar org-babel-temporary-directory ; from ob-core
+  (if (boundp 'org-babel-temporary-directory)
+    org-babel-temporary-directory
+  (temporary-file-directory)))
+
+(org-test-for-executable "java")
+(org-test-for-executable "javac")
+(unless (featurep 'ob-java)
+  (signal 'missing-test-dependency "Support for java code blocks"))
+
+; simple tests
+
+(ert-deftest ob-java/simple ()
+  "Hello world program that writes output."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+System.out.print(42);
+#+end_src"
+   (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-bracket ()
+  "Hello world program that outputs an open square bracket."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+System.out.print(\"[42\");
+#+end_src"
+   (should (string= "[42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-quote ()
+  "Hello world program that writes quotes."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+System.out.print(\"\\\"42\\\"\");
+#+end_src"
+   (should (string= "\"42\"" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-return-int ()
+  "Hello world program that returns an int value."
+  (org-test-with-temp-text
+      "#+begin_src java :results silent
+return 42;
+#+end_src"
+   (should (eq 42 (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-return-float ()
+  "Hello world program that returns a float value."
+  (org-test-with-temp-text
+      "#+begin_src java :results silent
+return 42.0;
+#+end_src"
+   (should (equal 42.0 (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-return-string ()
+  "Hello world program that returns a string value."
+  (org-test-with-temp-text
+      "#+begin_src java :results silent
+return \"forty two\";
+#+end_src"
+    (should (string= "forty two" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-main ()
+  "Hello world program that defines a main function."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+public static void main(String[] args) {
+    System.out.print(42);
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-two-methods ()
+  "Hello world program with two methods and no class."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+public static void main(String[] args) {
+    System.out.print(foo());
+}
+public static int foo() {
+    return 42;
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-no-main ()
+  "Hello world program with no main method.  Babel adds a dummy one so it can run without error."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+public static int foo() {
+    return 42;
+}
+#+end_src"
+    (should (string= "success" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-main-args-array ()
+  "Hello world program that defines a main function with the square brackets after `args'."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+public static void main(String args[]) {
+    System.out.print(42);
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-class ()
+  "Hello world program that defines a class."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+public class Simple {
+    public static void main(String[] args) {
+        System.out.print(42);
+    }
+}
+#+end_src"
+   (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-class-and-package ()
+  "Hello world program that defines a class and package."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+package pkg;
+public class Simple {
+    public static void main(String[] args) {
+        System.out.print(42);
+    }
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-class-attr ()
+  "Hello world program with class header attribute."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent :classname Simple
+public static void main(String[] args) {
+    System.out.print(42);
+}
+#+end_src"
+   (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/simple-with-class-attr-with-package ()
+  "Hello world program with class attr with package."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent :classname pkg.Simple
+public static void main(String[] args) {
+    System.out.print(42);
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+
+(ert-deftest ob-java/one-arg ()
+  "Command line arg."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent :cmdargs \"fortytwo\"
+System.out.print(args[0]);
+#+end_src"
+    (should (string= "fortytwo" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/args-quoted-string ()
+  "Two command line args, first contains a space."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent :cmdargs \"\\\"forty two\\\" 42\"
+System.out.println(args[0]);
+System.out.println(args[1]);
+#+end_src"
+    (should (string= "forty two\n42\n" (org-babel-execute-src-block)))))
+
+;; var tests
+
+(ert-deftest ob-java/integer-var ()
+  "Read and write an integer variable."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=42 :results output silent
+System.out.print(a);
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/var-with-main ()
+  "Read and write an integer variable, with main function provided."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=42 :results output silent
+public static void main(String[] args) {
+    System.out.print(a);
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/var-with-class ()
+  "Read and write an integer variable, with class provided."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=42 :results output silent
+public class Main {
+    public static void main(String[] args) {
+        System.out.print(a);
+    }
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/var-with-class-and-package ()
+  "Read and write an integer variable, with class and package provided."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=42 :results output silent
+package pkg;
+public class Main {
+    public static void main(String[] args) {
+        System.out.print(a);
+    }
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/var-with-class-and-hanging-curlies ()
+  "Read and write an integer variable, with class with hanging curlies."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=42 :results output silent
+public class Main
+{
+    public static void main(String[] args)
+    {
+        System.out.print(a);
+    }
+}
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/two-vars ()
+  "Read two integer variables, combine and write them."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=21 b=2 :results output silent
+System.out.print(a*b);
+#+end_src"
+    (should (string= "42" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/string-var ()
+  "Read and write a string variable."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=\"forty two\" :results output silent
+System.out.print(String.format(\"%s, len=%d\", a, a.length()));
+#+end_src"
+    (should (string= "forty two, len=9" (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/multiline-string-var ()
+  "Java doesn't support multiline string literals, so this errors."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=\"forty\ntwo\" :results output silent
+System.out.print(String.format(\"%s, len=%d\", a, a.length()));
+#+end_src"
+    (should-error (org-babel-execute-src-block)))
+  :type 'error)
+
+;; return list
+
+(ert-deftest ob-java/return-vector-using-list ()
+  "Return a vector using a list."
+  (org-test-with-temp-text
+      "#+begin_src java :results vector silent
+import java.util.List;
+import java.util.Arrays;
+List<List<Integer>> a = Arrays.asList(Arrays.asList(4),
+                                      Arrays.asList(2));
+return a;
+#+end_src"
+    (should (equal '((4) (2))
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/return-vector-using-array ()
+  "Return a vector using an array."
+  (org-test-with-temp-text
+      "#+begin_src java :results vector silent
+Integer[][] a = {{4}, {2}};
+return a;
+#+end_src"
+    (should (equal '((4) (2))
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/read-return-list ()
+  "Read and return a list."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=java_list :results silent
+import java.util.List;
+import java.util.Arrays;
+List<String> b = Arrays.asList(a.get(0).get(0),
+                               a.get(1).get(0));
+return b;
+#+end_src
+
+#+name: java_list
+- forty
+- two"
+    (should (equal '("forty" "two")
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/read-list-return-array ()
+  "Read a list and return an array."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=java_list :results silent
+String[] b = {a.get(0).get(0), a.get(1).get(0)};
+return b;
+#+end_src
+
+#+name: java_list
+- forty
+- two"
+    (should (equal '("forty" "two")
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/read-return-list-with-package ()
+  "Return a vector."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=java_list :results silent
+package pkg;
+import java.util.List;
+import java.util.Arrays;
+List<String> b = Arrays.asList(a.get(0).get(0),
+                               a.get(1).get(0));
+return b;
+#+end_src
+
+#+name: java_list
+- forty
+- two"
+    (should (equal '("forty" "two")
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/output-list-with-spaces ()
+  "Return a vector."
+  (org-test-with-temp-text
+      "#+begin_src java :results output list raw silent
+System.out.println(\"forty two\");
+System.out.println(\"forty two\");
+#+end_src"
+    (should (equal "forty two\nforty two\n"
+                   (org-babel-execute-src-block)))))
+
+;; list vars
+
+(ert-deftest ob-java/list-var ()
+  "Read and write a list variable."
+  (org-test-with-temp-text
+      "#+begin_src java :var a='(\"forty\" \"two\") :results silent
+import java.util.List;
+List<String> b = a;
+return b;
+#+end_src"
+    (should (equal '("forty" "two")
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/vector-var ()
+  "Read and write a vector variable."
+  (org-test-with-temp-text
+      "#+begin_src java :var a='[\"forty\" \"two\"] :results silent
+import java.util.List;
+List<String> b = a;
+return b;
+#+end_src"
+    (should (equal '("forty" "two")
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/matrix-var ()
+  "Read and write matrix variable."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=java_matrix :results silent
+import java.util.List;
+import java.util.Arrays;
+List<List<Integer>> b = Arrays.asList(Arrays.asList(a.get(0).get(0), a.get(1).get(0)),
+                                      Arrays.asList(a.get(0).get(1), a.get(1).get(1)));
+return b; // transpose
+#+end_src
+
+#+name: java_matrix
+| 2 | 1 |
+| 4 | 2 |"
+    (should (equal '((2 4) (1 2))
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/matrix-var-with-header ()
+  "Read matrix variable and write it with header."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=java_matrix :results value table silent
+import java.util.List;
+import java.util.Arrays;
+List<List> b = Arrays.asList(Arrays.asList(\"col1\", \"col2\"),
+                                     null,
+                                     Arrays.asList(a.get(0).get(0), a.get(1).get(0)),
+                                     Arrays.asList(a.get(0).get(1), a.get(1).get(1)));
+return b; // transpose
+#+end_src
+
+#+name: java_matrix
+| 2 | 1 |
+| 4 | 2 |"
+    (should (equal '(("col1" "col2") hline (2 4) (1 2))
+                   (org-babel-execute-src-block)))))
+
+;; output table
+
+(ert-deftest ob-java/output-table-with-header ()
+  "Write a table that includes a header."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=java_matrix :results output raw table silent
+System.out.println(\"|col1|col2|\");
+System.out.println(\"|-\");
+for (int ii=0; ii<a.size(); ii++) {
+    for (int jj=0; jj<a.get(0).size(); jj++) {
+        System.out.print(\"|\" + a.get(ii).get(jj));
+    }
+    System.out.println(\"\");
+ }
+#+end_src
+
+#+name: java_matrix
+| 2 | 1 |
+| 4 | 2 |"
+    (should (equal "|col1|col2|\n|-\n|2|1\n|4|2\n"
+                   (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/inhomogeneous_table ()
+  "Read and write an inhomogeneous table."
+  (org-test-with-temp-text
+      "#+begin_src java :var a=java_table :results silent
+import java.util.List;
+import java.util.Arrays;
+List<List> b = Arrays.asList(Arrays.asList(a.get(0).get(0),
+                                           Integer.parseInt(a.get(0).get(1))*2),
+                             Arrays.asList(a.get(1).get(0),
+                                           Integer.parseInt(a.get(1).get(1))*2));
+return b;
+#+end_src
+
+#+name: java_table
+  | string | number |
+  |--------+--------|
+  | forty  |      2 |
+  | two    |      1 |"
+   (should (equal
+            '(("forty" 4) ("two" 2))
+            (org-babel-execute-src-block)))))
+
+;; imports
+
+(ert-deftest ob-java/import_library ()
+  "Import a standard java library."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent :imports java.util.Base64
+byte[] encoded = Base64.getEncoder().encode(\"42\".getBytes());
+String decoded = new String(Base64.getDecoder().decode(encoded));
+System.out.print(String.format(\"encoded=%s, decoded=%s\", new String(encoded), decoded));
+#+end_src"
+   (should (string=
+            "encoded=NDI=, decoded=42"
+            (org-babel-execute-src-block)))))
+
+(ert-deftest ob-java/import_library_inline ()
+  "Import a standard java library."
+  (org-test-with-temp-text
+      "#+begin_src java :results output silent
+import java.util.Base64;
+byte[] encoded = Base64.getEncoder().encode(\"42\".getBytes());
+String decoded = new String(Base64.getDecoder().decode(encoded));
+System.out.print(String.format(\"encoded=%s, decoded=%s\", new String(encoded), decoded));
+#+end_src"
+   (should (string=
+            "encoded=NDI=, decoded=42"
+            (org-babel-execute-src-block)))))
+
+;; tangle
+
+(ert-deftest ob-java/tangle ()
+  "Tangle a source block."
+  (org-test-with-temp-text-in-file
+      "#+begin_src java :tangle \"Tangle.java\" :results value :classname Tangle
+return \"tangled\";
+#+end_src"
+    (should
+     (string=
+      "public class Tangle {
+    public static void main(String[] args) {
+        return \"tangled\";
+    }
+}
+"
+      (unwind-protect
+          (progn (org-babel-tangle)
+                 (with-temp-buffer
+                   (insert-file-contents "Tangle.java")
+                   (untabify (point-min) (point-max))
+                   (buffer-string)))
+        (delete-file "Tangle.java"))))))
+
+(ert-deftest ob-java/tangle-with-package ()
+  "Tangle a source block."
+  (org-test-with-temp-text-in-file
+      "#+begin_src java :tangle \"tangle/Tangle.java\" :results value :classname tangle.Tangle
+return \"tangled\";
+#+end_src"
+    (should
+     (string=
+      "package tangle;
+
+public class Tangle {
+    public static void main(String[] args) {
+        return \"tangled\";
+    }
+}
+"
+      (unwind-protect
+          (progn
+            (make-directory "tangle")
+            (org-babel-tangle)
+            (with-temp-buffer
+              (insert-file-contents "tangle/Tangle.java")
+              (untabify (point-min) (point-max))
+              (buffer-string)))
+        (delete-file "tangle/Tangle.java")
+        (delete-directory "tangle"))))))
+
+
+;; specify output dir
+
+(ert-deftest ob-java/simple-dir ()
+  "Hello world program that writes output."
+  (org-test-with-temp-text
+      (format  "#+begin_src java :results output silent :dir %s
+System.out.print(42);
+#+end_src" org-babel-temporary-directory)
+    (should (string=
+             "42"
+             (unwind-protect
+                 (org-babel-execute-src-block)
+               (delete-file (concat (file-name-as-directory org-babel-temporary-directory)
+                                    "Main.java"))
+               (delete-file (concat (file-name-as-directory org-babel-temporary-directory)
+                                    "Main.class")))))))
+
+(ert-deftest ob-java/simple-dir-with-package ()
+  "Hello world program that writes output."
+  (org-test-with-temp-text
+      (format "#+begin_src java :results output silent :dir %s
+package pkg;
+
+public class Main {
+    public static void main(String[] args) {
+      System.out.print(42);
+    }
+}
+#+end_src" org-babel-temporary-directory)
+    (should (string=
+             "42"
+             (unwind-protect
+                 (org-babel-execute-src-block)
+               (delete-file (concat (file-name-as-directory org-babel-temporary-directory)
+                                    "pkg/Main.java"))
+               (delete-file (concat (file-name-as-directory org-babel-temporary-directory)
+                                    "pkg/Main.class"))
+               (delete-directory (concat (file-name-as-directory org-babel-temporary-directory)
+                                         "pkg")))))))
+
+
+;;; test-ob-java.el ends here
-- 
2.25.1


  reply	other threads:[~2020-10-09 11:16 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-10-05 12:35 [PATCH] ob-java ian martins
2020-10-05 13:23 ` ian martins
2020-10-09 11:15   ` ian martins [this message]
2020-10-20 18:28     ` John Herrlin
2020-10-20 19:17     ` John Herrlin
2020-10-21  2:37       ` ian martins
2020-10-21  5:59         ` John Herrlin
2020-10-21 12:47           ` ian martins
2020-10-21 13:54             ` John Herrlin
2020-10-22 12:23               ` ian martins
2020-10-22 12:56                 ` John Herrlin
2020-10-24 17:05     ` Kyle Meyer
2020-10-25  2:10       ` ian martins
2020-10-25  2:40         ` Kyle Meyer
2020-10-25 19:36           ` ian martins
2020-11-05 16:29             ` Jarmo Hurri
2020-11-05 17:10               ` ian martins
2020-11-06  5:21                 ` Jarmo Hurri
2020-11-06 23:00                   ` ian martins
2020-11-09 14:06                     ` Jarmo Hurri
2020-11-10 13:14                       ` ian martins
2020-11-10  6:29                     ` Jarmo Hurri
2020-11-14 11:47                       ` Jarmo Hurri
2020-11-14 15:46                         ` ian martins
2020-11-15  4:36                           ` Jarmo Hurri
2020-11-17 12:07                             ` ian martins
2020-12-14  5:55                               ` Bastien
2020-11-11  7:45                   ` Bastien
2020-10-24 11:58 ` Bastien
2020-10-25  0:30   ` ian martins
2020-10-28  9:13     ` Bastien
2020-10-31 11:03       ` ian martins

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.orgmode.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='CAC=rjb5ed68AK0umWwjphDoc2ETdcp=kqptEXun=nV+STUC9Qg@mail.gmail.com' \
    --to=ianxm@jhu.edu \
    --cc=emacs-orgmode@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).