From f213965b93b982d406404529503ed3346c983dac Mon Sep 17 00:00:00 2001 From: Filipp Gunbin Date: Fri, 18 Mar 2016 20:33:00 +0300 Subject: [PATCH] packages/javaimp: full rewrite * Use defstruct's from cl-lib. * Parse pom.xml structure into tree for correct updates. * Clean up docs. * Simplify code. --- packages/javaimp/javaimp.el | 1200 +++++++++++++++++------------------ 1 file changed, 597 insertions(+), 603 deletions(-) diff --git a/packages/javaimp/javaimp.el b/packages/javaimp/javaimp.el index df402cea3..a32a9a0a1 100644 --- a/packages/javaimp/javaimp.el +++ b/packages/javaimp/javaimp.el @@ -1,79 +1,56 @@ ;;; javaimp.el --- Add and reorder Java import statements in Maven projects -*- lexical-binding: t; -*- -;; Copyright (C) 2014, 2015 Free Software Foundation, Inc. +;; Copyright (C) 2014, 2015, 2016 Free Software Foundation, Inc. ;; Author: Filipp Gunbin ;; Maintainer: Filipp Gunbin -;; Version: 0.6 +;; Version: 0.7 ;; Keywords: java, maven, programming ;;; Commentary: ;; Allows to manage Java import statements in Maven projects. ;; -;; Quick start: customize `javaimp-import-group-alist', `javaimp-jdk-home' -;; and call `javaimp-maven-visit-root', then in a Java buffer visiting a -;; file under that module or one of its submodules call -;; `javaimp-organize-imports' or `javaimp-add-import'. `javaimp-add-import' -;; will provide you a helpful completion, and the default value (the one -;; you'll get if you hit `M-n' in the minibuffer) is the symbol under point, -;; so usually it's enough to hit `M-n', then add some starting letters of a -;; package and hit `TAB'. The module does not add all needed imports -;; automatically! It only helps you to quickly add imports when stepping -;; through compilation errors. +;; Quick start: ;; +;; - customize `javaimp-import-group-alist' +;; +;; - call `javaimp-maven-visit-project', giving it the top-level project +;; directory where pom.xml 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'. +;; +;; This module does not add all needed imports automatically! It only helps +;; you to quickly add imports when stepping through compilation errors. +;; +;; +;; Some details: +;; ;; If Maven 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 first command should take a considerable amount of time -;; to complete. When it is detected that a particular jar or pom.xml file's -;; timestamp changed, it is re-read and cache is updated. +;; to complete. If a modules's pom.xml or any of its parents' pom.xml was +;; changed (i.e. any of them 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. ;; -;; Details on variables. -;; -;; `javaimp-import-group-alist' defines the order of import statement -;; groups. By default java.* and javax.* imports are assigned an order of -;; 10, which is low, so it puts those imports at the beginning. Your -;; project's imports typically should come after, so the sample config below -;; sets 80 for them. -;; -;; `javaimp-jdk-home' is a path for JDK. It is used to scan JDK jars. -;; Usually you will need to set this. +;; If you make some changes which change project hierarchy, you should +;; re-visit the parent again with `javaimp-maven-visit-project'. ;; -;; `javaimp-additional-source-dirs' is a list specifying directories where -;; additional (e.g. generated) source files reside. Each directory is a -;; relative path from ${project.build.directory} project property value. +;; Currently inner classes are filtered out from completion alternatives. +;; You can always import top-level class and use qualified name. ;; -;; `javaimp-mvn-program' defines path of the `mvn' program. Use if it's -;; not on `exec-path'. -;; -;; `javaimp-cygpath-program' defines path of the `cygpath' program (applies -;; to Cygwin only, of course). Use if it's not on `exec-path'. -;; -;; `javaimp-jar-program' defines path of the `jar' program. Use if it's -;; not on `exec-path'. -;; -;; Details on commands. ;; -;; `javaimp-maven-visit-root' is the first command you should issue to -;; use this module. It reads the pom structure recursively and records -;; which files belong to which module. Maven help:effective-pom command is -;; used to do that. -;; -;; `javaimp-organize-imports' groups import statement and writes those -;; group according to the value of `javaimp-import-group-alist'. Imports -;; which are not matched by any regexp in that variable are assigned a -;; default order defined by `javaimp-import-default-order' (50 by default). -;; -;; Sample setup (put this into your .emacs): +;; Example of initialization: ;; ;; (require 'javaimp) ;; -;; (add-to-list 'javaimp-import-group-alist '("\\`\\(ru\\.yota\\.\\|tv\\.okko\\.\\)" . 80)) +;; (add-to-list 'javaimp-import-group-alist +;; '("\\`\\(my\\.company\\.\\|my\\.company2\\.\\)" . 80)) ;; -;; (setq javaimp-jdk-home (getenv "JAVA_HOME")) -;; (setq javaimp-include-current-project-classes t) ;; (setq javaimp-additional-source-dirs '("generated-sources/thrift")) ;; ;; (add-hook 'java-mode-hook @@ -83,23 +60,28 @@ ;; ;; ;; TODO: -;; -;; Support adding static imports by giving a prefix argument to -;; `javaimp-add-import'. ;; -;; Use functions `cygwin-convert-file-name-from-windows' and +;; - 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. - +;; calling `cygpath'. See https://cygwin.com/ml/cygwin/2013-03/msg00228.html. +;; +;; - save/restore state +;; +;; - `javaimp-add-import': without prefix arg narrow alternatives by local name; +;; with prefix arg include all classes in alternatives +;; ;;; Code: +(require 'cl-lib) +(require 'seq) +(require 'xml) + -;;; User options +;; User options (defgroup javaimp () - "Add and reorder Java import statements in Maven projects.") + "Add and reorder Java import statements in Maven projects") (defcustom javaimp-import-group-alist '(("\\`javax?\\." . 10)) "Specifies how to group classes and how to order resulting @@ -107,7 +89,7 @@ groups in the imports list. Each element should be of the form `(CLASSNAME-REGEXP . ORDER)' where `CLASSNAME-REGEXP' is a regexp matching the fully qualified -class name. +class name. Lowest-order groups are placed earlier. The order of classes which were not matched is defined by `javaimp-import-default-order'.") @@ -116,8 +98,9 @@ The order of classes which were not matched is defined by "Defines the order of classes which were not matched by `javaimp-import-group-alist'") -(defcustom javaimp-jdk-home nil - "Path to the JDK") +(defcustom javaimp-jdk-home (getenv "JAVA_HOME") + "Path to the JDK. It is used to find JDK jars to scan. By +default, it is set from the JAVA_HOME environment variable.") (defcustom javaimp-additional-source-dirs nil "List of directories where additional (e.g. generated) @@ -137,607 +120,618 @@ Custom values set in plugin configuration in pom.xml are not supported yet.") (defcustom javaimp-mvn-program "mvn" - "Path to the `mvn' program") + "Path to the `mvn' program. Customize it if the program is not +on `exec-path'.") -(defcustom javaimp-cygpath-program "cygpath" - "Path to the `cygpath' program") +(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") - -(defcustom javaimp-include-current-project-classes t - "If non-nil, current project's classes are included into completion -alternatives. + "Path to the `jar' program used to read contents of jar files. +Customize it if the program is not on `exec-path'.") -Only top-level classes are included.") +(defcustom javaimp-include-current-module-classes t + "If non-nil, current module's classes are included into +completion alternatives. `javaimp-add-import' will find all java +files in the current project and add their fully-qualified names +to the completion alternatives list.") -;;; Variables and constants +;; Variables and constants -(defvar javaimp-maven-root-modules nil - "Loaded root Maven modules") +(defvar javaimp-project-forest nil + "Visited projects.") -(defvar javaimp-jar-classes-cache nil - "Jar classes cache") +(defvar javaimp-cached-jars nil + "Alist of cached jars. Each element is of the form (FILE + . CACHED-JAR).") (defconst javaimp-debug-buf-name "*javaimp-debug*") - -;;; Dealing with XML - -(defun javaimp-xml-child-list (xml-tree child-name) - "Returns list of children of XML-TREE filtered by CHILD-NAME" - (let (result) - (dolist (child (cddr xml-tree) result) - (when (and (listp child) - (eq (car child) child-name)) - (push child result))))) - -(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))) - - -;; A module is represented as a list of the form `(ARTIFACT POM-FILE -;; SOURCE-DIR TEST-SOURCE-DIR BUILD-DIR POM-FILE-MOD-TS PARENT PARENT-TS)'. - -(defsubst javaimp-make-mod (artifact pom-file source-dir - test-source-dir build-dir - pom-file-mod-ts jars-list - parent parent-ts) - (list artifact pom-file source-dir test-source-dir build-dir - pom-file-mod-ts jars-list parent parent-ts)) - -(defsubst javaimp-get-mod-artifact (module) - (nth 0 module)) - -(defsubst javaimp-get-mod-pom-file (module) - (nth 1 module)) - -(defsubst javaimp-get-mod-source-dir (module) - (nth 2 module)) - -(defsubst javaimp-get-mod-test-source-dir (module) - (nth 3 module)) - -(defsubst javaimp-get-mod-build-dir (module) - (nth 4 module)) - -(defsubst javaimp-get-mod-pom-mod-ts (module) - (nth 5 module)) -(defsubst javaimp-set-mod-pom-mod-ts (module value) - (setcar (nthcdr 5 module) value)) - -(defsubst javaimp-get-mod-pom-deps (module) - (nth 6 module)) -(defsubst javaimp-set-mod-pom-deps (module value) - (setcar (nthcdr 6 module) value)) - -(defsubst javaimp-get-mod-parent (module) - (nth 7 module)) -(defsubst javaimp-set-mod-parent (module value) - (setcar (nthcdr 7 module) value)) - -(defsubst javaimp-get-mod-parent-ts (module) - (nth 8 module)) -(defsubst javaimp-set-mod-parent-ts (module value) - (setcar (nthcdr 8 module) value)) - - -;; An artifact is represented as a list: (GROUP-ID ARTIFACT-ID VERSION). - -;; FIXME: use cl-defstruct! +;; Structs -(defun javaimp-make-artifact (group-id artifact-id version) - (list group-id artifact-id version)) +(cl-defstruct javaimp-node + parent children contents) -(defun javaimp-artifact-group-id (artifact) - (car artifact)) +(cl-defstruct javaimp-module + id parent-id + file + final-name + packaging + source-dir test-source-dir build-dir + modules + dep-jars + load-ts) -(defun javaimp-artifact-artifact-id (artifact) - (cadr artifact)) - -(defun javaimp-artifact-version (artifact) - (nth 2 artifact)) - -(defun javaimp-artifact-to-string (artifact) - (format "%s:%s:%s" - (javaimp-artifact-artifact-id artifact) - (javaimp-artifact-group-id artifact) - (javaimp-artifact-version artifact))) ;FIXME: `artifact' is not a function! - -(defun javaimp-parse-artifact (artifact) - (apply #'javaimp-make-artifact (split-string artifact ":"))) +(cl-defstruct javaimp-id + group artifact version) +(cl-defstruct javaimp-cached-jar + file read-ts classes) -;; A jar is represented as follows: `(JAR-PATH JAR-MOD-TS . CLASSES-LIST). +;; Utilities -(defsubst javaimp-make-jar (jar-path jar-mod-ts classes-list) - (cons jar-path (cons jar-mod-ts classes-list))) - -(defsubst javaimp-get-jar-path (jar) - (car jar)) +(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))) -(defsubst javaimp-get-jar-mod-ts (jar) - (cadr jar)) +(defun javaimp--xml-child (name el) + "Returns a child of EL named by symbol NAME" + (assq name (cddr el))) -(defsubst javaimp-set-jar-mod-ts (jar value) - (setcar (cdr jar) value)) +(defun javaimp--xml-first-child (el) + "Returns a first child of EL" + (car (cddr el))) -(defsubst javaimp-get-jar-classes-list (jar) - (cddr jar)) +(defun javaimp--get-file-ts (file) + (nth 5 (file-attributes file))) -(defsubst javaimp-set-jar-classes-list (jar value) - (setcdr (cdr jar) value)) +(defun javaimp--get-jdk-jars () + (if javaimp-jdk-home + (let ((jre-lib-dir + (concat (file-name-as-directory javaimp-jdk-home) + (file-name-as-directory "jre") + (file-name-as-directory "lib")))) + (directory-files jre-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)) -;;; Loading maven projects tree +;; Project loading ;;;###autoload -(defun javaimp-maven-visit-root (path) - "Loads all modules starting from root module identified by -PATH. PATH should point to a directory." - (interactive "DVisit maven root project: ") - (let ((root-pom (expand-file-name - (concat (file-name-as-directory path) "pom.xml"))) - modules existing-module) - (unless (file-readable-p root-pom) - (error "Cannot read root pom: %s" root-pom)) - (setq modules (javaimp-maven-load-module-tree root-pom)) - ;; if a root module with such path is already loaded, replace its - ;; modules - (setq existing-module (assoc root-pom javaimp-maven-root-modules)) - (if existing-module - (setcdr existing-module modules) - (push (cons root-pom modules) javaimp-maven-root-modules)) - (message "Loaded modules for %s" path))) - -(defun javaimp-get-projects (xml-tree) - (cond ((assq 'projects xml-tree) - (javaimp-xml-child-list (assq 'projects xml-tree) 'project)) - ((assq 'project xml-tree) - (list (assq 'project xml-tree))) - (t - (error "Cannot find projects in mvn output")))) +(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. + +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: ") + (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)) + (let ((tree (javaimp--maven-xml-load-tree file))) + (if tree + (push tree javaimp-project-forest))) + (message "Loaded tree for %s" file))) -(defun javaimp-maven-load-module-tree (pom) - "Returns an alist of all Maven modules in a hierarchy starting -with POM" + +;; Maven XML routines + +(defun javaimp--maven-xml-load-tree (file) + "Invokes `mvn help:effective-pom' on FILE and using its output +creates a tree of Maven projects starting from FILE. Children +which link to the parent via the element are inheriting +children and are also included. Subordinate modules with no +inheritance are not included." + (let ((xml-tree (javaimp--maven-xml-read-effective-pom file))) + (cond ((assq 'project xml-tree) + (let ((project-elt (assq 'project xml-tree)) + (submodules (javaimp--xml-children + (javaimp--xml-child 'modules project-elt) 'module))) + (and submodules + ;; no real children + (message "Independent submodules: %s" + (mapconcat #'javaimp--xml-first-child submodules ", "))) + (let ((module (javaimp--maven-xml-parse-module project-elt))) + (javaimp--maven-build-tree + (javaimp-module-id module) nil (list module) file)))) + ((assq 'projects xml-tree) + ;; we have are inheriting children - they and their children, if + ;; any, are listed in a linear list + (let* ((project-elts (javaimp--xml-children + (assq 'projects xml-tree) 'project)) + (all-modules (mapcar #'javaimp--maven-xml-parse-module project-elts))) + (message "Total modules: %d" (length all-modules)) + (javaimp--maven-build-tree + (javaimp-module-id (car all-modules)) nil all-modules file))) + (t + ;; neither nor - error + (error "Invalid `help:effective-pom' output"))))) + +(defun javaimp--maven-xml-read-effective-pom (pom) + "Calls `mvn help:effective:pom and returns XML parse tree" (message "Loading root pom %s..." pom) - (javaimp-call-mvn + (javaimp--maven-call pom "help:effective-pom" (lambda () - (let (xml-start-pos xml-end-pos) - ;; find where we should start parsing XML - (goto-char (point-min)) - (re-search-forward "<\\?xml\\|")) - (setq xml-end-pos (match-end 0)) - ;; parse - (let ((artifact-pomfile-alist - (javaimp-build-artifact-pomfile-alist (list pom))) - (children (javaimp-get-projects - (xml-parse-region xml-start-pos xml-end-pos)))) - (javaimp-maven-build-children children artifact-pomfile-alist)))))) - -(defun javaimp-make-artifact-from-xml (node) - (javaimp-make-artifact - (javaimp-xml-first-child (javaimp-xml-child 'groupId node)) - (javaimp-xml-first-child (javaimp-xml-child 'artifactId node)) - (javaimp-xml-first-child (javaimp-xml-child 'version node)))) - -(defun javaimp-get-pom-file-path-lax (artifact artifact-pomfile-alist) - (assoc-default - artifact artifact-pomfile-alist - (lambda (tested target) - (or (equal target tested) - (equal (javaimp-artifact-artifact-id target) - (javaimp-artifact-artifact-id tested)))))) - -(defun javaimp-maven-build-children (projects artifact-pomfile-alist) - (let (result) - (dolist (proj projects result) - (let* ((artifact (javaimp-make-artifact-from-xml proj)) - (pom-file-path (javaimp-get-pom-file-path-lax - artifact artifact-pomfile-alist)) - (build (javaimp-xml-child 'build proj)) - (source-dir (javaimp-xml-first-child - (javaimp-xml-child 'sourceDirectory build))) - (test-source-dir (javaimp-xml-first-child - (javaimp-xml-child 'testSourceDirectory - build))) - (build-dir (javaimp-xml-first-child - (javaimp-xml-child 'directory build))) - (parent (javaimp-make-artifact-from-xml - (javaimp-xml-child 'parent proj)))) - (push (javaimp-make-mod - artifact - pom-file-path - (file-name-as-directory - (if (eq system-type 'cygwin) - (car (process-lines javaimp-cygpath-program "-u" - source-dir)) - source-dir)) - (file-name-as-directory - (if (eq system-type 'cygwin) - (car (process-lines javaimp-cygpath-program "-u" - test-source-dir)) - test-source-dir)) - (file-name-as-directory - (if (eq system-type 'cygwin) - (car (process-lines javaimp-cygpath-program "-u" - build-dir)) - build-dir)) - nil nil parent nil) - result))))) - -(defun javaimp-build-artifact-pomfile-alist (pom-file-list) - "Recursively builds an alist where each element is of the -form (\"ARTIFACT\" . \"POM-FILE-PATH\"). This is needed because -there is no pom file path in the output of `mvn -help:effective-pom'. Each pom file path in POM-FILE-LIST should -be in platform's default format." - (when pom-file-list - (let ((pom-file (car pom-file-list)) - xml-tree project) - (message "Saving artifact id -> pom file mapping for %s" pom-file) - (with-temp-buffer - (insert-file-contents pom-file) - (setq xml-tree (xml-parse-region (point-min) (point-max)))) - (setq project (if (assq 'top xml-tree) - (assq 'project (cddr (assq 'top xml-tree))) - (assq 'project xml-tree))) - (cons - ;; this pom - (cons (javaimp-make-artifact-from-xml project) pom-file) - (append - ;; submodules - (javaimp-build-artifact-pomfile-alist - (mapcar (lambda (submodule) - (expand-file-name - (concat - ;; this pom's path - (file-name-directory pom-file) - ;; relative submodule directory - (file-name-as-directory - (let ((submodule-path (car (cddr submodule)))) - (if (eq system-type 'cygwin) - (car (process-lines javaimp-cygpath-program "-u" - submodule-path)) - submodule-path))) - ;; well-known file name - "pom.xml"))) - (javaimp-xml-child-list (assq 'modules (cddr project)) 'module))) - ;; rest items - (javaimp-build-artifact-pomfile-alist (cdr pom-file-list))))))) - -(defun javaimp-call-mvn (pom-file target handler) + (let ((xml-start-pos + (save-excursion + (progn + (goto-char (point-min)) + (re-search-forward "<\\?xml\\|")) + (match-end 0))))) + (xml-parse-region xml-start-pos xml-end-pos))))) + +(defun javaimp--maven-xml-parse-module (project-elt) + (let ((build-elt (javaimp--xml-child 'build project-elt))) + (make-javaimp-module + :id (javaimp--maven-xml-extract-id project-elt) + :parent-id (javaimp--maven-xml-extract-id (javaimp--xml-child 'parent project-elt)) + ;; we set `file' slot later because raw element does not contain + ;; pom file path, so we need to construct it during tree construction + :file nil + :final-name (javaimp--xml-first-child + (javaimp--xml-child 'finalName build-elt)) + :packaging (javaimp--xml-first-child + (javaimp--xml-child 'packaging project-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)))) + :modules (mapcar (lambda (module-elt) + (javaimp--xml-first-child module-elt)) + (javaimp--xml-children (javaimp--xml-child 'modules project-elt) 'module)) + :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)))) + +(defun javaimp--maven-xml-file-matches (file id parent-id) + (let* ((xml-tree (with-temp-buffer + (insert-file-contents file) + (xml-parse-region (point-min) (point-max)))) + (project-elt (assq 'project xml-tree)) + (tested-id (javaimp--maven-xml-extract-id project-elt)) + (tested-parent-id (javaimp--maven-xml-extract-id (assq 'parent 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 + (let ((test (if (or (null (javaimp-id-group tested-id)) + (null (javaimp-id-version tested-id)) + (null (javaimp-id-group tested-parent-id)) + (null (javaimp-id-version tested-parent-id))) + (progn + (message "File %s contains incomplete id, using lax match" file) + (lambda (first second) + (equal (javaimp-id-artifact first) (javaimp-id-artifact second)))) + #'equal))) + (and (funcall test tested-id id) + (funcall test tested-parent-id parent-id))))) + + +;; 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 (if (eq system-type 'cygwin) - (car (process-lines javaimp-cygpath-program - "-m" pom-file)) - pom-file)) + (let* ((pom-file (javaimp-cygpath-convert-maybe pom-file)) (status - ;; FIXME on GNU/Linux Maven strangely outputs ^M chars. Check - ;; also jar output with the same var binding below. - (let ((coding-system-for-read (when (eq system-type 'cygwin) 'utf-8-dos))) + ;; 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))) - (output-buf (current-buffer))) + (buf (current-buffer))) (with-current-buffer (get-buffer-create javaimp-debug-buf-name) (erase-buffer) - (insert-buffer-substring output-buf)) - (unless (and (numberp status) (= status 0)) - (error "Maven target \"%s\" failed with status \"%s\"" - target status)) + (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 (id parent-node all-modules file) + (message "Building tree for project: %s" id) + (let ((this (or (seq-find (lambda (m) (equal (javaimp-module-id m) id)) + all-modules) + (error "Cannot find module %s!" id))) + ;; although each real parent has section, more reliable + ;; way to build hirarchy is to analyze node in each child + (children (seq-filter (lambda (m) (equal (javaimp-module-parent-id m) id)) + all-modules))) + (if (and (null children) + (string= (javaimp-module-packaging this) "pom")) + (progn (message "Skipping empty aggregate module %s" (javaimp-module-id this)) + nil) + ;; here we can finally set the `file' slot as the path is known at + ;; this time + (setf (javaimp-module-file this) file) + ;; make node + (let ((this-node (make-javaimp-node + :parent parent-node + :children nil + :contents this))) + (setf (javaimp-node-children this-node) + (mapcar (lambda (child) + (let ((child-file + (javaimp--maven-get-submodule-file + child file (javaimp-module-modules this)))) + (javaimp--maven-build-tree + (javaimp-module-id child) this-node all-modules child-file))) + children)) + this-node)))) + +(defun javaimp--maven-get-submodule-file (submodule parent-file rel-paths-from-parent) + ;; seems that the only reliable way to match a module parsed from + ;; element with module relative path taken from is to + ;; visit pom and check that id and parent-id matches + (let* ((parent-dir (file-name-directory parent-file)) + (files (mapcar (lambda (rel-path) + (concat parent-dir + (file-name-as-directory rel-path) + "pom.xml")) + rel-paths-from-parent))) + (or (seq-find + (lambda (file) + (javaimp--maven-xml-file-matches + file (javaimp-module-id submodule) (javaimp-module-parent-id submodule))) + files) + (error "Cannot find file for module: %s" (javaimp-module-id submodule))))) + -;;; Reading and caching dependencies +;;; Loading dep-jars + +(defun javaimp--maven-update-module-maybe (node) + (let (need-update) + ;; are deps not initialized? + (let ((module (javaimp-node-contents node))) + (if (null (javaimp-module-dep-jars module)) + (setq need-update t))) + ;; were any pom.xml files updated after last load? + (let ((tmp node)) + (while (and tmp + (not need-update)) + (let ((module (javaimp-node-contents tmp))) + (if (> (float-time (javaimp--get-file-ts (javaimp-module-file module))) + (float-time (javaimp-module-load-ts module))) + (setq need-update t))) + (setq tmp (javaimp-node-parent tmp)))) + (when need-update + ;; update current module + (let ((module (javaimp-node-contents node))) + ;; reload & update dep-jars + (setf (javaimp-module-dep-jars module) + (javaimp--maven-fetch-dep-jars module)) + ;; update load-ts + (setf (javaimp-module-load-ts module) (current-time)))))) + +(defun javaimp--maven-fetch-dep-jars (module) + (let ((raw-line + (javaimp--maven-call + (javaimp-module-file module) "dependency:build-classpath" + (lambda () + (goto-char (point-min)) + (search-forward "Dependencies classpath:") + (forward-line 1) + (thing-at-point 'line)))) + (separator-regex (concat "[" path-separator "\n" "]+"))) + (split-string (javaimp-cygpath-convert-maybe raw-line 'unix t) separator-regex t))) -(defun javaimp-maven-fetch-module-deps (module) - "Returns list of dependency jars for MODULE" - (javaimp-call-mvn - (javaimp-get-mod-pom-file module) "dependency:build-classpath" - (lambda () - (let (deps-line) - (goto-char (point-min)) - (search-forward "Dependencies classpath:") - (forward-line 1) - (setq deps-line (thing-at-point 'line)) - (when (eq system-type 'cygwin) - (setq deps-line (car (process-lines javaimp-cygpath-program - "-up" - deps-line)))) - (split-string deps-line (concat "[" path-separator "\n" "]+") t))))) - -(defun javaimp-get-file-ts (file) - (nth 5 (file-attributes file))) -(defun javaimp-any-file-ts-updated (files) - (if (null files) - nil - (let ((curr-ts (javaimp-get-file-ts (car (car files)))) - (last-ts (cdr (car files)))) - (or (null last-ts) ; reading for the first time? - (not (equal (float-time curr-ts) (float-time last-ts))) - (javaimp-any-file-ts-updated (cdr files)))))) - -(defun javaimp-get-dep-jars-cached (module parent) - "Returns a list of dependency jar file paths for a MODULE. -Both MODULE and PARENT poms are checked for updates because -PARENT pom may have some versions which are inherited by the -MODULE." - (when (javaimp-any-file-ts-updated - (remq nil (list (cons (javaimp-get-mod-pom-file module) - (javaimp-get-mod-pom-mod-ts module)) - (when parent - (cons - (javaimp-get-mod-pom-file parent) - ;; here we check the saved parent ts because it - ;; matters what version we had when we were - ;; reloading this pom the last time - (javaimp-get-mod-parent-ts module)))))) - ;; (re-)fetch dependencies - (javaimp-set-mod-pom-deps - module (javaimp-maven-fetch-module-deps module)) - ;; update timestamps - (javaimp-set-mod-pom-mod-ts - module (javaimp-get-file-ts (javaimp-get-mod-pom-file module))) - (when parent - (javaimp-set-mod-parent-ts - module (javaimp-get-file-ts (javaimp-get-mod-pom-file parent))))) - (javaimp-get-mod-pom-deps module)) - -(defun javaimp-get-jdk-jars () - "Returns list of jars from the jre/lib subdirectory of the JDK -directory" - (when javaimp-jdk-home - (directory-files (concat (file-name-as-directory javaimp-jdk-home) - (file-name-as-directory "jre/lib")) - t "\\.jar$"))) - -(defun javaimp-get-jar-classes-cached (jar) - (let ((current-jar-mod-ts - (nth 5 (file-attributes (javaimp-get-jar-path jar))))) - (unless (equal (float-time (javaimp-get-jar-mod-ts jar)) - (float-time current-jar-mod-ts)) - (javaimp-set-jar-classes-list jar (javaimp-fetch-jar-classes jar)) - (javaimp-set-jar-mod-ts jar current-jar-mod-ts)) - (javaimp-get-jar-classes-list jar))) - -(defun javaimp-fetch-jar-classes (jar) - (let ((jar-file (javaimp-get-jar-path jar)) - result) - (message "Reading classes in jar: %s" jar-file) - (with-temp-buffer - (let ((jar-file (if (eq system-type 'cygwin) - (car (process-lines javaimp-cygpath-program - "-m" jar-file)) - jar-file)) - (coding-system-for-read (when (eq system-type 'cygwin) 'utf-8-dos))) - (process-file javaimp-jar-program nil t nil "-tf" jar-file)) + +;; Working with jar classes + +(defun javaimp--get-jar-classes (file) + (let ((cached (cdr (assoc file javaimp-cached-jars)))) + (cond ((null cached) + ;; create, load & put into cache + (setq cached + (make-javaimp-cached-jar + :file file + :read-ts (javaimp--get-file-ts file) + :classes (javaimp--fetch-jar-classes file))) + (push (cons file cached) javaimp-cached-jars)) + ((> (float-time (javaimp--get-file-ts (javaimp-cached-jar-file cached))) + (float-time (javaimp-cached-jar-read-ts cached))) + ;; reload + (setf (javaimp-cached-jar-classes cached) (javaimp--fetch-jar-classes file)) + ;; update read-ts + (setf (javaimp-cached-jar-read-ts cached) (current-time)))) + ;; return from cached + (javaimp-cached-jar-classes cached))) + +(defun javaimp--fetch-jar-classes (file) + (message "Reading classes in file: %s" file) + (with-temp-buffer + (let ((coding-system-for-read (and (eq system-type 'cygwin) 'utf-8-dos))) + ;; On Cygwin, "jar" is a Windows program, so file path needs to be + ;; converted appropriately. + (process-file javaimp-jar-program nil t nil + ;; `jar' accepts commands/options as a single string + "tf" (javaimp-cygpath-convert-maybe file 'windows)) (goto-char (point-min)) - (while (re-search-forward "^\\(.+\\)\\.class$" nil t) - (push (replace-regexp-in-string "[/$]" "." (match-string 1)) - result)) - result))) - -(defun javaimp-collect-jar-classes (jar-paths) - (let (result jar) - (dolist (jar-path jar-paths result) - (setq jar (assoc jar-path javaimp-jar-classes-cache)) - (unless jar - (setq jar (javaimp-make-jar jar-path nil nil)) - (push jar javaimp-jar-classes-cache)) - (setq result (append (javaimp-get-jar-classes-cached jar) result))))) - -(defun javaimp-get-module-from-root (roots predicate) - (if (null roots) - nil - (let ((result (javaimp-get-module (cdr (car roots)) predicate))) - (or result - (javaimp-get-module-from-root (cdr roots) predicate))))) - -(defun javaimp-get-module (modules predicate) - (cond ((null modules) - nil) - ((funcall predicate (car modules)) - (car modules)) - (t - (javaimp-get-module (cdr modules) predicate)))) + (while (search-forward "/" nil t) + (replace-match ".")) + (goto-char (point-min)) + (let (result) + (while (re-search-forward "\\(^[[:alnum:]._]+\\)\\.class$" nil t) + (push (match-string 1) result)) + result)))) + + +;; Some API functions -(defun javaimp-get-module-by-file (file) - (javaimp-get-module-from-root - javaimp-maven-root-modules - (lambda (mod) - (or (string-prefix-p (javaimp-get-mod-source-dir mod) file) - (string-prefix-p (javaimp-get-mod-test-source-dir mod) file))))) +(defun javaimp-get-all-modules () + (javaimp-select-nodes (lambda (module) t))) -(defun javaimp-get-module-by-artifact (artifact) - (javaimp-get-module-from-root - javaimp-maven-root-modules - (lambda (mod) - (equal (javaimp-get-mod-artifact mod) artifact)))) +(defun javaimp-find-node (predicate) + (javaimp--find-in-forest javaimp-project-forest predicate)) + +(defun javaimp-select-nodes (predicate) + (javaimp--select-from-forest javaimp-project-forest predicate)) + + +;; Tree search routines + +(defun javaimp--find-in-forest (forest predicate) + (catch 'found + (dolist (tree forest) + (javaimp--find-node tree predicate)))) + +(defun javaimp--find-node (tree predicate) + (if tree + (progn (if (funcall predicate (javaimp-node-contents tree)) + (throw 'found tree)) + (dolist (child (javaimp-node-children tree)) + (javaimp--find-node child predicate))))) + +(defun javaimp--select-from-forest (forest predicate) + (apply #'seq-concatenate 'list + (mapcar (lambda (tree) + (javaimp--select-nodes tree predicate)) + forest))) + +(defun javaimp--select-nodes (tree predicate) + (if tree + (append (if (funcall predicate (javaimp-node-contents tree)) + (list tree)) + (apply #'seq-concatenate 'list + (mapcar (lambda (child) + (javaimp--select-nodes child predicate)) + (javaimp-node-children tree)))))) -;;; Adding and organizing imports +;;; Adding imports ;;;###autoload (defun javaimp-add-import (classname) "Imports CLASSNAME in the current file. Interactively, -performs class name completion based on the current module's -dependencies, JDK jars and top-level classes in the current -module." +asks for a class to import, adds import statement and calls +`javaimp-organize-imports'. Import statements are not +duplicated. Completion alternatives are constructed based on +this module's dependencies' classes, JDK classes and top-level +classes in the current module." (interactive - (let* ((file (expand-file-name - (or buffer-file-name - (error "Buffer is not visiting a file!")))) - (module (or (javaimp-get-module-by-file file) - (error "Cannot determine module for file: %s" file))) - (parent (javaimp-get-module-by-artifact - (javaimp-get-mod-parent module)))) - (list (completing-read - "Import: " - (append - (javaimp-collect-jar-classes - (append (javaimp-get-dep-jars-cached module parent) - (javaimp-get-jdk-jars))) - (and javaimp-include-current-project-classes - (javaimp-get-module-classes module))) - nil t nil nil (symbol-name (symbol-at-point)))))) - (javaimp-organize-imports classname)) - -(defun javaimp-get-module-classes (module) - "Scans current project and returns a list of top-level classes in both the -source directory and test source directory" - (let ((src-dir (javaimp-get-mod-source-dir module)) - (test-src-dir (javaimp-get-mod-test-source-dir module)) - (build-dir (javaimp-get-mod-build-dir module))) - (append - (and javaimp-additional-source-dirs - (seq-mapcat + (progn + (barf-if-buffer-read-only) + (let* ((file (expand-file-name + (or buffer-file-name + (error "Buffer is not visiting a file!")))) + (node (or (javaimp-find-node + (lambda (m) + (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) + (let ((module (javaimp-node-contents node))) + (list (completing-read + "Import: " + (append + ;; we're not caching full list of classes coming from module + ;; dependencies because jars may change and we need to reload + ;; them + (let ((jars (append (javaimp-module-dep-jars module) + (javaimp--get-jdk-jars)))) + (apply #'seq-concatenate 'list + (mapcar #'javaimp--get-jar-classes jars))) + (and javaimp-include-current-module-classes + (javaimp--get-module-classes module))) + nil t nil nil (symbol-name (symbol-at-point)))))))) + (javaimp-organize-imports (cons classname 'ordinary))) + +(defun javaimp--get-module-classes (module) + "Returns list of top-level classes in current module" + (append + (let ((build-dir (javaimp-module-build-dir module))) + (and (seq-mapcat (lambda (rel-dir) - (let ((dir (file-name-as-directory (concat build-dir rel-dir)))) + (let ((dir (concat build-dir (file-name-as-directory rel-dir)))) (and (file-accessible-directory-p dir) - (javaimp-get-directory-classes dir nil)))) - javaimp-additional-source-dirs)) - (and (file-accessible-directory-p test-src-dir) - (javaimp-get-directory-classes test-src-dir nil)) - (and (file-accessible-directory-p src-dir) - (javaimp-get-directory-classes src-dir nil))))) - -(defun javaimp-get-directory-classes (dir prefix) - "Returns the list of classes found in the directory DIR. PREFIX is the -initial package prefix." - (let (result) - ;; traverse subdirectories - (dolist (file (directory-files-and-attributes dir nil nil t)) - (if (and (eq (cadr file) t) - (not (or (string= (car file) ".") - (string= (car file) "..")))) - (setq result - (append (javaimp-get-directory-classes - (concat dir (file-name-as-directory (car file))) - (concat prefix (car file) ".")) - result)))) - ;; add .java files in the current directory - (dolist (file (directory-files-and-attributes dir nil "\\.java\\'" t)) - (unless (cadr file) - (push (concat prefix (file-name-sans-extension (car file))) result))) - result)) - -(defun javaimp-add-to-import-groups (new-class groups) - "Subroutine of `javaimp-organize-imports'" - (let* ((order (or (assoc-default new-class javaimp-import-group-alist - 'string-match) - javaimp-import-default-order)) - (group (assoc order groups))) - (if group - (progn - ;; add only if this class is not already there - (unless (member new-class (cdr group)) - (setcdr group (cons new-class (cdr group)))) - groups) - (cons (cons order (list new-class)) groups)))) - -(defun javaimp-insert-import-groups (groups static-p) - "Inserts all imports in GROUPS. Non-nil STATIC-P means that - all imports are static." - (when groups - (dolist (group (sort groups (lambda (g1 g2) - (< (car g1) (car g2))))) - (dolist (class (sort (cdr group) 'string<)) - (insert (concat "import " (when static-p "static ") class ";\n"))) - (insert ?\n)) - ;; remove newline after the last group - (delete-char -1))) + (javaimp--get-directory-classes dir nil)))) + javaimp-additional-source-dirs))) + (let ((dir (javaimp-module-source-dir module))) + (and (file-accessible-directory-p dir) + (javaimp--get-directory-classes dir nil))) + (let ((dir (javaimp-module-test-source-dir module))) + (and (file-accessible-directory-p dir) + (javaimp--get-directory-classes dir nil))))) + +(defun javaimp--get-directory-classes (dir prefix) + (append + ;; .java files in current directory + (mapcar (lambda (file) + (concat prefix (file-name-sans-extension (car file)))) + (seq-filter (lambda (file) (null (cadr file))) ;only files + (directory-files-and-attributes dir nil "\\.java\\'" t))) + ;; descend into subdirectories + (apply #'seq-concatenate 'list + (mapcar (lambda (subdir) + (let ((name (car subdir))) + (javaimp--get-directory-classes + (concat dir (file-name-as-directory name)) (concat prefix name ".")))) + (seq-filter (lambda (file) (and (cadr file) ;only directories + (null (member (car file) '("." ".."))))) + (directory-files-and-attributes dir nil nil t)))))) -;;;###autoload -(defun javaimp-organize-imports (&rest new-classes) - "Groups and orders import statements in the current buffer. Groups are -formed and ordered according to `javaimp-import-group-alist'. Classes within a -single group are ordered in a lexicographic order. Optional NEW-CLASSES -argument is a list of additional classes to import." - (interactive) - (barf-if-buffer-read-only) - (save-excursion - (let (import-groups static-import-groups old-imports-start) - ;; existing imports - (goto-char (point-min)) - (while (re-search-forward - "^\\s-*import\\s-+\\(static\\s-+\\)?\\([._[:word:]]+\\)" - nil t) - (if (null (match-string 1)) - (setq import-groups - (javaimp-add-to-import-groups (match-string 2) - import-groups)) - (setq static-import-groups - (javaimp-add-to-import-groups (match-string 2) - static-import-groups))) - (beginning-of-line) - (unless old-imports-start (setq old-imports-start (point))) - (delete-region (point) (line-beginning-position 2)) - ;; delete whatever was between import statements - (when (/= (point) old-imports-start) - (delete-region old-imports-start (point)))) - ;; new imports - (dolist (class new-classes) - (setq import-groups (javaimp-add-to-import-groups class import-groups))) - ;; insert all - (if (or import-groups static-import-groups) - (progn - ;; prepare the position - (cond (old-imports-start - ;; when there were any imports, do not touch blank lines - ;; before imports - (goto-char old-imports-start)) - ((re-search-forward "^\\s-*package\\s-" nil t) - ;; when there is a package statement, insert one or two - ;; blank lines after it - (when (= (forward-line) 1) (insert ?\n)) ;; last line? - (insert ?\n)) - (t - ;; otherwise, start at the bob, insert one empty line - ;; after point - (goto-char (point-min)) - (insert ?\n) - (backward-char))) - (javaimp-insert-import-groups import-groups nil) - (and import-groups static-import-groups (insert ?\n)) - (javaimp-insert-import-groups static-import-groups t)) - (message "Nothing to organize"))))) + +;; Organizing imports ;;;###autoload -(defun javaimp-invalidate-jar-classes-cache () - "Resets jar classes cache (debugging only)" - (interactive) - (setq javaimp-jar-classes-cache nil)) +(defun javaimp-organize-imports (&rest new-imports) + "Groups import statements according to the value of +`javaimp-import-group-alist' (which see) and prints resulting +groups leaving one blank line in between. -;;;###autoload -(defun javaimp-forget-all-visited-modules () - "Resets `javaimp-maven-root-modules' (debugging only)" - (interactive) - (setq javaimp-maven-root-modules nil)) +Classes within a single group are ordered in a lexicographic +order. -;;;###autoload -(defun javaimp-reset () - "Resets all data (debugging only)" +Imports not matched by any regexp in `javaimp-import-group-alist' +are assigned a default order defined by +`javaimp-import-default-order'. + +NEW-IMPORTS is a list of additional imports; each element should +be of the form (CLASS . TYPE), where CLASS is a string and TYPE +is `'ordinary' or `'static'. Interactively, NEW-IMPORTS is nil." (interactive) - (javaimp-forget-all-visited-modules) - (javaimp-invalidate-jar-classes-cache)) + (barf-if-buffer-read-only) + (save-excursion + (goto-char (point-min)) + (let* ((old-data (javaimp--parse-imports)) + (first (car old-data)) + (last (cadr old-data)) + (all-imports (append new-imports (cddr old-data)))) + ;; delete old imports, if any + (if first + (progn + (goto-char last) + (forward-line) + (delete-region first (point)))) + (javaimp--prepare-for-insertion first) + (setq all-imports + (delete-duplicates all-imports + :test (lambda (first second) + (equal (car first) (car second))))) + ;; assign order + (let ((with-order + (mapcar + (lambda (import) + (let ((order (or (assoc-default (car import) + javaimp-import-group-alist + 'string-match) + javaimp-import-default-order))) + (cons import order))) + all-imports))) + (setq with-order + (sort with-order + (lambda (first second) + ;; sort by order, name + (if (= (cdr first) (cdr second)) + (string< (caar first) (caar second)) + (< (cdr first) (cdr second)))))) + (javaimp--insert-imports with-order))))) + +(defun javaimp--parse-imports () + (let (first last list) + (while (re-search-forward "^\\s-*import\\s-+\\(static\\s-+\\)?\\([._[:word:]]+\\)" nil t) + (push (cons (match-string 2) (if (match-string 1) 'static 'ordinary)) list) + (setq last (line-beginning-position)) + (or first (setq first last))) + (cons first (cons last list)))) + +(defun javaimp--prepare-for-insertion (start) + (cond (start + ;; if there were any imports, we start inserting at the same place + (goto-char start)) + ((re-search-forward "^\\s-*package\\s-" nil t) + ;; if there's a package directive, move to the next line, creating it + ;; if needed + (end-of-line) + (if (eobp) + (insert ?\n) + (forward-line)) + ;; then insert one blank line and we're done + (insert ?\n)) + (t + ;; otherwise, just go to bob + (goto-char (point-min))))) + +(defun javaimp--insert-imports (imports) + (let ((static (seq-filter (lambda (elt) + (eq (cdar elt) 'static)) + imports)) + (ordinary (seq-filter (lambda (elt) + (eq (cdar elt) 'ordinary)) + imports))) + (javaimp--insert-import-group "import static %s;" static) + (and static ordinary (insert ?\n)) + (javaimp--insert-import-group "import %s;" ordinary))) + +(defun javaimp--insert-import-group (pattern imports) + (let (last-order) + (dolist (import imports) + ;; if adjacent imports have different order value, insert a newline + ;; between them + (let ((order (cdr import))) + (and last-order + (/= order last-order) + (insert ?\n)) + (insert (format pattern (caar import)) ?\n) + (setq last-order order))))) (provide 'javaimp) -- 2.39.2