branch: scratch/javaimp-gradle commit edc0e55b4292985fa0ceaf47dc767fbb524f6e8d Author: Filipp Gunbin <fgun...@fastmail.fm> Commit: Filipp Gunbin <fgun...@fastmail.fm>
packages/javaimp: Support gradle (wip) --- packages/javaimp/javaimp-gradle.el | 135 ++++++++++++++ packages/javaimp/javaimp-maven.el | 170 +++++++++++++++++ packages/javaimp/javaimp-tests.el | 14 +- packages/javaimp/javaimp-util.el | 131 +++++++++++++ packages/javaimp/javaimp.el | 366 +++++++------------------------------ 5 files changed, 505 insertions(+), 311 deletions(-) diff --git a/packages/javaimp/javaimp-gradle.el b/packages/javaimp/javaimp-gradle.el new file mode 100644 index 0000000..2bf8e8e --- /dev/null +++ b/packages/javaimp/javaimp-gradle.el @@ -0,0 +1,135 @@ +;;; javaimp-gradle.el --- javaimp gradle support -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; Author: Filipp Gunbin <fgun...@fastmail.fm> +;; Maintainer: Filipp Gunbin <fgun...@fastmail.fm> +;; Version: 0.6.1 + +;; 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/>. + +(require 'javaimp-util) + +(defcustom javaimp-gradle-program "gradle" + "Path to the `gradle' program. Customize it if the program is +not on `exec-path'.") + +(defun javaimp--gradle-visit (file) + "Calls gradle on FILE to get various project information. + +Passes specially crafted init file as -I argument to gradle and +invokes task contained in it. This task returns all needed +information." + (message "Visiting Gradle build file %s..." file) + (let* ((init-file (make-temp-file "javaimp" nil ".kts" + javaimp--gradle-init-file-contents)) + (modules + (javaimp--call-build-tool javaimp-gradle-program + #'javaimp--gradle-handler + "-q" + "-b" (javaimp-cygpath-convert-maybe file) + "-I" (javaimp-cygpath-convert-maybe init-file) + "javaimpTask"))) + (prog1 + ;; first module is always root + (javaimp--build-tree (car modules) nil modules)) + (message "Loaded tree for %s" file))) + +(defun javaimp--gradle-handler () + (goto-char (point-min)) + (let (modules alist pair sym val) + (while (not (eobp)) + (setq pair (split-string (thing-at-point 'line) "=")) + (unless (= (length pair) 2) + (error "Invalid pair from gradle output: %s" pair)) + (setq sym (intern (car pair)) + val (cadr pair)) + (when (and (eq sym 'id) alist) ;start of next module + (push (javaimp--gradle-module-from-alist alist) modules) + (setq alist nil)) + (push (cons sym val) alist) + (forward-line 1)) + (when alist ;last module + (push (javaimp--gradle-module-from-alist alist) modules)) + modules)) + +(defun javaimp--gradle-module-from-alist (alist) + (make-javaimp-module + :id (javaimp--gradle-id-from-colon-separated (cadr (assq 'id alist))) + :parent-id (javaimp--gradle-id-from-colon-separated (cadr (assq 'parent-id alist))) + :file (cadr (assq 'file alist)) + :final-name (cadr (assq 'final-name alist)) + :packaging "jar" ;TODO + :source-dir (file-name-as-directory + (javaimp-cygpath-convert-maybe + (cadr (assq 'source-dir alist)))) + :test-source-dir (file-name-as-directory + (javaimp-cygpath-convert-maybe + (cadr (assq 'test-source-dir alist)))) + :build-dir (file-name-as-directory + (javaimp-cygpath-convert-maybe + (cadr (assq 'build-dir alist)))) + :dep-jars (javaimp--split-native-path (cadr (assq 'dep-jars alist))) + :load-ts (current-time) + :dep-jars-path-fetcher #'javaimp--gradle-fetch-dep-jars-path)) + +(defun javaimp--gradle-id-from-colon-separated (str) + (when str + (let ((parts (split-string str ":"))) + (unless (= (length parts) 3) + (error "Invalid maven id: %s" str)) + (make-javaimp-id :group (nth 0 parts) :artifact (nth 1 parts) :version (nth 2 parts))))) + + +(defun javaimp--gradle-fetch-dep-jars-path (file) + (let ((init-file (make-temp-file "javaimp" nil ".kts" + javaimp--gradle-init-file-contents-dep-jars-only))) + (javaimp--call-build-tool javaimp-gradle-program + (lambda () + ;; expect just a single line + (thing-at-point 'line)) + "-q" + "-p" (javaimp-cygpath-convert-maybe file) + "-I" (javaimp-cygpath-convert-maybe init-file) + "javaimpTask"))) + + +(defconst javaimp--gradle-init-file-contents + "allprojects { + tasks.register(\"javaimpTask\") { + doLast { + println(\"id=$project.group:$project.name:$project.version\") + if (project.parent != null) { + println(\"parent-id=${project.parent.group}:${project.parent.name}:${project.parent.version}\") + } + println(\"file=${project.buildFile}\") + println(\"final-name=${project.archivesBaseName}\") + println(\"source-dir=${sourceSets.main.java.sourceDirectories.asPath}\") + println(\"test-source-dir=${sourceSets.test.java.sourceDirectories.asPath}\") + println(\"build-dir=${project.buildDir}\") + println(\"dep-jars=${configurations.testCompileClasspath.asPath}\") + } + } +}") + +(defconst javaimp--gradle-init-file-contents-dep-jars-only + "allprojects { + tasks.register(\"javaimpTask\") { + doLast { + println(\"${configurations.testCompileClasspath.asPath}\") + } + } +}") + +(provide 'javaimp-gradle) diff --git a/packages/javaimp/javaimp-maven.el b/packages/javaimp/javaimp-maven.el new file mode 100644 index 0000000..ee63acb --- /dev/null +++ b/packages/javaimp/javaimp-maven.el @@ -0,0 +1,170 @@ +;;; javaimp-maven.el --- javaimp maven support -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; Author: Filipp Gunbin <fgun...@fastmail.fm> +;; Maintainer: Filipp Gunbin <fgun...@fastmail.fm> +;; Version: 0.6.1 + +;; 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 'javaimp-util) + +(defcustom javaimp-mvn-program "mvn" + "Path to the `mvn' program. Customize it if the program is not +on `exec-path'.") + + +(defun javaimp--maven-visit (file) + "Calls `mvn help:effective-pom' on FILE, +reads project structure from the output and records which files +belong to which modules and other module information" + (message "Visiting Maven POM file %s..." file) + (let* ((xml-tree (javaimp--call-build-tool javaimp-mvn-program + #'javaimp--maven-effective-pom-handler + "-f" (javaimp-cygpath-convert-maybe file) + "help:effective-pom")) + (projects (javaimp--maven-projects-from-xml xml-tree)) + (modules (mapcar #'javaimp--maven-module-from-xml projects)) + ;; first module is always root + (tree (javaimp--build-tree (car modules) nil modules))) + (when tree + ;; Set files in a separate step after building the tree because "real" + ;; parent of a child (given by <parent>) does not necessary contains the + ;; child in its <modules>. This is rare, but happens. + (javaimp--maven-fill-modules-files file tree) + ;; check that no :file slot is empty + (let ((modules-without-files + (mapcar #'javaimp-node-contents + (javaimp--select-nodes-from-tree + tree (lambda (m) + (null (javaimp-module-file m))))))) + (if modules-without-files + (error "Cannot find file for module(s): %s" + (mapconcat #'javaimp-module-id modules-without-files ", ")))) + tree))) + +(defun javaimp--maven-effective-pom-handler () + (let ((start + (save-excursion + (progn + (goto-char (point-min)) + (re-search-forward "<\\?xml\\|<projects?") + (match-beginning 0)))) + (end + (save-excursion + (progn + (goto-char (point-min)) + (re-search-forward "<\\(projects?\\)") + ;; corresponding close tag is the end of parse region + (search-forward (concat "</" (match-string 1) ">")) + (match-end 0))))) + (xml-parse-region start end))) + +(defun javaimp--maven-projects-from-xml (tree) + "Analyzes result of `mvn help:effective-pom' and returns list +of <project> elements" + (let ((project (assq 'project tree)) + (projects (assq 'projects tree))) + (cond (project + (list project)) + (projects + (javaimp--xml-children projects 'project)) + (t + (error "Neither <project> nor <projects> was found in pom"))))) + +(defun javaimp--maven-module-from-xml (elt) + (let ((build-elt (javaimp--xml-child 'build elt))) + (make-javaimp-module + :id (javaimp--maven-id-from-xml elt) + :parent-id (javaimp--maven-id-from-xml (javaimp--xml-child 'parent elt)) + ;; <project> element does not contain pom file path, so we set this slot + ;; later, see javaimp--maven-fill-modules-files + :file nil + :final-name (javaimp--xml-first-child + (javaimp--xml-child 'finalName build-elt)) + :packaging (javaimp--xml-first-child + (javaimp--xml-child 'packaging elt)) + :source-dir (file-name-as-directory + (javaimp-cygpath-convert-maybe + (javaimp--xml-first-child + (javaimp--xml-child 'sourceDirectory build-elt)))) + :test-source-dir (file-name-as-directory + (javaimp-cygpath-convert-maybe + (javaimp--xml-first-child + (javaimp--xml-child 'testSourceDirectory build-elt)))) + :build-dir (file-name-as-directory + (javaimp-cygpath-convert-maybe + (javaimp--xml-first-child (javaimp--xml-child 'directory build-elt)))) + :dep-jars nil ; dep-jars is initialized lazily on demand + :load-ts (current-time) + :dep-jars-path-fetcher #'javaimp--maven-fetch-dep-jars-path))) + +(defun javaimp--maven-id-from-xml (elt) + (make-javaimp-id + :group (javaimp--xml-first-child (javaimp--xml-child 'groupId elt)) + :artifact (javaimp--xml-first-child (javaimp--xml-child 'artifactId elt)) + :version (javaimp--xml-first-child (javaimp--xml-child 'version elt)))) + +(defun javaimp--maven-fill-modules-files (file tree) + ;; Reads module id from FILE, looks up corresponding module in TREE, sets its + ;; :file slot, then recurses for each submodule. A submodule file path is + ;; constructed by appending relative path taken from <module> to FILE's + ;; directory. + (let* ((xml-tree (with-temp-buffer + (insert-file-contents file) + (xml-parse-region (point-min) (point-max)))) + (project-elt (assq 'project xml-tree)) + (this-id (javaimp--maven-id-from-xml project-elt)) + ;; seems that the only mandatory component in tested ids is artifact, while + ;; group and version may be inherited and thus not presented in pom.xml + (id-pred (if (or (null (javaimp-id-group this-id)) + (null (javaimp-id-version this-id))) + (progn + (message "File %s contains incomplete id, will check artifact only" file) + (lambda (tested-id) + (equal (javaimp-id-artifact this-id) + (javaimp-id-artifact tested-id)))) + (lambda (tested-id) + (equal this-id tested-id)))) + (module + (javaimp-node-contents + (or (javaimp--find-node-in-tree + tree (lambda (m) + (funcall id-pred (javaimp-module-id m)))) + (error "Cannot find module for id %s (taken from file %s)" this-id file))))) + (setf (javaimp-module-file module) file) + (let ((rel-paths + (mapcar #'javaimp--xml-first-child + (javaimp--xml-children (javaimp--xml-child 'modules project-elt) 'module)))) + (dolist (rel-path rel-paths) + (javaimp--maven-fill-modules-files (concat (file-name-directory file) + (file-name-as-directory rel-path) + "pom.xml") + tree))))) + +(defun javaimp--maven-fetch-dep-jars-path (file) + (javaimp--call-build-tool javaimp-mvn-program + (lambda () + (goto-char (point-min)) + (search-forward "Dependencies classpath:") + (forward-line 1) + (thing-at-point 'line)) + "-f" (javaimp-cygpath-convert-maybe file) + "dependency:build-classpath")) + +(provide 'javaimp-maven) diff --git a/packages/javaimp/javaimp-tests.el b/packages/javaimp/javaimp-tests.el index cd8acb2..0dbe86f 100644 --- a/packages/javaimp/javaimp-tests.el +++ b/packages/javaimp/javaimp-tests.el @@ -1,23 +1,23 @@ -;;; javaimp-tests.el --- javaimp module tests -*- lexical-binding: t; -*- +;;; javaimp-tests.el --- javaimp tests -*- lexical-binding: t; -*- -;; Copyright (C) 2016 Free Software Foundation, Inc. +;; Copyright (C) 2016-2019 Free Software Foundation, Inc. ;; Author: Filipp Gunbin <fgun...@fastmail.fm> ;; Maintainer: Filipp Gunbin <fgun...@fastmail.fm> (require 'ert) -(require 'javaimp) +(require 'javaimp-maven) -(ert-deftest javaimp-test--maven-xml-extract-projects--project () +(ert-deftest javaimp-test--maven-projects-from-xml--project () (with-temp-buffer (insert "<project/>") - (let ((projects (javaimp--maven-xml-extract-projects + (let ((projects (javaimp--maven-projects-from-xml (xml-parse-region (point-min) (point-max))))) (should (eql (length projects) 1))))) -(ert-deftest javaimp-test--maven-xml-extract-projects--projects () +(ert-deftest javaimp-test--maven-projects-from-xml--projects () (with-temp-buffer (insert "<projects><project/><project/></projects>") - (let ((projects (javaimp--maven-xml-extract-projects + (let ((projects (javaimp--maven-projects-from-xml (xml-parse-region (point-min) (point-max))))) (should (eql (length projects) 2))))) diff --git a/packages/javaimp/javaimp-util.el b/packages/javaimp/javaimp-util.el new file mode 100644 index 0000000..67e9786 --- /dev/null +++ b/packages/javaimp/javaimp-util.el @@ -0,0 +1,131 @@ +;;; javaimp-util.el --- javaimp util -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; Author: Filipp Gunbin <fgun...@fastmail.fm> +;; Maintainer: Filipp Gunbin <fgun...@fastmail.fm> +;; Version: 0.6.1 + +;; 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 'xml) + +(defcustom javaimp-cygpath-program + (if (eq system-type 'cygwin) "cygpath") + "Path to the `cygpath' program (Cygwin only). Customize it if +the program is not on `exec-path'.") + + +(defun javaimp--xml-children (xml-tree child-name) + "Returns list of children of XML-TREE filtered by CHILD-NAME" + (seq-filter (lambda (child) + (and (consp child) + (eq (car child) child-name))) + (cddr xml-tree))) + +(defun javaimp--xml-child (name el) + "Returns a child of EL named by symbol NAME" + (assq name (cddr el))) + +(defun javaimp--xml-first-child (el) + "Returns a first child of EL" + (car (cddr el))) + + +(defun javaimp--get-file-ts (file) + (nth 5 (file-attributes file))) + + +(defun javaimp--get-jdk-jars () + (and javaimp-java-home + (file-accessible-directory-p javaimp-java-home) + (let ((lib-dir + (concat (file-name-as-directory javaimp-java-home) + (file-name-as-directory "jre") + (file-name-as-directory "lib")))) + (directory-files lib-dir t "\\.jar\\'")))) + + +;; TODO use functions `cygwin-convert-file-name-from-windows' and +;; `cygwin-convert-file-name-to-windows' when they are available +;; instead of calling `cygpath'. See +;; https://cygwin.com/ml/cygwin/2013-03/msg00228.html + +(defun javaimp-cygpath-convert-maybe (path &optional mode is-really-path) + "On Cygwin, converts PATH using cygpath according to MODE and +IS-REALLY-PATH. If MODE is `unix' (the default), adds -u switch. +If MODE is `windows', adds -m switch. If `is-really-path' is +non-nil, adds `-p' switch. On other systems, PATH is returned +unchanged." + (if (eq system-type 'cygwin) + (progn + (unless mode (setq mode 'unix)) + (let (args) + (push (cond ((eq mode 'unix) "-u") + ((eq mode 'windows) "-m") + (t (error "Invalid mode: %s" mode))) + args) + (and is-really-path (push "-p" args)) + (push path args) + (car (apply #'process-lines javaimp-cygpath-program args)))) + path)) + + +(defun javaimp--call-build-tool (program handler &rest args) + "Runs PROGRAM with ARGS, then calls HANDLER in the temporary +buffer and returns its result" + (message "Calling \"%s %s\" on args: %s" program target args) + (with-temp-buffer + (let ((status (let ((coding-system-for-read + (if (eq system-type 'cygwin) 'utf-8-dos))) + ;; TODO check in output on Gnu/Linux + `(process-file ,program nil t nil ,@args))) + (buf (current-buffer))) + (with-current-buffer (get-buffer-create javaimp-debug-buf-name) + (erase-buffer) + (insert-buffer-substring buf)) + (or (and (numberp status) (= status 0)) + (error "Build tool target \"%s\" failed with status \"%s\"" target status)) + (goto-char (point-min)) + (funcall handler)))) + +(defun javaimp--split-native-path (path) + (let ((converted (javaimp-cygpath-convert-maybe path 'unix t)) + (sep-regex (concat "[" path-separator "\n" "]+"))) + (split-string converted sep-regex t))) + +(defun javaimp--build-tree (this parent-node all) + (message "Building tree for module: %s" (javaimp-module-id this)) + (let ((children + ;; more or less reliable way to find children is to look for + ;; modules with "this" as the parent + (seq-filter (lambda (m) + (equal (javaimp-module-parent-id m) (javaimp-module-id this))) + all))) + (let* ((this-node (make-javaimp-node + :parent parent-node + :children nil + :contents this)) + ;; recursively build child nodes + (child-nodes + (mapcar (lambda (child) + (javaimp--build-tree child this-node all)) + children))) + (setf (javaimp-node-children this-node) child-nodes) + this-node))) + +(provide 'javaimp-util) diff --git a/packages/javaimp/javaimp.el b/packages/javaimp/javaimp.el index 8a3f4f3..29e3f2a 100644 --- a/packages/javaimp/javaimp.el +++ b/packages/javaimp/javaimp.el @@ -1,11 +1,11 @@ -;;; javaimp.el --- Add and reorder Java import statements in Maven projects -*- lexical-binding: t; -*- +;;; javaimp.el --- Add and reorder Java import statements in Maven/Gradle projects -*- lexical-binding: t; -*- -;; Copyright (C) 2014-2018 Free Software Foundation, Inc. +;; Copyright (C) 2014-2019 Free Software Foundation, Inc. ;; Author: Filipp Gunbin <fgun...@fastmail.fm> ;; Maintainer: Filipp Gunbin <fgun...@fastmail.fm> ;; Version: 0.6.1 -;; Keywords: java, maven, programming +;; Keywords: java, maven, gradle, programming ;; 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 @@ -22,13 +22,13 @@ ;;; Commentary: -;; Allows to manage Java import statements in Maven projects. +;; Allows to manage Java import statements in Maven/Gradle projects. ;; ;; Quick start: ;; ;; - customize `javaimp-import-group-alist' -;; - call `javaimp-maven-visit-project', giving it the top-level project -;; directory where pom.xml resides +;; - call `javaimp-visit-project', giving it the top-level project +;; directory where pom.xml / build.gradle.kts resides ;; ;; Then in a Java buffer visiting a file under that project or one of its ;; submodules call `javaimp-organize-imports' or `javaimp-add-import'. @@ -38,21 +38,22 @@ ;; ;; Some details: ;; -;; If Maven failed, you can see its output in the buffer named by -;; `javaimp-debug-buf-name' (default is "*javaimp-debug*"). +;; If Maven/Gradle failed, you can see its output in the buffer named +;; by `javaimp-debug-buf-name' (default is "*javaimp-debug*"). ;; -;; Contents of jar files and Maven project structures (pom.xml) are cached, -;; so usually only the first command should take a considerable amount of -;; time to complete. If a module's pom.xml or any of its parents' pom.xml -;; (within visited tree) was modified after information was loaded, `mvn -;; dependency:build-classpath' is re-run on the current module. If a jar -;; file was changed, its contents are re-read. +;; Contents of jar files and Maven/Gradle project structures are +;; cached, so usually only the first command should take a +;; considerable amount of time to complete. If a module's build file +;; or any of its parents' build files (within visited tree) was +;; modified after information was loaded, dependencies are fetched +;; from the build tool again. If a jar file was changed, its contents +;; are re-read. ;; ;; Currently inner classes are filtered out from completion alternatives. ;; You can always import top-level class and use qualified name. ;; ;; -;; Example of initialization: +;; Example: ;; ;; (require 'javaimp) ;; @@ -66,32 +67,25 @@ ;; (local-set-key "\C-ci" 'javaimp-add-import) ;; (local-set-key "\C-co" 'javaimp-organize-imports))) ;; -;; -;; TODO: -;; -;; - use functions `cygwin-convert-file-name-from-windows' and -;; `cygwin-convert-file-name-to-windows' when they are available instead of -;; calling `cygpath'. See https://cygwin.com/ml/cygwin/2013-03/msg00228.html -;; -;; - save/restore state, on restore check if a root exists and delete it if -;; not -;; -;; - `javaimp-add-import': without prefix arg narrow alternatives by local name; -;; with prefix arg include all classes in alternatives -;; -;; - :type for defcustom + ;;; Code: (require 'cl-lib) (require 'seq) -(require 'xml) +(require 'javaimp-util) +(require 'javaimp-maven) +(require 'javaimp-gradle) + ;; User options +;; TODO add :type for defcustoms + (defgroup javaimp () - "Add and reorder Java import statements in Maven projects" + "Add and reorder Java import statements in Maven/Gradle +projects" :group 'c) (defcustom javaimp-import-group-alist '(("\\`java\\." . 10) ("\\`javax\\." . 15)) @@ -131,15 +125,6 @@ of the leading slash. Custom values set in plugin configuration in pom.xml are not supported yet.") -(defcustom javaimp-mvn-program "mvn" - "Path to the `mvn' program. Customize it if the program is not -on `exec-path'.") - -(defcustom javaimp-cygpath-program - (if (eq system-type 'cygwin) "cygpath") - "Path to the `cygpath' program (Cygwin only). Customize it if -the program is not on `exec-path'.") - (defcustom javaimp-jar-program "jar" "Path to the `jar' program used to read contents of jar files. Customize it if the program is not on `exec-path'.") @@ -174,7 +159,8 @@ to the completion alternatives list.") packaging source-dir test-source-dir build-dir dep-jars - load-ts) + load-ts + dep-jars-path-fetcher) (cl-defstruct javaimp-id group artifact version) @@ -182,262 +168,46 @@ to the completion alternatives list.") (cl-defstruct javaimp-cached-jar file read-ts classes) - -;; Utilities - -(defun javaimp--xml-children (xml-tree child-name) - "Returns list of children of XML-TREE filtered by CHILD-NAME" - (seq-filter (lambda (child) - (and (consp child) - (eq (car child) child-name))) - (cddr xml-tree))) - -(defun javaimp--xml-child (name el) - "Returns a child of EL named by symbol NAME" - (assq name (cddr el))) - -(defun javaimp--xml-first-child (el) - "Returns a first child of EL" - (car (cddr el))) - -(defun javaimp--get-file-ts (file) - (nth 5 (file-attributes file))) - -(defun javaimp--get-jdk-jars () - (and javaimp-java-home - (file-accessible-directory-p javaimp-java-home) - (let ((lib-dir - (concat (file-name-as-directory javaimp-java-home) - (file-name-as-directory "jre") - (file-name-as-directory "lib")))) - (directory-files lib-dir t "\\.jar\\'")))) - -(defun javaimp-cygpath-convert-maybe (path &optional mode is-really-path) - "On Cygwin, converts PATH using cygpath according to MODE and -IS-REALLY-PATH. If MODE is `unix' (the default), adds -u switch. -If MODE is `windows', adds -m switch. If `is-really-path' is -non-nil, adds `-p' switch. On other systems, PATH is returned -unchanged." - (if (eq system-type 'cygwin) - (progn - (unless mode (setq mode 'unix)) - (let (args) - (push (cond ((eq mode 'unix) "-u") - ((eq mode 'windows) "-m") - (t (error "Invalid mode: %s" mode))) - args) - (and is-really-path (push "-p" args)) - (push path args) - (car (apply #'process-lines javaimp-cygpath-program args)))) - path)) -;; Project loading - ;;;###autoload -(defun javaimp-maven-visit-project (path) - "Loads a project and its submodules. PATH should point to a -directory containing pom.xml. - -Calls `mvn help:effective-pom' on the pom.xml in the PATH, reads -project structure from the output and records which files belong -to which modules and other module information. +(defun javaimp-visit-project (dir) + "Loads a project and its submodules. DIR should point to a +directory containing pom.xml / build.gradle.kts. After being processed by this command, the module tree becomes -known to javaimp and `javaimp-add-import' maybe called inside any -module file." - (interactive "DVisit maven project in directory: ") - (let ((file (expand-file-name - (concat (file-name-as-directory path) "pom.xml")))) - (unless (file-readable-p file) - (error "Cannot read file: %s" file)) - ;; delete previous loaded tree, if any - (setq javaimp-project-forest - (seq-remove (lambda (tree) - (equal (javaimp-module-file (javaimp-node-contents tree)) - file)) - javaimp-project-forest)) - (message "Loading file %s..." file) - (let* ((xml-tree - (javaimp--maven-call file "help:effective-pom" - #'javaimp--maven-xml-effective-pom-handler)) - (projects (javaimp--maven-xml-extract-projects xml-tree)) - (modules (mapcar #'javaimp--maven-xml-parse-project projects)) - ;; first module is always root - (tree (javaimp--maven-build-tree (car modules) nil modules))) - (when tree - ;; Set files in a separate step after building the tree because "real" - ;; parent of a child (given by <parent>) does not necessary contains the - ;; child in its <modules>. This is rare, but happens. - (javaimp--maven-fill-modules-files file tree) - ;; check that no :file slot is empty - (let ((modules-without-files - (mapcar #'javaimp-node-contents - (javaimp--select-nodes-from-tree - tree (lambda (m) - (null (javaimp-module-file m))))))) - (if modules-without-files - (error "Cannot find file for module(s): %s" - (mapconcat #'javaimp-module-id modules-without-files ", ")))) - (push tree javaimp-project-forest))) - (message "Loaded tree for %s" file))) +known to javaimp and `javaimp-add-import' may be called inside +any module file." + (interactive "DVisit Maven / Gradle project in directory: ") + (let* ((exp-dir (expand-file-name (file-name-as-directory dir))) + build-file + (tree (cond + ((file-readable-p (setq build-file (concat exp-dir "pom.xml"))) + (javaimp--maven-visit build-file)) + ((file-readable-p (setq build-file (concat exp-dir "build.gradle.kts"))) + (javaimp--gradle-visit build-file)) + (error "Could not find build file in dir %s" dir)))) + (when tree + ;; delete previous loaded tree, if any + (setq javaimp-project-forest + (seq-remove (lambda (tree) + (equal (javaimp-module-file (javaimp-node-contents tree)) + build-file)) + javaimp-project-forest)) + (push tree javaimp-project-forest) + (message "Loaded tree for %s" dir)))) -;; Maven XML routines - -(defun javaimp--maven-xml-effective-pom-handler () - (let ((start - (save-excursion - (progn - (goto-char (point-min)) - (re-search-forward "<\\?xml\\|<projects?") - (match-beginning 0)))) - (end - (save-excursion - (progn - (goto-char (point-min)) - (re-search-forward "<\\(projects?\\)") - ;; corresponding close tag is the end of parse region - (search-forward (concat "</" (match-string 1) ">")) - (match-end 0))))) - (xml-parse-region start end))) - -(defun javaimp--maven-xml-extract-projects (xml-tree) - "Analyzes result of `mvn help:effective-pom' and returns list -of <project> elements" - (let ((project (assq 'project xml-tree)) - (projects (assq 'projects xml-tree))) - (cond (project - (list project)) - (projects - (javaimp--xml-children projects 'project)) - (t - (error "Neither <project> nor <projects> was found in pom"))))) - -(defun javaimp--maven-xml-parse-project (project) - (let ((build-elt (javaimp--xml-child 'build project))) - (make-javaimp-module - :id (javaimp--maven-xml-extract-id project) - :parent-id (javaimp--maven-xml-extract-id (javaimp--xml-child 'parent project)) - ;; <project> element does not contain pom file path, so we set this slot - ;; later, see javaimp--maven-fill-modules-files - :file nil - :final-name (javaimp--xml-first-child - (javaimp--xml-child 'finalName build-elt)) - :packaging (javaimp--xml-first-child - (javaimp--xml-child 'packaging project)) - :source-dir (file-name-as-directory - (javaimp-cygpath-convert-maybe - (javaimp--xml-first-child - (javaimp--xml-child 'sourceDirectory build-elt)))) - :test-source-dir (file-name-as-directory - (javaimp-cygpath-convert-maybe - (javaimp--xml-first-child - (javaimp--xml-child 'testSourceDirectory build-elt)))) - :build-dir (file-name-as-directory - (javaimp-cygpath-convert-maybe - (javaimp--xml-first-child (javaimp--xml-child 'directory build-elt)))) - :dep-jars nil ; dep-jars is initialized lazily on demand - :load-ts (current-time)))) - -(defun javaimp--maven-xml-extract-id (elt) - (make-javaimp-id - :group (javaimp--xml-first-child (javaimp--xml-child 'groupId elt)) - :artifact (javaimp--xml-first-child (javaimp--xml-child 'artifactId elt)) - :version (javaimp--xml-first-child (javaimp--xml-child 'version elt)))) +;; Dep jars - - -;; Maven routines - -(defun javaimp--maven-call (pom-file target handler) - "Runs Maven target TARGET on POM-FILE, then calls HANDLER in -the temporary buffer and returns its result" - (message "Calling \"mvn %s\" on pom: %s" target pom-file) - (with-temp-buffer - (let* ((pom-file (javaimp-cygpath-convert-maybe pom-file)) - (status - ;; TODO check in Maven output on Gnu/Linux - (let ((coding-system-for-read - (if (eq system-type 'cygwin) 'utf-8-dos))) - (process-file javaimp-mvn-program nil t nil "-f" pom-file target))) - (buf (current-buffer))) - (with-current-buffer (get-buffer-create javaimp-debug-buf-name) - (erase-buffer) - (insert-buffer-substring buf)) - (or (and (numberp status) (= status 0)) - (error "Maven target \"%s\" failed with status \"%s\"" target status)) - (goto-char (point-min)) - (funcall handler)))) - -(defun javaimp--maven-build-tree (this parent-node all) - (message "Building tree for module: %s" (javaimp-module-id this)) - (let ((children - ;; reliable way to find children is to look for modules with "this" as - ;; the parent - (seq-filter (lambda (m) - (equal (javaimp-module-parent-id m) (javaimp-module-id this))) - all))) - (let* ((this-node (make-javaimp-node - :parent parent-node - :children nil - :contents this)) - ;; recursively build child nodes - (child-nodes - (mapcar (lambda (child) - (javaimp--maven-build-tree child this-node all)) - children))) - (setf (javaimp-node-children this-node) child-nodes) - this-node))) - -(defun javaimp--maven-fill-modules-files (file tree) - ;; Reads module id from FILE, looks up corresponding module in TREE, sets its - ;; :file slot, then recurses for each submodule. A submodule file path is - ;; constructed by appending relative path taken from <module> to FILE's - ;; directory. - (let* ((xml-tree (with-temp-buffer - (insert-file-contents file) - (xml-parse-region (point-min) (point-max)))) - (project-elt (assq 'project xml-tree)) - (this-id (javaimp--maven-xml-extract-id project-elt)) - ;; seems that the only mandatory component in tested ids is artifact, while - ;; group and version may be inherited and thus not presented in pom.xml - (id-pred (if (or (null (javaimp-id-group this-id)) - (null (javaimp-id-version this-id))) - (progn - (message "File %s contains incomplete id, will check artifact only" file) - (lambda (tested-id) - (equal (javaimp-id-artifact this-id) - (javaimp-id-artifact tested-id)))) - (lambda (tested-id) - (equal this-id tested-id)))) - (module - (javaimp-node-contents - (or (javaimp--find-node-in-tree - tree (lambda (m) - (funcall id-pred (javaimp-module-id m)))) - (error "Cannot find module for id %s (taken from file %s)" this-id file))))) - (setf (javaimp-module-file module) file) - (let ((rel-paths - (mapcar #'javaimp--xml-first-child - (javaimp--xml-children (javaimp--xml-child 'modules project-elt) 'module)))) - (dolist (rel-path rel-paths) - (javaimp--maven-fill-modules-files (concat (file-name-directory file) - (file-name-as-directory rel-path) - "pom.xml") - tree))))) - - -;;; Loading dep-jars - -(defun javaimp--maven-update-module-maybe (node) +(defun javaimp--update-module-maybe (node) (let ((module (javaimp-node-contents node)) need-update) ;; check if deps are initialized (or (javaimp-module-dep-jars module) (progn (message "Loading dependencies: %s" (javaimp-module-id module)) (setq need-update t))) - ;; check if any pom up to the top one has changed + ;; check if any build-file up to the top one has changed (let ((tmp node)) (while (and tmp (not need-update)) @@ -445,32 +215,18 @@ the temporary buffer and returns its result" (if (> (float-time (javaimp--get-file-ts (javaimp-module-file checked))) (float-time (javaimp-module-load-ts module))) (progn - (message "Reloading %s (pom changed)" (javaimp-module-id checked)) + (message "Reloading dependencies for %s (build-file changed)" + (javaimp-module-id checked)) (setq need-update t)))) (setq tmp (javaimp-node-parent tmp)))) (when need-update - (let* ((new-dep-jars (javaimp--maven-fetch-dep-jars module)) + (let* ((path (funcall (javaimp-module-dep-jars-path-fetcher module) + (javaimp-module-file module))) + (new-dep-jars (javaimp--split-native-path path)) (new-load-ts (current-time))) (setf (javaimp-module-dep-jars module) new-dep-jars) (setf (javaimp-module-load-ts module) new-load-ts))))) -(defun javaimp--maven-fetch-dep-jars (module) - (let* ((path (javaimp--maven-call (javaimp-module-file module) - "dependency:build-classpath" - #'javaimp--maven-build-classpath-handler)) - (converted-path (javaimp-cygpath-convert-maybe path 'unix t)) - (path-separator-regex (concat "[" path-separator "\n" "]+"))) - (split-string converted-path path-separator-regex t))) - -(defun javaimp--maven-build-classpath-handler () - (goto-char (point-min)) - (search-forward "Dependencies classpath:") - (forward-line 1) - (thing-at-point 'line)) - - -;; Working with jar classes - (defun javaimp--get-jar-classes (file) (let ((cached (cdr (assoc file javaimp-cached-jars)))) (cond ((null cached) @@ -559,6 +315,8 @@ the temporary buffer and returns its result" ;;; Adding imports +;; TODO narrow alternatives by class local name + ;;;###autoload (defun javaimp-add-import (classname) "Imports classname in the current file. Interactively, @@ -575,7 +333,7 @@ classes in the current module." (or (string-prefix-p (javaimp-module-source-dir m) file) (string-prefix-p (javaimp-module-test-source-dir m) file)))) (error "Cannot find module by file: %s" file)))) - (javaimp--maven-update-module-maybe node) + (javaimp--update-module-maybe node) (let ((module (javaimp-node-contents node))) (list (completing-read "Import: " @@ -694,7 +452,7 @@ is `ordinary' or `static'. Interactively, NEW-IMPORTS is nil." (string< (caar first) (caar second)) (< (cdr first) (cdr second)))))) (javaimp--insert-imports with-order))) - (message "Nothing to organize!"))))) + (message "Nothing to organize!"))))) (defun javaimp--parse-imports () "Returns (FIRST LAST . IMPORTS)"