;;; java-stuff.el --- Some java related elisp functions

;; Copyright (C) 2001, 2002 Alex Moffat
;; Keywords: java
;; Version: 1.5

;;; This file is not part of GNU Emacs.

;; This file 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 2, or (at your option)
;; any later version.

;; This file 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 GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; Purpose
;;
;; Just a collection, currently very small, of what I think are some
;; useful funtions when using emacs to edit java source code. There
;; are three functions to help organize the import statements at the
;; start of a program, and one function to insert debugging statements
;; using log4j.
;;
;; Installation:
;; 
;; Make the file accessible somewhere on your load path.
;;
;; Two possibilities.
;; 
;; 1. To access the functions using M-x add the autoload statements
;; below to your .emacs file.
;;
;; (autoload 'java-sort-imports "java-stuff" "" t)
;; (autoload 'java-check-imports "java-stuff" "" t)
;; (autoload 'java-delete-commented-imports "java-stuff" "" t)
;; (autoload 'java-sort-and-check-imports "java-stuff" "" t)
;; (autoload 'java-insert-debug "java-stuff" "" nil)
;;
;; 2. You can make the functions available as menu items in an
;; "Extras" menu under the Java menu in java mode. To do this you only
;; need to add the following to your .emacs file. This will also make
;; the functions available using M-x as for option 1.
;;
;; (autoload 'java-add-extras-menu "java-stuff" "" nil)
;; (add-hook 'java-mode-hook 'java-add-extras-menu)
;;
;; Customization:
;; 
;; You can change the value of the variable java-log4j-statement to
;; the name of the java variable that contains the
;; org.apache.log4j.Category instance to use for logging. It can also
;; be set to a method call, for example getLogCategory(), that returns
;; the Category instance to use.
;;
;; Change log:
;;
;; 20020102 ajm Changed hoist-forward to use delete-region instead of
;;              kill-region so that the kill ring is not modified when
;;              java-sort-imports is run.
;;
;; 20020102 ajm Added java-delete-commented-imports command to make it
;;              easier to get rid of the import statements that
;;              java-check-imports comments out.
;;
;; 20020102 ajm Added java-add-extras-menu to make the commands
;;              available as menu options when in java mode.
;;
;; 20020104 ajm Changed to use forward-line instead of
;;              beginning-of-line so that we ignore field boundaries
;;              if they are enabled.
;;
;; 20020104 ajm If deleting an import and the preceeding and following
;;              lines are blank then also delete the following
;;              line. This avoids introducing unwanted blank lines.
;;
;; 20020111 ajm Added functions to insert logging messages into java
;;              programs. Wrote more documentation.
;;
;; 20020212 ajm Improved java-sort-imports so that it comments out
;;              duplicate import statements and import statements that
;;              import classes that are referred to in commented out
;;              code.
;;

(defun java-get-import-region (import-regexp)
  "Find the region containing the import statements and return a
cons of its start and end position."
  (let ((import-regexp (if (equal (aref import-regexp 0) ?^)
			   import-regexp
			 (concat "^" import-regexp))))
    ;; Start at the end of the buffer
    (goto-char (point-max))
    ;; If no imports return empty list
    (if (not (re-search-backward import-regexp nil t))
	()
      ;; Find the end of the region containing all the imports and
      ;; record it
      (end-of-line)
      (let ((end-of-imports (+ (point) 1)))
	;; Now find the start of the "import region" and return the
	;; start and end positions.
	(goto-char (point-min))
	(re-search-forward import-regexp)
	(cons (match-beginning 0) end-of-imports)))))
  
(defun java-sort-imports (&optional no-message)
  "Sort the import statements at the top of a java program.  They are
grouped into three sets in the following order, those starting with
java, those starting with org, and those starting with com. Within
each set they are sorted alphabetically and blank lines are inserted
between packages."
  (interactive)
  (let* (;; Regexp to recognize import statements, which may be commented out.
	 ;; hoist-forwards appends a package name to the end of this to
	 ;; form a new regular expression so be careful changing it.
         (import-regexp "\\s-*\\(//\\)?\\s-*import\\s-+")
         ;; Function to move a block of import statements that all
         ;; start with hoist-import from their current position to
         ;; hoist-point. Must be no blank lines between import
         ;; statements when this is called.
         (hoist-forward
          (lambda (hoist-import hoist-point)
            (goto-char (point-min))
            (if (re-search-forward
                 (concat "^" import-regexp hoist-import)
                 nil
                 t)
                (progn
                  (forward-line 0)
                  (let ((start-of-region (point)))
                    (next-line 1)
                    (while (looking-at
                            (concat import-regexp hoist-import))
                      (next-line 1))
		    (let ((copied-text (buffer-substring start-of-region
							 (point))))
		      (delete-region start-of-region
				     (point))
		      (goto-char hoist-point)
		      (insert copied-text)))))))
         ;; Function to sort the import statements based on the names
         ;; being imported. All blank lines or lines that are not
         ;; import statements sort to the front.
         (sort-imports
          (lambda (start-of-sort-region end-of-sort-region)
            (save-restriction
              (narrow-to-region start-of-sort-region
                                end-of-sort-region)
              (goto-char (point-min))
              (sort-subr
               nil
               ;; start of next record
               (function (lambda ()
                           (next-line 1)
                           (forward-line 0)))
               ;; end of current record
               (function (lambda ()
                           (end-of-line)))
	      ;; key of record, this is the name imported if an import
               ;; statement is found, otherwise it is blank
               (function (lambda ()
                           (let ((here (point)))
                             (end-of-line)
                             (let ((limit (point)))
                               (goto-char here)
                               (if (re-search-forward (concat "^" import-regexp) limit t)
                                   (buffer-substring (match-end 0) limit)
                                 "")))))))))
         ;; Reformat a region of the current buffer that contains
         ;; import statements. This sorts the statements and inserts
         ;; blank lines between packages.
         (reformat-region
          (lambda (start-of-sort-region end-of-sort-region)
            ;; sort all the import statements based on the package
            ;; names
            (funcall sort-imports start-of-sort-region end-of-sort-region)
            ;; remove the non import lines that sorted to the top
            (goto-char start-of-sort-region)
            (re-search-forward (concat "^" import-regexp))
            (forward-line 0)
            (delete-region start-of-sort-region (point))
            ;; move the org stuff to the front
            (funcall hoist-forward "org"
                     start-of-sort-region)
            ;; move the java stuff to the front (so now it
            ;; has java in front of org in front of com)
            (funcall hoist-forward "java"
                     start-of-sort-region)
            ;; back to the beginning
            (goto-char start-of-sort-region)
            ;; put blank lines between the packages
            (let ((last-package "")
                  (this-package "")
                  (start-of-package 0))
              (while (looking-at import-regexp)
                (setq start-of-package (+ (match-end 0) 1))
                (end-of-line)
                (search-backward "." start-of-package)
                (setq this-package
                      (buffer-substring
                       start-of-package (point)))
                (if (not (equal last-package this-package))
                    (progn
                      (if (not (equal last-package ""))
                          (progn
                            (forward-line 0)
                            (newline)))
                      (setq last-package this-package)))
                (next-line 1)
                (forward-line 0))))))
    (save-excursion
      (let ((import-region (java-get-import-region import-regexp)))
	(if (null import-region)
	    ()
          (let ( ;; beginning of region containing imports
		(start-of-sort-region (car import-region))
		;; end of region containing imports
		(end-of-sort-region (cdr import-region))
                ;; current buffer 
                (old-buf (current-buffer))
                ;; temp buffer for sorting in, name starts with space
                ;; so it won't be listed to user
                (temp-buf (generate-new-buffer " sort-imports"))
		;; was the buffer modified
		(modified nil))
            ;; go to the temp buffer, insert the import statements and
            ;; reformat/sort them. we rely on save-excursion to always
            ;; get us back to the correct buffer
            (set-buffer temp-buf)
            (insert-buffer-substring old-buf start-of-sort-region
                                     end-of-sort-region)
            (funcall reformat-region (point-min) (point-max))
            ;; are the newly sorted statements the same as before
            ;; sorting?
            (if (equal
                 (compare-buffer-substrings
                  (current-buffer) (point-min) (point-max)
                  old-buf start-of-sort-region end-of-sort-region)
                 0)
                ;; yes, nothing changed
		(or no-message
		    (message "Already sorted. No changes made to buffer."))
              ;; no, so remove the unsorted ones and replace with
              ;; the sorted ones
              (set-buffer old-buf)
              (delete-region start-of-sort-region end-of-sort-region)
              (goto-char start-of-sort-region)
              (insert-buffer temp-buf)
              (or no-message
		  (message "Sorting completed. Buffer has been modified"))
	      (setq modified t))
            ;; get rid of the temp buffer
            (kill-buffer temp-buf)
	    modified))))))

(defun java-check-imports (&optional no-message)
  "Check the import statements in a program and comment out any where the class name
can not be found in the program. Can not deal with imports that end in * so they are
ignored. Returns t if the buffer was modified, nil otherwise."
  (interactive)
  ;; Don't loose our place.
  (save-excursion
    (let* ( ;; Regexp to recognize import statements that are not commented out
	   (import-regexp "^\\s-*import\\s-+\\([^ ]+\\.\\([^;]+\\)\\);")
	   ;; The region containing the imports
	   (import-region (java-get-import-region import-regexp)))
      ;; if no imports then do nothing and return nil 
      (if (null import-region)
	  nil
	(let* (	;; was the buffer modified
	       (modified nil)
	       ;; Name of the last package seen
	       (last-package-name "")
	       ;; Don't ignore case when searching
	       (case-fold-search nil)
	       ;; start of import region
	       (start-of-region (car import-region))
	       ;; end of import region
	       (end-of-region (cdr import-region))
	       ;; Function to comment out a line if its not already
	       ;; commented out.
	       (comment-line (lambda (n)
			       (let ((pos (point)))
				 (forward-line n)
				 (if (not (looking-at "^\\s-*//"))
				     (progn
				       (insert "//")
				       (setq modified t)))
				 (goto-char pos))))
	       ;; Function to find non commented out class name
	       (find-non-commented
		(lambda (class-name)
		  (if (re-search-forward
		       (concat "\\Sw\\(" class-name "\\)\\Sw")
		       (point-max) t)
		      (if (equal (get-text-property (match-beginning 1) 'face)
				 'font-lock-comment-face)
			  (funcall find-non-commented class-name)
			t)
		    nil))))
	  ;; Start at the beginning.
	  (goto-char start-of-region)
	  ;; While there are still import statements.
	  (while (re-search-forward import-regexp end-of-region t)
	    ;; Grab the package and class names from the buffer.
	    (let ((package-name (match-string-no-properties 1))
		  (class-name (match-string-no-properties 2)))
	      ;; If same package as last seen comment out previous
	      ;; one. This means check of rest of program done more
	      ;; than once but easier to program.
	      (if (equal package-name last-package-name)
		  (funcall comment-line -1))
	      ;; Don't bother with *.
	      (if (not (equal class-name "*"))
		  ;; Remember where we are.
		  (let ((pos (point)))
		    ;; go to the end of the import region
		    (goto-char end-of-region)
		    ;; See if the class is referenced anywhere.
		    (let ((found (funcall find-non-commented class-name)))
		      ;; point moved and we need to move back.
		      (goto-char pos)
		      ;; Didn't find one so comment out the current
		      ;; line.
		      (if (not found)
			  (funcall comment-line 0)))))
	      ;; Remember this package
	      (setq last-package-name package-name))
	    ;; Start searching again from the next line
	    (forward-line 1))
	  (or no-message
	      (if modified
		  (message "Check completed. Buffer has been modified")
		(message "Check completed. No changes made to buffer")))
	  modified)))))

(defun java-delete-commented-imports (&optional no-message)
  "Delete any import statements that have been commented out by
java-check-imports."
  (interactive)
  (let ( ;; Regexp to recognize import statements commented out by
        ;; java-check-imports
        (import-regexp "\\s-*//\\s-*import\\s-+[^ ]+\\.\\([^;]+\\);")
        ;; was the buffer modified
        (modified nil)
        (limit (point-max)))
    ;; Don't loose our place
    (save-excursion
      ;; Start at the beginning
      (goto-char (point-min))
      ;; While there are still commented out import statements
      (while (re-search-forward import-regexp limit t)
        ;; After the regexp matches point is at the end of the line
        ;; Move to start of previous line
        (forward-line -1)
        ;; Is this line blank
        (let ((blank-before (looking-at "^\\s-*$")))
          ;; Go to start of line to be deleted
          (forward-line 1)
          ;; Record position
          (let ((pos (point)))
            ;; Go to start of next line
            (forward-line 1)
            ;; If preceeded by a blank line and this line
            ;; is blank then go forward another line.
            (if (and blank-before
                     (looking-at "^\\s-*$"))
                (forward-line 1))
            ;; Delete the commented out line and
            ;; possibly the following blank line
            (delete-region (point) pos))
          (setq modified t))))
    (or no-message
        (if modified
            (message "Delete completed. Buffer has been modified")
          (message "Delete completed. No changes made to buffer")))
    modified))
  
(defun java-sort-and-check-imports ()
  "Sort the import statements in the program and then check them to
comment out any that are not used."
  (interactive)
  (let ((modified (java-sort-imports t)))
    (if (or (java-check-imports t)
	    modified)
	(message "Buffer was modified")
      (message "Buffer not modified"))))

(defvar java-log4j-statement
  "logCat"
  "*Name of the java variable holding the instance of
org.apache.log4j.Category that should be used for logging. Can also be
a method call that returns a Category. Must support isDebugEnabled and
debug methods.")

(defun java-insert-debug ()
  "Insert statements to write a debugging message using log4j."
  (interactive)
  (end-of-line)
  (newline-and-indent)
  (insert "if (" java-log4j-statement ".isDebugEnabled())")
  (newline-and-indent)
  (insert java-log4j-statement ".debug(")
  (java-insert-more-debug (read-string "Enter msg: " "") t)
  (insert ");"))

(defun java-insert-more-debug (msg &optional firstTime)
  "Insert an additional debugging message with a plus between it and
any previous messages."
  (if (or (null msg)
          (eq (length msg) 0))
      ()
    (if (not firstTime)
        (progn
          (insert " + ")
          (newline-and-indent)))
    (insert msg)
    (java-insert-more-debug (read-string "More msg or enter to end: " ""))))

;; The keymap used for the extras menu
(defvar java-extras-menu
  (make-sparse-keymap "Extras")
  "The menu that presents some extra java mode functionality")

;; Adding the menu items to the extras menu.
(define-key java-extras-menu [delete-commented-imports]
  '(menu-item "Delete Commented Imports" java-delete-commented-imports
	      :key-sequence nil
	      :help "Delete import statements that are commented out"))

(define-key java-extras-menu [sort-and-check-imports]
  '(menu-item "Sort and Check" java-sort-and-check-imports
	      :key-sequence nil
	      :help "Sort the import statements and check for unused ones"))

(define-key java-extras-menu [check-imports]
  '(menu-item "Check Imports" java-check-imports
	      :key-sequence nil
	      :help "Check for unused import statements"))

(define-key java-extras-menu [sort-imports]
  '(menu-item "Sort Imports" java-sort-imports
	      :key-sequence nil
	      :help "Sort the import statements"))

(define-key java-extras-menu [debug-statement]
  '(menu-item "Insert log4j" java-insert-debug
              :key-sequence nil
              :help "Insert a log4j debugging statement."))

(defun java-add-extras-menu ()
  "Add the extras menu if it is not already present. Should normally
be invoked from java-mode-hook."
  (let ((java-menu (lookup-key java-mode-map [menu-bar Java])))
    (or (lookup-key java-menu [extras])
	(define-key-after java-menu
	  [extras] (cons "Extras" java-extras-menu) t))))