1 ;;; javaimp.el --- Add and reorder Java import statements in Maven projects -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2014, 2015, 2016 Free Software Foundation, Inc.
5 ;; Author: Filipp Gunbin <fgunbin@fastmail.fm>
6 ;; Maintainer: Filipp Gunbin <fgunbin@fastmail.fm>
8 ;; Keywords: java, maven, programming
12 ;; Allows to manage Java import statements in Maven projects.
16 ;; - customize `javaimp-import-group-alist'
17 ;; - call `javaimp-maven-visit-project', giving it the top-level project
18 ;; directory where pom.xml resides
20 ;; Then in a Java buffer visiting a file under that project or one of its
21 ;; submodules call `javaimp-organize-imports' or `javaimp-add-import'.
23 ;; This module does not add all needed imports automatically! It only helps
24 ;; you to quickly add imports when stepping through compilation errors.
28 ;; If Maven failed, you can see its output in the buffer named by
29 ;; `javaimp-debug-buf-name' (default is "*javaimp-debug*").
31 ;; Contents of jar files and Maven project structures (pom.xml) are cached,
32 ;; so usually only the first command should take a considerable amount of
33 ;; time to complete. If a module's pom.xml or any of its parents' pom.xml
34 ;; (within visited tree) was modified after information was loaded, `mvn
35 ;; dependency:build-classpath' is re-run on the current module. If a jar
36 ;; file was changed, its contents are re-read.
38 ;; Currently inner classes are filtered out from completion alternatives.
39 ;; You can always import top-level class and use qualified name.
42 ;; Example of initialization:
46 ;; (add-to-list 'javaimp-import-group-alist
47 ;; '("\\`\\(my\\.company\\.\\|my\\.company2\\.\\)" . 80))
49 ;; (setq javaimp-additional-source-dirs '("generated-sources/thrift"))
51 ;; (add-hook 'java-mode-hook
53 ;; (local-set-key "\C-ci" 'javaimp-add-import)
54 ;; (local-set-key "\C-co" 'javaimp-organize-imports)))
59 ;; - use functions `cygwin-convert-file-name-from-windows' and
60 ;; `cygwin-convert-file-name-to-windows' when they are available instead of
61 ;; calling `cygpath'. See https://cygwin.com/ml/cygwin/2013-03/msg00228.html
63 ;; - save/restore state, on restore check if a root exists and delete it if
66 ;; - `javaimp-add-import': without prefix arg narrow alternatives by local name;
67 ;; with prefix arg include all classes in alternatives
69 ;; - :type for defcustom
81 "Add and reorder Java import statements in Maven projects"
84 (defcustom javaimp-import-group-alist '(("\\`javax?\\." . 10))
85 "Specifies how to group classes and how to order resulting
86 groups in the imports list.
88 Each element should be of the form `(CLASSNAME-REGEXP . ORDER)'
89 where `CLASSNAME-REGEXP' is a regexp matching the fully qualified
90 class name. Lowest-order groups are placed earlier.
92 The order of classes which were not matched is defined by
93 `javaimp-import-default-order'.")
95 (defcustom javaimp-import-default-order 50
96 "Defines the order of classes which were not matched by
97 `javaimp-import-group-alist'")
99 (defcustom javaimp-java-home (getenv "JAVA_HOME")
100 "Path to the JDK. Directory jre/lib underneath this path is
101 searched for JDK libraries. By default, it is initialized from
102 the JAVA_HOME environment variable.")
104 (defcustom javaimp-additional-source-dirs nil
105 "List of directories where additional (e.g. generated)
108 Each directory is a relative path from ${project.build.directory} project
111 Typically you would check documentation for a Maven plugin, look
112 at the parameter's default value there and add it to this list.
114 E.g. \"${project.build.directory}/generated-sources/<plugin_name>\"
115 becomes \"generated-sources/<plugin_name>\" (note the absence
116 of the leading slash.
118 Custom values set in plugin configuration in pom.xml are not
121 (defcustom javaimp-mvn-program "mvn"
122 "Path to the `mvn' program. Customize it if the program is not
125 (defcustom javaimp-cygpath-program
126 (if (eq system-type 'cygwin) "cygpath")
127 "Path to the `cygpath' program (Cygwin only). Customize it if
128 the program is not on `exec-path'.")
130 (defcustom javaimp-jar-program "jar"
131 "Path to the `jar' program used to read contents of jar files.
132 Customize it if the program is not on `exec-path'.")
134 (defcustom javaimp-include-current-module-classes t
135 "If non-nil, current module's classes are included into
136 completion alternatives. `javaimp-add-import' will find all java
137 files in the current project and add their fully-qualified names
138 to the completion alternatives list.")
141 ;; Variables and constants
143 (defvar javaimp-project-forest nil
146 (defvar javaimp-cached-jars nil
147 "Alist of cached jars. Each element is of the form (FILE
150 (defconst javaimp-debug-buf-name "*javaimp-debug*")
154 (cl-defstruct javaimp-node
155 parent children contents)
157 (cl-defstruct javaimp-module
162 source-dir test-source-dir build-dir
167 (cl-defstruct javaimp-id
168 group artifact version)
170 (cl-defstruct javaimp-cached-jar
171 file read-ts classes)
176 (defun javaimp--xml-children (xml-tree child-name)
177 "Returns list of children of XML-TREE filtered by CHILD-NAME"
178 (seq-filter (lambda (child)
180 (eq (car child) child-name)))
183 (defun javaimp--xml-child (name el)
184 "Returns a child of EL named by symbol NAME"
185 (assq name (cddr el)))
187 (defun javaimp--xml-first-child (el)
188 "Returns a first child of EL"
191 (defun javaimp--get-file-ts (file)
192 (nth 5 (file-attributes file)))
194 (defun javaimp--get-jdk-jars ()
195 (and javaimp-java-home
196 (file-accessible-directory-p javaimp-java-home)
198 (concat (file-name-as-directory javaimp-java-home)
199 (file-name-as-directory "jre")
200 (file-name-as-directory "lib"))))
201 (directory-files lib-dir t "\\.jar\\'"))))
203 (defun javaimp-cygpath-convert-maybe (path &optional mode is-really-path)
204 "On Cygwin, converts PATH using cygpath according to MODE and
205 IS-REALLY-PATH. If MODE is `unix' (the default), adds -u switch.
206 If MODE is `windows', adds -m switch. If `is-really-path' is
207 non-nil, adds `-p' switch. On other systems, PATH is returned
209 (if (eq system-type 'cygwin)
211 (unless mode (setq mode 'unix))
213 (push (cond ((eq mode 'unix) "-u")
214 ((eq mode 'windows) "-m")
215 (t (error "Invalid mode: %s" mode)))
217 (and is-really-path (push "-p" args))
219 (car (apply #'process-lines javaimp-cygpath-program args))))
226 (defun javaimp-maven-visit-project (path)
227 "Loads a project and its submodules. PATH should point to a
228 directory containing pom.xml.
230 Calls `mvn help:effective-pom' on the pom.xml in the PATH, reads
231 project structure from the output and records which files belong
232 to which modules and other module information.
234 After being processed by this command, the module tree becomes
235 known to javaimp and `javaimp-add-import' maybe called inside any
237 (interactive "DVisit maven project in directory: ")
238 (let ((file (expand-file-name
239 (concat (file-name-as-directory path) "pom.xml"))))
240 (unless (file-readable-p file)
241 (error "Cannot read file: %s" file))
242 ;; delete previous loaded tree, if any
243 (setq javaimp-project-forest
244 (seq-remove (lambda (tree)
245 (equal (javaimp-module-file (javaimp-node-contents tree))
247 javaimp-project-forest))
248 (message "Loading file %s..." file)
250 (javaimp--maven-call file "help:effective-pom"
251 #'javaimp--maven-xml-effective-pom-handler))
252 (projects (javaimp--maven-xml-extract-projects xml-tree))
253 (modules (mapcar #'javaimp--maven-xml-parse-project projects))
254 ;; first module is always root
255 (tree (javaimp--maven-build-tree (car modules) nil modules file)))
257 (push tree javaimp-project-forest)))
258 (message "Loaded tree for %s" file)))
261 ;; Maven XML routines
263 (defun javaimp--maven-xml-effective-pom-handler ()
267 (goto-char (point-min))
268 (re-search-forward "<\\?xml\\|<projects?")
269 (match-beginning 0))))
273 (goto-char (point-min))
274 (re-search-forward "<\\(projects?\\)")
275 ;; corresponding close tag is the end of parse region
276 (search-forward (concat "</" (match-string 1) ">"))
278 (xml-parse-region start end)))
280 (defun javaimp--maven-xml-extract-projects (xml-tree)
281 "Analyzes result of `mvn help:effective-pom' and returns list
282 of <project> elements"
283 (let ((project (assq 'project xml-tree))
284 (projects (assq 'projects xml-tree)))
288 (javaimp--xml-children projects 'project))
290 (error "Neither <project> nor <projects> was found in pom")))))
292 (defun javaimp--maven-xml-parse-project (project)
293 (let ((build-elt (javaimp--xml-child 'build project)))
295 :id (javaimp--maven-xml-extract-id project)
296 :parent-id (javaimp--maven-xml-extract-id (javaimp--xml-child 'parent project))
297 ;; <project> element does not contain pom file path (we set :file slot later)
299 :final-name (javaimp--xml-first-child
300 (javaimp--xml-child 'finalName build-elt))
301 :packaging (javaimp--xml-first-child
302 (javaimp--xml-child 'packaging project))
303 :source-dir (file-name-as-directory
304 (javaimp-cygpath-convert-maybe
305 (javaimp--xml-first-child
306 (javaimp--xml-child 'sourceDirectory build-elt))))
307 :test-source-dir (file-name-as-directory
308 (javaimp-cygpath-convert-maybe
309 (javaimp--xml-first-child
310 (javaimp--xml-child 'testSourceDirectory build-elt))))
311 :build-dir (file-name-as-directory
312 (javaimp-cygpath-convert-maybe
313 (javaimp--xml-first-child (javaimp--xml-child 'directory build-elt))))
314 :modules (mapcar (lambda (module-elt)
315 (javaimp--xml-first-child module-elt))
316 (javaimp--xml-children (javaimp--xml-child 'modules project) 'module))
317 :dep-jars nil ; dep-jars is initialized lazily on demand
318 :load-ts (current-time))))
320 (defun javaimp--maven-xml-extract-id (elt)
322 :group (javaimp--xml-first-child (javaimp--xml-child 'groupId elt))
323 :artifact (javaimp--xml-first-child (javaimp--xml-child 'artifactId elt))
324 :version (javaimp--xml-first-child (javaimp--xml-child 'version elt))))
326 (defun javaimp--maven-xml-file-matches (file id parent-id)
327 (let* ((xml-tree (with-temp-buffer
328 (insert-file-contents file)
329 (xml-parse-region (point-min) (point-max))))
330 (project-elt (assq 'project xml-tree))
331 (tested-id (javaimp--maven-xml-extract-id project-elt))
332 (tested-parent-id (javaimp--maven-xml-extract-id (assq 'parent project-elt))))
333 ;; seems that the only mandatory component in tested ids is artifact, while
334 ;; group and version may be inherited and thus not presented in pom.xml
335 (let ((test (if (or (null (javaimp-id-group tested-id))
336 (null (javaimp-id-version tested-id))
337 (null (javaimp-id-group tested-parent-id))
338 (null (javaimp-id-version tested-parent-id)))
340 (message "File %s contains incomplete id, using lax match" file)
341 (lambda (first second)
342 (equal (javaimp-id-artifact first) (javaimp-id-artifact second))))
344 (and (funcall test tested-id id)
345 (funcall test tested-parent-id parent-id)))))
350 (defun javaimp--maven-call (pom-file target handler)
351 "Runs Maven target TARGET on POM-FILE, then calls HANDLER in
352 the temporary buffer and returns its result"
353 (message "Calling \"mvn %s\" on pom: %s" target pom-file)
355 (let* ((pom-file (javaimp-cygpath-convert-maybe pom-file))
357 ;; TODO check
in Maven output on Gnu/Linux
358 (let ((coding-system-for-read
359 (if (eq system-type 'cygwin) 'utf-8-dos)))
360 (process-file javaimp-mvn-program nil t nil "-f" pom-file target)))
361 (buf (current-buffer)))
362 (with-current-buffer (get-buffer-create javaimp-debug-buf-name)
364 (insert-buffer-substring buf))
365 (or (and (numberp status) (= status 0))
366 (error "Maven target \"%s\" failed with status \"%s\"" target status))
367 (goto-char (point-min))
370 (defun javaimp--maven-build-tree (this parent-node all file)
371 (message "Building tree for module: %s" (javaimp-module-id this))
373 ;; reliable way to find children is to look for modules with "this" as
375 (seq-filter (lambda (m) (equal (javaimp-module-parent-id m)
376 (javaimp-module-id this)))
378 (if (and (null children)
379 (equal (javaimp-module-packaging this) "pom"))
380 (progn (message "Skipping empty aggregate module: %s" (javaimp-module-id this))
382 ;; filepath was not set before, but now we know it
383 (setf (javaimp-module-file this) file)
385 (let* ((this-node (make-javaimp-node
389 ;; recursively build child nodes
391 (mapcar (lambda (child)
394 (javaimp--maven-get-submodule-file
395 child file (javaimp-module-modules this))))
396 (javaimp--maven-build-tree
397 child this-node all child-file)))
399 (setf (javaimp-node-children this-node) child-nodes)
402 (defun javaimp--maven-get-submodule-file (submodule parent-file rel-paths-from-parent)
403 ;; Seems that the only reliable way to match a module parsed from <project>
404 ;; element with module relative path taken from <modules> is to visit pom and
405 ;; check that id and parent-id matches
406 (let* ((parent-dir (file-name-directory parent-file))
407 (files (mapcar (lambda (rel-path)
409 (file-name-as-directory rel-path)
411 rel-paths-from-parent)))
414 (javaimp--maven-xml-file-matches
415 file (javaimp-module-id submodule) (javaimp-module-parent-id submodule)))
417 (error "Cannot find file for module: %s" (javaimp-module-id submodule)))))
422 (defun javaimp--maven-update-module-maybe (node)
423 (let ((module (javaimp-node-contents node))
425 ;; check if deps are initialized
426 (or (javaimp-module-dep-jars module)
427 (progn (message "Loading dependencies: %s" (javaimp-module-id module))
428 (setq need-update t)))
429 ;; check if any pom up to the top one has changed
433 (let ((checked (javaimp-node-contents tmp)))
434 (if (> (float-time (javaimp--get-file-ts (javaimp-module-file checked)))
435 (float-time (javaimp-module-load-ts module)))
437 (message "Reloading %s (pom changed)" (javaimp-module-id checked))
438 (setq need-update t))))
439 (setq tmp (javaimp-node-parent tmp))))
441 (let* ((new-dep-jars (javaimp--maven-fetch-dep-jars module))
442 (new-load-ts (current-time)))
443 (setf (javaimp-module-dep-jars module) new-dep-jars)
444 (setf (javaimp-module-load-ts module) new-load-ts)))))
446 (defun javaimp--maven-fetch-dep-jars (module)
447 (let* ((path (javaimp--maven-call (javaimp-module-file module)
448 "dependency:build-classpath"
449 #'javaimp--maven-build-classpath-handler))
450 (converted-path (javaimp-cygpath-convert-maybe path 'unix t))
451 (path-separator-regex (concat "[" path-separator "\n" "]+")))
452 (split-string converted-path path-separator-regex t)))
454 (defun javaimp--maven-build-classpath-handler ()
455 (goto-char (point-min))
456 (search-forward "Dependencies classpath:")
458 (thing-at-point 'line))
461 ;; Working with jar classes
463 (defun javaimp--get-jar-classes (file)
464 (let ((cached (cdr (assoc file javaimp-cached-jars))))
466 ;; create, load & put into cache
468 (make-javaimp-cached-jar
470 :read-ts (javaimp--get-file-ts file)
471 :classes (javaimp--fetch-jar-classes file)))
472 (push (cons file cached) javaimp-cached-jars))
473 ((> (float-time (javaimp--get-file-ts (javaimp-cached-jar-file cached)))
474 (float-time (javaimp-cached-jar-read-ts cached)))
476 (setf (javaimp-cached-jar-classes cached) (javaimp--fetch-jar-classes file))
478 (setf (javaimp-cached-jar-read-ts cached) (current-time))))
479 ;; return from cached
480 (javaimp-cached-jar-classes cached)))
482 (defun javaimp--fetch-jar-classes (file)
483 (message "Reading classes in file: %s" file)
485 (let ((coding-system-for-read (and (eq system-type 'cygwin) 'utf-8-dos)))
486 ;; on cygwin, "jar" is a windows program, so file path needs to be
487 ;; converted appropriately.
488 (process-file javaimp-jar-program nil t nil
489 ;; `jar' accepts commands/options as a single string
490 "tf" (javaimp-cygpath-convert-maybe file 'windows))
491 (goto-char (point-min))
492 (while (search-forward "/" nil t)
494 (goto-char (point-min))
496 (while (re-search-forward "\\(^[[:alnum:]._]+\\)\\.class$" nil t)
497 (push (match-string 1) result))
501 ;; Tree search routines
503 (defun javaimp--find-node (predicate)
504 (javaimp--find-node-in-forest javaimp-project-forest predicate))
506 (defun javaimp--select-nodes (predicate)
507 (javaimp--select-nodes-from-forest javaimp-project-forest predicate))
509 (defun javaimp--find-node-in-forest (forest predicate)
511 (dolist (tree forest)
512 (javaimp--find-node-in-tree tree predicate))))
514 (defun javaimp--find-node-in-tree (tree predicate)
516 (progn (if (funcall predicate (javaimp-node-contents tree))
518 (dolist (child (javaimp-node-children tree))
519 (javaimp--find-node-in-tree child predicate)))))
521 (defun javaimp--select-nodes-from-forest (forest predicate)
522 (apply #'seq-concatenate 'list
523 (mapcar (lambda (tree)
524 (javaimp--select-nodes-from-tree tree predicate))
527 (defun javaimp--select-nodes-from-tree (tree predicate)
529 (append (if (funcall predicate (javaimp-node-contents tree))
531 (apply #'seq-concatenate 'list
532 (mapcar (lambda (child)
533 (javaimp--select-nodes-from-tree child predicate))
534 (javaimp-node-children tree))))))
537 ;; Some API functions
539 ;; do not expose tree structure, return only modules
541 (defun javaimp-find-module (predicate)
542 (let ((node (javaimp--find-node predicate)))
544 (javaimp-node-contents node))))
546 (defun javaimp-select-modules (predicate)
547 (mapcar #'javaimp-node-contents
548 (javaimp--select-nodes predicate)))
554 (defun javaimp-add-import (classname)
555 "Imports classname in the current file. Interactively,
556 asks for a class to import, adds import statement and calls
557 `javaimp-organize-imports'. Import statements are not
558 duplicated. Completion alternatives are constructed based on
559 this module's dependencies' classes, JDK classes and top-level
560 classes in the current module."
563 (barf-if-buffer-read-only)
564 (let* ((file (expand-file-name
566 (error "Buffer is not visiting a file!"))))
567 (node (or (javaimp--find-node
569 (or (string-prefix-p (javaimp-module-source-dir m) file)
570 (string-prefix-p (javaimp-module-test-source-dir m) file))))
571 (error "Cannot find module by file: %s" file))))
572 (javaimp--maven-update-module-maybe node)
573 (let ((module (javaimp-node-contents node)))
574 (list (completing-read
577 ;; we're not caching full list of classes coming from module
578 ;; dependencies because jars may change and we need to reload
580 (let ((jars (append (javaimp-module-dep-jars module)
581 (javaimp--get-jdk-jars))))
582 (apply #'seq-concatenate 'list
583 (mapcar #'javaimp--get-jar-classes jars)))
584 (and javaimp-include-current-module-classes
585 (javaimp--get-module-classes module)))
586 nil t nil nil (symbol-name (symbol-at-point))))))))
587 (javaimp-organize-imports (cons classname 'ordinary)))
589 (defun javaimp--get-module-classes (module)
590 "Returns list of top-level classes in current module"
592 (let ((build-dir (javaimp-module-build-dir module)))
593 ;; additional source dirs
596 (let ((dir (concat build-dir (file-name-as-directory rel-dir))))
597 (and (file-accessible-directory-p dir)
598 (javaimp--get-directory-classes dir nil))))
599 javaimp-additional-source-dirs)))
601 (let ((dir (javaimp-module-source-dir module)))
602 (and (file-accessible-directory-p dir)
603 (javaimp--get-directory-classes dir nil)))
605 (let ((dir (javaimp-module-test-source-dir module)))
606 (and (file-accessible-directory-p dir)
607 (javaimp--get-directory-classes dir nil)))))
609 (defun javaimp--get-directory-classes (dir prefix)
611 ;; .java files in current directory
612 (mapcar (lambda (file)
613 (concat prefix (file-name-sans-extension (car file))))
614 (seq-filter (lambda (file) (null (cadr file))) ;only files
615 (directory-files-and-attributes dir nil "\\.java\\'" t)))
616 ;; descend into subdirectories
617 (apply #'seq-concatenate 'list
618 (mapcar (lambda (subdir)
619 (let ((name (car subdir)))
620 (javaimp--get-directory-classes
621 (concat dir (file-name-as-directory name)) (concat prefix name "."))))
622 (seq-filter (lambda (file)
623 (and (eq (cadr file) t) ;only directories
624 (null (member (car file) '("." "..")))))
625 (directory-files-and-attributes dir nil nil t))))))
628 ;; Organizing imports
631 (defun javaimp-organize-imports (&rest new-imports)
632 "Groups import statements according to the value of
633 `javaimp-import-group-alist' (which see) and prints resulting
634 groups leaving one blank line between groups.
636 If the file already contains some import statements, this command
637 rewrites them, starting with the same place. Else, if the the
638 file contains package directive, this command inserts one blank
639 line below and then imports. Otherwise, imports are inserted at
640 the beginning of buffer.
642 Classes within a single group are ordered in a lexicographic
643 order. Imports not matched by any regexp in `javaimp-import-group-alist'
644 are assigned a default order defined by
645 `javaimp-import-default-order'.
647 NEW-IMPORTS is a list of additional imports; each element should
648 be of the form (CLASS . TYPE), where CLASS is a string and TYPE
649 is `'ordinary' or `'static'. Interactively, NEW-IMPORTS is nil."
651 (barf-if-buffer-read-only)
653 (goto-char (point-min))
654 (let* ((old-data (javaimp--parse-imports))
655 (first (car old-data))
656 (last (cadr old-data))
657 (all-imports (append new-imports (cddr old-data))))
660 ;; delete old imports, if any
665 (delete-region first (point))))
666 (javaimp--prepare-for-insertion first)
668 (cl-delete-duplicates
670 :test (lambda (first second)
671 (equal (car first) (car second)))))
676 (let ((order (or (assoc-default (car import)
677 javaimp-import-group-alist
679 javaimp-import-default-order)))
680 (cons import order)))
684 (lambda (first second)
685 ;; sort by order, name
686 (if (= (cdr first) (cdr second))
687 (string< (caar first) (caar second))
688 (< (cdr first) (cdr second))))))
689 (javaimp--insert-imports with-order)))
690 (message "Nothing to organize!")))))
692 (defun javaimp--parse-imports ()
693 (let (first last list)
694 (while (re-search-forward "^\\s-*import\\s-+\\(static\\s-+\\)?\\([._[:word:]]+\\)" nil t)
695 (push (cons (match-string 2) (if (match-string 1) 'static 'ordinary)) list)
696 (setq last (line-beginning-position))
697 (or first (setq first last)))
698 (cons first (cons last list))))
700 (defun javaimp--prepare-for-insertion (start)
702 ;; if there were any imports, we start inserting at the same place
704 ((re-search-forward "^\\s-*package\\s-" nil t)
705 ;; if there's a package directive, insert one blank line below and
706 ;; leave point after it
711 ;; then insert one blank line and we're done
714 ;; otherwise, just go to bob
715 (goto-char (point-min)))))
717 (defun javaimp--insert-imports (imports)
718 (let ((static (seq-filter (lambda (elt)
719 (eq (cdar elt) 'static))
721 (ordinary (seq-filter (lambda (elt)
722 (eq (cdar elt) 'ordinary))
724 (javaimp--insert-import-group "import static %s;" static)
725 (and static ordinary (insert ?\n))
726 (javaimp--insert-import-group "import %s;" ordinary)))
728 (defun javaimp--insert-import-group (pattern imports)
730 (dolist (import imports)
731 ;; if adjacent imports have different order value, insert a newline
733 (let ((order (cdr import)))
735 (/= order last-order)
737 (insert (format pattern (caar import)) ?\n)
738 (setq last-order order)))))
742 ;;; javaimp.el ends here