Eigenes Refactoring-Tool für Eclipse schreiben

◄== zurück zur Übersichtsseite


Refactoring-Tools gibt es für sehr unterschiedliche Einsatzzwecke. Trotzdem kommt es vor, dass zu einer speziellen Aufgabe noch kein Tool existiert. Spätestens dann ist die Zeit reif dafür, ein eigenes Tool zu schreiben.

Eclipse ist eine Sammlung aus Plugins. Die erste Regel von Eclipse lautet "Everything is a contribution". Das bedeutet, dass alles Sichtbare in der Eclipse Workbench - Views, Editoren, Buttons und Menüs - durch Plugins realisiert wurde. Es gibt nur einen kleinen Kernel, der als Plugin-Loader dient. Wer ein eigenes Refactoring-Tool schreiben will, muss es als ein Eclipse Plugin implementieren. Plugins in Eclipse können andere bereits installierte Plugins erweitern. Dafür können Plugins sogenannte Extension Points definieren, an welchen andere Plugins andocken können, um die Funktionalität des Wirt-Plugins zu nutzen.[1]

Für weitere Informationen zu Eclipse Plugins sehen Sie sich auch die Artikelreihe Eclipse Plugins in diesem Wiki an.

Das Ziel

Wir wollen hier ein Refactoring-Tool etnwickeln, welches eine Java-Methode umbenennen kann. Es entspricht in den Grundzügen dem Standard-Refactoring-Tool Rename.
Im Vergleich zu Rename fehlen diesem Tool einige Optionen (z.B. "Keep original method as delegate to renamed method" und "Mark as deprecated") und es funktioniert nur für Methoden.

Die Struktur des Plugin-Projekts

Abb. 1: Die Projektstruktur des Refactoring-Konstrukts ExampleRefactoring

Die Struktur des Beispielprojekts feu.progsys.S01919.refactoring sieht wie folgt aus:

  • Den Rahmen bildet das gleichnamige Eclipse Plugin, welches hier durch die beiden Dateien MANIFEST.MF und plugin.xml repräsentiert wird. Beim Start der Eclipse Workbench werden zuerst nur diese beiden Dateien geladen; die restlichen Module werden erst dann geladen, wenn sie zum ersten Mal benutzt werden (Lazy Evaluation).
  • feu.progsys.S01919.refactoring
    • RefactoringPlugin
      ist die Singelton Klasse des Plugins. Sie wird als erste geladen (noch vor der Action) und liefert den Zugriff auf die Eclipse Workbench und die Shell.
  • feu.progsys.S01919.refactoring.actions
    • ExampleRefactoringAction
      Zum Start unseres Refactorings wird ein Einstiegspunkt benötigt - eine Aktion des Users. Einen idealen Einstiegspunkt bietet eine Action, welche beim Klick auf einen Button oder einen (Kontext-)Menüeintrag ausgelöst wird.
  • feu.progsys.S01919.refactoring.refactoring
    • ExampleRefactoring
      implementiert das Refactoring - eine Subklasse von org.eclipse.ltk.core.refactoring.Refactoring.
    • ExampleRefactoringContribution
      implementiert die optionale RefactoringContribution
    • ExampleRefactoringDescriptor
      implementiert den optionalen RefactoringDescriptor
  • feu.progsys.S01919.refactoring.ui
    Das Benutzerinterface (GUI) implementieren die beiden Klassen:
    • ExampleRefactoringWizard
      implementiert den Wizard
    • ExampleRefactoringWizardInputPage
      implementiert die erste Seite des Wizards


▲___zum Seitenanfang

Das Plugin

Eclipse bietet für die beiden Konfigurationsdateien MANIFEST.MF und plugin.xml einen komfortablen Editor. Alternativ können aber auch die Textdateien direkt editiert werden. Wir werden in diesem Beispiel den Editor benutzen. Dazu führen wir einen Doppelklick auf eine dieser Dateien aus.

Dependencies

Als Erstes müssen wir in unserem Plugin die Abhängigkeiten von anderen Plugins festlegen, welche wir benutzen werden.

Abb. 2: Dependencies des Beispiel-Refactoring- Plugins

Die ersten drei Plugins

  • org.eclipse.ui
  • org.eclipse.jdt.core
  • org.eclipse.core.runtime

sind Standard Plugins des JDT - Java Development Tooling, welche fast alle Plugins automatisch inkludieren (wird vom Plug-in Project-Wizard generiert). Die beiden letzteren

  • org.eclipse.ltk.core.refactoring
  • org.eclipse.ltk.ui.refactoring

sind die beiden Bestandteile des LTK - (Refactoring) Langauge Tool Kit und werden nur in Plugins inkludiert, die eigene Refactoring-Tools implementieren oder fremde benutzen.

Extensions

Als zweites müssen wir definieren, wie wir die inkludierten Plugins erweitern wollen, oder in der Eclipse-Sprache ausgedrückt, welche eigenen Extensions wir für die vorhandenen Extension Points der Standard Plugins schreiben wollen.

Abb. 3: Extensions des Beispiel-Refactoring-Plugins

Im Plugin Editor wechseln wir auf die Karteikarte Extensions. Dort finden wir eine Erweiterung (objectContribution) des Extension Points org.eclipse.ui.popupMenus:

  • exampleRefactoring.methods

Mit Hilfe dieses Extension Points wird es dem Entwickler ermöglicht, einen eigenen Eintrag in das Kontextmenü hinzuzufügen. Das Kontextmenü erscheint, wenn man einen Rechtsklick an einer definierten Stelle in der Eclipse Workbench macht. In unserem Fall legen wir mit der Object contribution "exampleRefactoring.methods" fest, dass uns nur ein bestimmtes Kontextmenü interessiert, nämlich das Refactoring-Menü für Java-Methoden (Rechtsklick auf eine Java-Methode in der Outline oder im Package Explorer).

Abb. 4: Extension Element Details einer objectContribution-Extension

Dass unser Menüeintrag nur im Kontextmenü der Java Methoden erscheinen darf, legen wir im rechten Teil des Editors fest, in den Extension Element Details der objectContribution-Extension. Das Attribut objectClass: org.eclipse.jdt.core.IMethod legt fest, dass dieser Menüeintrag nur bei Java Elementen erscheint, die dieses Interface implementieren.

Zu beachten ist, dass nur das Kontextmenü beim Rechtsklick im Package Explorer oder in der Outline unterstützt wird - nicht beim Rechtsklick auf Methoden im Java Editor

Abb. 5: Extension Element Details einer action-Extension

Der eigentliche Menüeintrag Beispiel-Refactoring... (action) steht eingerückt unter der objectContribution-Exension.
Hier die Bedeutung der Attribute:

  • label - der Text, der im Kontextmenü erscheinen soll
  • class - die Implementierung der Action-Klasse, die auf den Klick im Menü reagieren soll
  • menubarPath - die genaue Adresse des Kontextmenüs. Hier das Kontextmenü Refactroing (org.eclipse.jdt.ui.refactoring.menu) und der Abschnitt codingGroup
  • enablesFor - so viele Elemente (Klassen, Methoden, Felder) müssen markiert sein, damit der Eintrag im Kontextmenü erscheint. (Default ist 1)


▲___zum Seitenanfang

Action!

Im Abschnitt "Extensions" haben wir gesehen, dass jeder Eintrag im Kontextmenü (action) auf eine Action-Klasse verweist (Attribut class), die ausgeführt wird, sobald der User auf den Menüeintrag klickt. Im unserem Beispiel ist dies die Java-Klasse ExampleRefactoringAction. Diese Klasse ist unser Einstiegspunkt in das Programm. Zuerst werden die Methoden setActivePart und selectionChanged aufgerufen (den optionalen Constructor nicht mitgezählt). Diese Methoden liefern Informationen über das ausgewählte Objekt, zu dem das Kontextmenü Refactoring aufgerufen wurde.
Danach wird die Hauptmethode run aufgerufen.

public class ExampleRefactoringAction implements IObjectActionDelegate {
 
...
 
01 @Override
02 public void run(IAction action) {
03	if (!(selection instanceof IStructuredSelection)) return;
04	
05	Object selFirtsElement = ((IStructuredSelection)selection).getFirstElement();
06	if (!(selFirtsElement instanceof IMethod)) return;07
08	ExampleRefactoring refactoring = new ExampleRefactoring();
09	refactoring.setMethod((IMethod)selFirtsElement);10
11	ExampleRefactoringWizard refactoringWizard = new ExampleRefactoringWizard(refactoring);12	RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(refactoringWizard);
13	try {
14		op.run(getShell(), "Beispiel-Refactoring");15	}
16	catch (InterruptedException e) {
17	}
18 }
 
...
 
}

In den Zeilen 03 bis 06 wird geprüft, ob es sich bei der Selektion um eine gültige Auswahl handelt. Berücksichtigt werden nur Java Methoden (implementieren oder erben das Interface org.eclipse.jdt.core.IMethod).

In den Zeilen 08 und 09 wird die Refactoring-Klasse ExampleRefactoring instanziiert und die ausgewählte Methode wird übergeben.

In Zeile 11 wird der Refactoring-Wizard mit dem Refactoring-Objekt initialisiert. In den Zeilen 12 und 14 wird das Wizard Objekt an eine Wizard-Helper-Klasse RefactoringWizardOpenOperation übergeben und mit run aufgerufen. Die Helper-Klasse checkt eingangs die Vorbedingungen und öffnet je nach Ergebnis entweder den Wizard oder einen Fehlerdialog.

▲___zum Seitenanfang

Klasse Refactoring erweitern

Die Refactoring-Klasse ExampleRefactoring ist die Hauptklasse unseres Refactoring-Tools. ExampleRefactoring erweitert die Framework Basisklasse org.eclipse.ltk.core.refactoring.Refactoring und wird in der Action mit dem ausgewählten Java-Element (Class, Method oder Field) initialisiert und an den Refactoring-Wizard übergeben.

In unserem Beispiel benennen wir eine Java Methode um.

Die Refactoring-Klasse implementiert die vier abstrakten Methoden der Oberklasse:

Methode checkInitialConditions

Validierung der Vorbedingungen bevor der Wizard angezeigt wird

/**
 * Validiere die Vorbedingungen: überprüfe
 * - ob Methode ungleich null ist
 * - ob die Methode existiert
 * - ob die Methode nicht aus einer binären Klasse (.class File) stammt und 
 *   die Klassendatei keine Compile-Fehler aufweist
 */
@Override
public RefactoringStatus checkInitialConditions(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
   RefactoringStatus status = new RefactoringStatus();
   try {
     monitor.beginTask("Checking preconditions...", 1);
 
      // überprüfe, ob Methode ungleich null ist
      if (fMethod  == null)         status.merge(RefactoringStatus.createFatalErrorStatus("Method has not been specified."));      // ob die Methode existiert
      else if (!fMethod.exists())         status.merge(RefactoringStatus.createFatalErrorStatus(MessageFormat.format("Method ''{0}'' does not exist.", new Object[] { fMethod.getElementName()})));	      // ob die Methode nicht aus einer binären Klasse (.class File) stammt und 
      // die Klassendatei keine Compile-Fehler aufweist
      else {
         if (!fMethod.isBinary() && !fMethod.getCompilationUnit().isStructureKnown())            status.merge(RefactoringStatus.createFatalErrorStatus(MessageFormat.format("Compilation unit ''{0}'' contains compile errors.", new Object[] { fMethod.getCompilationUnit().getElementName()})));	 }   } finally {
      monitor.done();
   }
   return status;
}
In dieser Methode werden die Vorbedingungen validiert. Enthält der Rückgabewert RefactoringStatus schwerwiegende Fehler, dann erscheint nicht der Wizard, sondern ein Fehler-Dialog.

Hauptsächlich wird gecheckt, ob die Methode nicht null und vorhanden ist und ob die Klassendatei Kompilierungsfehler aufweist.

▲___zum Seitenanfang

Methode checkFinalConditions

Validierung der Nachbedingungen, bevor tatsächliche Änderungen erfolgen. Die geplanten Änderungen werden vorerst nur berechnet. Ein Großteil dieser Berechnungen kann später für die tatsächlichen Änderungen des Sourcecodes in createChange() benutzt werden.

In dieser Methode bzw. in den privaten Methoden, die von hier aufgerufen werden, steckt der Großteil der Refactoring-Logik.

001 /**
002  * Validiere die Nachbedingungen. Dafür werden die Änderungen für createChange() bereits hier berechnet:
003  * 1. Erstelle eine Tabelle der Änderungen pro .java-Datei (CompilationUnit)
004  * 2. Erstelle eine Suchanfrage für eine Workspace-weite Suche nach allen Referenzen der Methode
005  * 3. Baue aus dem Suchergebnis eine Tabelle der Suchtreffer pro CompilationUnit (.java-Datei) auf
006  * 4. Baue aus der Tabelle units (Tabelle der Suchtreffer pro CompilationUnit) eine Tabelle
007  *    der CompilationUnits pro Projekt
008  * 5. Erstelle pro CompilationUnit einen AST (Abstract Syntax Tree) mit Hilfe eines ASTRequestors
009  * 6. Parse jedes Projekt, in dem die gesuchte Methode oder ihre Referenzen vorkommen.
010  *    Für jede CompilationUnit das Projekts wird ein AST mit Hilfe des ASTRequestor erstellt
011  */
012 @Override
013 public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
014    final RefactoringStatus status = new RefactoringStatus();
015    try {
016      monitor.beginTask("Checking preconditions...", 2);
017		
018      // 1. Erstelle eine Tabelle der Änderungen pro .java-Datei (CompilationUnit)019	fChanges = new LinkedHashMap<ICompilationUnit, TextFileChange>();			020
021	// 2. Erstelle eine Suchanfrage für eine Workspace-weite Suche nach allen Referenzen der Methode022	final Set<SearchMatch> invocations = new HashSet<SearchMatch>();023	if (fUpdateReferences) {024         IJavaSearchScope scope = SearchEngine.createWorkspaceScope();025         SearchPattern pattern = SearchPattern.createPattern(fMethod, IJavaSearchConstants.ALL_OCCURRENCES, SearchPattern.R_EXACT_MATCH);026         SearchEngine engine = new SearchEngine();027 	   engine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant()}, scope, new SearchRequestor() {028         @Override029	   public void acceptSearchMatch(SearchMatch match) throws CoreException {030           if (match.getAccuracy()  == SearchMatch.A_ACCURATE && !match.isInsideDocComment())031              invocations.add(match);032	     }033         }, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));034	}035

In Zeile 19 erstellen wir eine Map, die pro CompilationUnit (der Inhalt einer .java-Datei: Package, Imports, Klassen) eine Liste der geplanten Änderungen speichern soll.
In den Zeilen 22 bis 34 definieren wir eine Suchanfrage und durchsuchen damit den gesamten Workspace mit einer Suchmaschine. Wir suchen nach allen Vorkommnissen (IJavaSearchConstants.ALL_OCCURRENCES) der Methode, welche wir umbenennen wollen.

036	// 3. Baue aus dem Suchergebnis eine Tabelle der Suchtreffer pro CompilationUnit (.java-Datei) auf
037	final Map<ICompilationUnit, Collection<SearchMatch>> units = new HashMap<ICompilationUnit, Collection<SearchMatch>>();038	units.put(fMethod.getCompilationUnit(), new ArrayList<SearchMatch>());039	units.put(fType.getCompilationUnit(), new ArrayList<SearchMatch>());040
041	if (fUpdateReferences) {
042   	   for (SearchMatch match : invocations) {
043		Object element = match.getElement();
044		if (element instanceof IMember) {
045		   ICompilationUnit unit = ((IMember) element).getCompilationUnit();
046		   if (unit != null) {
047		      Collection<SearchMatch> collection = units.get(unit);
048		      if (collection == null) {
049		         collection = new ArrayList<SearchMatch>();
050		         units.put(unit, collection);051		      }
052		      collection.add(match);053		   }
054	        }
055	    }
056	}
057

Alle Vorkommnisse der Methode (Deklarationen und Aufrufe) sortieren wir zuerst nach CompilationUnits in der Map units.

058	// 4. Baue aus der Tabelle units (Tabelle der Suchtreffer pro CompilationUnit) eine Tabelle
059	// der CompilationUnits pro Projekt
060	final Map<IJavaProject, Collection<ICompilationUnit>> projects = new HashMap<IJavaProject, Collection<ICompilationUnit>>();061	for (ICompilationUnit unit : units.keySet()) {
062	   IJavaProject project = unit.getJavaProject();
063	   if (project != null) {
064	      Collection<ICompilationUnit> collection = projects.get(project);
065	      if (collection  == null) {
066	         collection = new ArrayList<ICompilationUnit>();
067		 projects.put(project, collection);068	      }
069	      collection.add(unit);070	   }
071	}
072

Jetzt sortieren wir die CompilationUnits nach den Projekten im Workspace in die Map projects.

073	// 5. Erstelle pro CompilationUnit einen AST(Abstract Syntax Tree) mit Hilfe eines ASTRequestors
074	ASTRequestor astRequestor = new ASTRequestor() {075	   @Override
076	   public void acceptAST(ICompilationUnit source, CompilationUnit ast) {077		try {
078		   // jede CompilationUnit wird nach dem vorgegebenen Algorithmus geändert
079	 	   rewriteCompilationUnit(this, source, units.get(source), ast, status);080		} catch (CoreException exception) {
081		   RefactoringPlugin.log(exception);
082		}
083	   }
084	};
085

Jetzt wird ein ASTRequestor erstellt - ein Hilfsobjekt für die Abfrage eines AST (Abstract Syntax Tree). Der AST parst die CompilationUnits und erstellt im Speicher eine Baumstruktur eines Java Programms.
Der ASTRequestor wird in Zeile 74 als eine anonyme Klasse erstellt, dabei wird die abstrakte Methode acceptAST implementiert. Nach dem Visitor Muster wird hier der AST übergeben und an die private Methode rewriteCompilationUnit in Zeile 79 weitergereicht. Angestoßen wird dieser Prozess aber erst später (Zeile 98).

086	// 6. Parse jedes Projekt, in dem die gesuchte Methode oder ihre Referenzen vorkommen.
087	// Für jede CompilationUnit das Projekts wird ein AST mit Hilfe des ASTRequestor erstellt
088	IProgressMonitor subMonitor = new SubProgressMonitor(monitor, 1);
089	try {
090	   final Set<IJavaProject> set = projects.keySet();
091	   subMonitor.beginTask("Compiling source...", set.size());
092
093	   for (IJavaProject project : set) {
094	      ASTParser parser = ASTParser.newParser(AST.JLS3);
095	      parser.setProject(project);096	      parser.setResolveBindings(true);097	      Collection<ICompilationUnit> collection = projects.get(project);
098           parser.createASTs(collection.toArray(new ICompilationUnit[collection.size()]), new String[0], astRequestor, new SubProgressMonitor(subMonitor, 1));099	   }
100	} finally {
101	   subMonitor.done();
102	}
103   } finally {
104      monitor.done();
105   }
106   return status;
107 }

Die Liste der Projekte wird durchlaufen und für jedes wird ein ASTParser erzeugt. In Zeile 98 wird das vorher vorbereitete ASTRequestor Objekt übergeben.
Die Methode createASTs ruft die Methode ASTRequestor.acceptAST auf. Diese wiederum ruft unsere private Methode rewriteCompilationUnit auf, die den AST pro CompilationUnit überschreibt und so das Umbenennen der Methode ermöglicht.

Methode rewriteCompilationUnit

Überschreibt den AST einer CompilationUnit.

/**
 * Ändert eine CompilationUnit, indem sie den passenden AST überschreibt
 * 
 * @param requestor Mit Hilfe des Requestors wird ein AST für die CompilationUnit erstellt
 * @param unit Die CompilationUnit, die geändert weerden soll
 * @param matches Liste der Referenzen der gesuchten Methode 
 * @param node Der aktuelle Knoten des AST
 * @param status Status Objekt
 * @throws CoreException
 */
protected void rewriteCompilationUnit(ASTRequestor requestor, ICompilationUnit unit, Collection<SearchMatch> matches, CompilationUnit node, RefactoringStatus status) throws CoreException {
	ASTRewrite astRewrite = ASTRewrite.create(node.getAST());
 
	for (SearchMatch match : matches) {
		if (match.getAccuracy()  == SearchMatch.A_ACCURATE) {
			ASTNode result = NodeFinder.perform(node, match.getOffset(), match.getLength());
			if (result instanceof MethodInvocation)
				status.merge(rewriteMethodInvocation(requestor, astRewrite, (MethodInvocation)result));			else if (result instanceof MethodDeclaration)
				status.merge(rewriteMethodDeclaration(requestor, astRewrite, (MethodDeclaration)result));			else if (result instanceof SimpleName)
			{
				if (result.getParent() instanceof MethodDeclaration)				status.merge(rewriteMethodDeclaration(requestor, astRewrite, (MethodDeclaration)result.getParent()));			}
		}
	}
	rewriteAST(unit, astRewrite);}

In dieser Methode werden die Vorkommnisse der umzubenennenden Methode pro CompilationUnit nach Typ untersucht. Handelt es sich um einen Methoden-Aufruf (MethodInvocation), so wird an die private Methode rewriteMethodInvocation weitergegeben. Bei einer Methoden-Definition (MethodDeclaration) ruft man die private Methode rewriteMethodDeclaration auf. Beide Methoden halten ihre Änderungen im Helper Objekt ASTRewrite fest. Am Ende wird von der privaten Methode rewriteAST daraus die Tabelle der Änderungen fChanges befüllt. Auf Basis dieser Änderungstabelle wird von createChange das Change Objekt erzeugt. ▲___zum Seitenanfang

Methode rewriteMethodInvocation

Überschreibt einen einzigen Methoden-Aufruf.

/**
 * Überschreibt einen Methoden-Aufruf
 * @param requestor Das requestor Objekt managed die ASTs für eine CompilationUnit / enthält die Bindings 
 * @param astRewrite Das ASTRewrite Hilfsobjekt, zum Überschreiben des ASTs
 * @param oldInvocation Der Methoden-Aufruf der überschrieben werden soll
 * @return RefactoringStatus
 * @throws JavaModelException
 */	
private RefactoringStatus rewriteMethodInvocation(ASTRequestor requestor, ASTRewrite astRewrite, MethodInvocation oldInvocation) throws JavaModelException {
	RefactoringStatus status = new RefactoringStatus();
 
	ITypeBinding declaringBinding = null;
	IBinding[] bindings = requestor.createBindings(new String[] {fType.getKey()});	if (bindings[0] instanceof ITypeBinding) {
		declaringBinding = (ITypeBinding) bindings[0];
	}
	if (declaringBinding == null)
		return status;
 
	AST ast = oldInvocation.getAST(); 
	MethodInvocation newInvocation = ast.newMethodInvocation();	newInvocation.setName(ast.newSimpleName(getMethodName())); 
	astRewrite.replace(oldInvocation, newInvocation, null);	return status;
}

In dieser Methode werden zuerst die Bindings der Methode untersucht. Danach wird, basierend auf dem AST des alten Aufrufs, ein neuer Methoden-Aufruf erzeugt. Dieser erhält den neuen Methodennamen. Zum Schluss wird der alten Aufruf gegen den neuen ausgetauscht. ▲___zum Seitenanfang

Methode rewriteMethodDeclaration

Überschreibt eine einzige Methoden-Deklaration.

/**
 * Überschreibt eine Methoden-Deklaration
 * @param requestor Das requestor Objekt managed die ASTs für eine CompilationUnit / enthält die Bindings 
 * @param astRewrite Das ASTRewrite Hilfsobjekt, zum Überschreiben des ASTs
 * @param oldDeclaration Die Methoden-Deklaration, die überschrieben werden soll
 * @return RefactoringStatus
 * @throws JavaModelException
 */
private RefactoringStatus rewriteMethodDeclaration(ASTRequestor requestor, ASTRewrite astRewrite, MethodDeclaration oldDeclaration) throws JavaModelException {
	RefactoringStatus status = new RefactoringStatus();
 
	ITypeBinding declaringBinding = null;
	IBinding[] bindings = requestor.createBindings(new String[] {fType.getKey()});	if (bindings[0] instanceof ITypeBinding) {
		declaringBinding = (ITypeBinding) bindings[0];
	}
	if (declaringBinding == null)
		return status;
 
	AST ast = oldDeclaration.getAST(); 
	MethodDeclaration newDeclaration = (MethodDeclaration) ASTNode.copySubtree(ast, oldDeclaration);	newDeclaration.setName(ast.newSimpleName(getMethodName())); 
	astRewrite.replace(oldDeclaration, newDeclaration, null);	return status;
}

Wie schon in rewriteMethodInvocation werden zuerst die Bindings der umzubenennenden Methode untersucht. Danach wird, basierend auf dem AST der original Deklaration, eine neue Methoden-Deklaration als Kopie erzeugt. Diese erhält jetzt den neuen Methodennamen. Zum Schluss wird die Original-Deklaration gegen die umbenannte Deklaration ausgetauscht. ▲___zum Seitenanfang

Methode rewriteAST

Überschreibt den AST einer CompilationUnit.

/**
 * Überschreibe den AST der gegebenen CompilationUnit
 * @param unit Die zu ändernde CompilationUnit
 * @param astRewrite Das ASTRewrite Hilfsobjekt, zum Überschreiben des ASTs
 */
private void rewriteAST(ICompilationUnit unit, ASTRewrite astRewrite) {
	try {
		MultiTextEdit edit = new MultiTextEdit();
		TextEdit astEdit = astRewrite.rewriteAST(); 
		if (!isEmptyEdit(astEdit))
			edit.addChild(astEdit);		if (isEmptyEdit(edit))
			return;
 
		TextFileChange change = fChanges.get(unit);
		if (change == null) {
			change = new TextFileChange(unit.getElementName(), (IFile) unit.getResource());			change.setTextType("java");			change.setEdit(edit);		} else
			change.getEdit().addChild(edit);
 
		fChanges.put(unit, change);	} catch (MalformedTreeException exception) {
		RefactoringPlugin.log(exception);
	} catch (IllegalArgumentException exception) {
		RefactoringPlugin.log(exception);
	} catch (CoreException exception) {
		RefactoringPlugin.log(exception);
	}
}

Änderungen werden mit astRewrite.rewriteAST() ausgeführt und dann in einem MultiTextEdit Objekt zusammengefasst. Die Änderungstabelle fChanges wird befüllt. Auf Basis dieser Änderungstabelle wird später von createChange das Change Objekt erzeugt. ▲___zum Seitenanfang

Methode createChange

Berechnung der tatsächlichen Änderungen und deren Gruppierung im Change Objekt. Änderungen werden erst nach einer Bestätigung des Users vom Framework im gesamten Workspace durchgeführt (Voraussetzung: Flag Change References wurde angehakt)

/**
 * Erzeugt das Change Objekt auf Basis der Änderungstabelle fChanges
 * Das erzeugte Change Objekt bringt für jede Änderung einen RefactoringChangeDescriptor mit.
 * RefactoringChangeDescriptor beschreibt die Änderungen in der RefactoringHistory.
 */
@Override
public Change createChange(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
   try {
	monitor.beginTask("Creating change...", 1);
 
	// erzeugt ein CompositeChange Objekt auf Basis der Änderungstabelle fChanges
	final Collection<TextFileChange> changes = fChanges.values();				CompositeChange change = new CompositeChange(getName(), changes.toArray(new Change[changes.size()])) {		@Override
		public ChangeDescriptor getDescriptor() {
			String project = fMethod.getJavaProject().getElementName();
			String description = MessageFormat.format("Beispiel Refactoring für ''{0}''", new Object[] {fMethod.getElementName()});
			String methodLabel = JavaElementLabels.getTextLabel(fMethod, JavaElementLabels.ALL_FULLY_QUALIFIED);
			String typeLabel = JavaElementLabels.getTextLabel(fType, JavaElementLabels.ALL_FULLY_QUALIFIED);
			String comment = MessageFormat.format("Beispiel Refactoring für ''{0}'' in ''{1}''", new Object[] {methodLabel,typeLabel});
			Map<String, String> arguments = new HashMap<String, String>();
			arguments.put(METHOD, fMethod.getHandleIdentifier());
			arguments.put(TYPE, fType.getHandleIdentifier());
			arguments.put(NAME, fName);
			arguments.put(REFERENCES, Boolean.valueOf(fUpdateReferences).toString());
			return new RefactoringChangeDescriptor(new ExampleRefactoringDescriptor(project, description, comment, arguments));
		}
	};
	return change;
   } finally {
	monitor.done();
   }
}

Das Change Objekt wird auf Basis der Berechnungen in der Methode checkFinalConditions erzeugt. Das Change Objekt enthält einen angepassten RefactoringChangeDescriptor (hier ExampleRefactoringDescriptor). Der Descriptor liefert unter anderem eine menschenlesbare Beschreibung jeder Änderung (mit Zeitpunkt und Beschreibung) in der Refactoring-History.

▲___zum Seitenanfang

Methode getName

Liefert einen Namen des Refactoring-Tools

@Override
public String getName() {
   return "ExampleRefactoring";
}

Dieser Name erscheint unter anderem im Edit Menü in den Funktionen Undo ExampleRefactoring und Redo ExampleRefactoring.


▲___zum Seitenanfang

Der Refactoring Wizard

Der Refactoring-Wizard wird in zwei Klassen implementiert: ExampleRefactoringWizard und ExampleRefactoringWizardInputPage.

Klasse RefactoringWizard erweitern

ExampleRefactoringWizard erweitert die abstrakte Framework Klasse org.eclipse.ltk.ui.refactoring.RefactoringWizard und implementiert nur den Constructor und die abstrakte Methode addUserInputPages.

01 public class ExampleRefactoringWizard extends RefactoringWizard {
02
03    public ExampleRefactoringWizard(ExampleRefactoring refactoring) {
04	super(refactoring, DIALOG_BASED_USER_INTERFACE | CHECK_INITIAL_CONDITIONS_ON_OPEN |
05                             PREVIEW_EXPAND_FIRST_NODE | NO_BACK_BUTTON_ON_STATUS_DIALOG );
06	setDefaultPageTitle("Beispiel-Refactoring"); 
07    }
08 
09    @Override
10    protected void addUserInputPages(){		
11       addPage(new ExampleRefactoringWizardInputPage(this));
12    }
13 }

Der Constructor übergibt das Refactoring-Objekt zwecks späterer Ansteuerung an die Oberklasse und setzt eine Reihe von Flags:

  • DIALOG_BASED_USER_INTERFACE bestimmt, dass ein "light weight" Wizard benutzt wird. Dieser hat nur eine Seite, eine Vorschau, einen "Finish"- und einen "Cancel"-Button
  • CHECK_INITIAL_CONDITIONS_ON_OPEN bestimmt, dass die Vorbedingungen des Refactoring beim Öffnen des Wizards validiert werden
  • PREVIEW_EXPAND_FIRST_NODE bestimmt, dass der erste Knoten im ChangeTree auf der Vorschauseite aufgeklappt sein soll
  • NO_BACK_BUTTON_ON_STATUS_DIALOG bestimmt, dass der Dialog, der den Status des Refactorings anzeigen wird, keinen "Back"-Buton erhalten soll.

In Zeile 06 wird noch der Name der WizardPage gesetzt.

Zeilen 09 bis 12: Die Methode addUserInputPages fügt nur unsere eigene WizardPage ExampleRefactoringWizardInputPage dem Wizard hinzu.

▲___zum Seitenanfang

Klasse UserInputWizardPage erweitern

Die Klasse ExampleRefactoringWizardInputPage beschreibt eine anwendungsabhängige Seite eines Refactoring-Wizards. Diese Seite enthält GUI-Elemente, die zur Abfrage der notwendigen Startparameter des Refactorings benötigt werden. In unserem Beispiel gib es vier Arten von GUI-Elementen:

  • eine Group (ein Rahmen mit Titel)
  • ein Label (Feldbeschriftung für das Eingabe-Textfeld)
  • Eingabe-Textfeld (für die Aufnahme des neuen Methodennamens)
  • eine Checkbox (für das Flag "Update references")

In Eclipse verwendet man für die Konstruktion der Benutzeroberfläche die Frameworks JFace und SWT. Die Fenster und Dialoge und die darin enthaltenen Elemente werden in Java programmiert und nicht, wie z.B. bei Microsoft Visual Studio per Drag&Drop erstellt.

Betrachten wir den Sourcecode von ExampleRefactoringWizardInputPage:

01 public class ExampleRefactoringWizardInputPage extends UserInputWizardPage {
02
03 private static final String PAGE_NAME = "ExampleRefactoringInputPage";
04
05 private final ExampleRefactoring refactoring;
06 private boolean initialized = false;
07
08 public ExampleRefactoringWizardInputPage(ExampleRefactoringWizard wizard) {
09    super(PAGE_NAME);
10    refactoring = (ExampleRefactoring) wizard.getRefactoring();
11 }

Der Constructor ruft die Superklasse auf und initialisiert die interne Referenz auf die Refactoring-Klasse.

13 @Override
14 public void createControl(Composite parent) {
15    initializeDialogUnits(parent);
16    Composite composite = new Composite(parent, SWT.NONE);
17    setControl(composite);
18    GridLayout gridLayout = new GridLayout(1, false);
19    gridLayout.verticalSpacing = 8;
20    composite.setLayout(gridLayout);
21 }

Die überschriebene Methode createControl initialisiert in Zeile 15 die vertikale und horizontale Berechnungseinheit des Dialogs, basierend auf der aktuellen Schriftart. Als Testeinheit zur Ermittlung der Schriftart dient das Containerelement parent. Die Initialisierung der Berechnungseinheit muss stets vor dem Aufruf der Konvertierungsmethoden:

  • convertHeightInCharsToPixels
  • convertHorizontalDLUsToPixels
  • convertVerticalDLUsToPixels
  • convertWidthInCharsToPixels

der Klasse DialogPage (und deren Subklassen wie unsere ExampleRefactoringWizardInputPage) erfolgen. In Zeilen 16 bis 20 wird ein Composite GUI-Element erzeugt, welches als Container alle anderen GUI-Elemente der WizardInputPage beinhalten soll. Als Layout wird ein einfaches GridLayout gewählt, in welchem die einzelnen Elemente untereinander mit einem Mindestabstand von 8 Pixeln angeordnet werden.

23 @Override
24 public void setVisible(boolean visible) {
25    if (visible && !initialized) {
26       initialized = true;
27       Composite composite = (Composite) getControl();
28       addOptionGroup(composite);
29       Dialog.applyDialogFont(composite);
30    }
31    super.setVisible(visible);
32 }

Die überschriebene Methode setVisible aktiviert bzw. deaktiviert die DialogInputPage und setzt deren Sichtbarkeit auf true oder false. Soll die InputPage zum erstem Mal aktiviert werden, dann wird deren Inhalt mittels der privaten Methode addOptionGroup aufgebaut. Es werden die Kinder-Elemente dem Hauptcontainer composite (ermittelt mit getControl und vorher mit createControl erzeugt) hinzugefügt. In Zeile 29 wird allen Kinderelementen die Schriftart des Dialogs zugewiesen (wenn das Kind-Element keine eigene Schriftart verwenden soll).

34 private void addOptionGroup(Composite composite) {
35	Group group = new Group(composite, SWT.NONE);36	group.setText("Beispiel: GUI-Element-Gruppe:");37	GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);38	gridData.verticalIndent = 8;39	group.setLayoutData(gridData);40	GridLayout gridLayout2 = new GridLayout(1, false);41	gridLayout2.verticalSpacing = 8;42	gridLayout2.marginTop = 3;43	group.setLayout(gridLayout2);44
45	Label label= new Label(group, SWT.NONE);46	label.setText("&Method name:");47
48	methodName = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);49	methodName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));50	methodName.addModifyListener(new ModifyListener() {51		public void modifyText(ModifyEvent event) {52			methodeNameChanged();53		}54	});55		
56	Button referenzenAnpassenCheckbox = new Button(group, SWT.CHECK);57	referenzenAnpassenCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, true, false));58	referenzenAnpassenCheckbox.setText("Referenzen anpassen?");59	referenzenAnpassenCheckbox.setSelection(true);60	referenzenAnpassenCheckbox.setEnabled(true);61	referenzenAnpassenCheckbox.addSelectionListener(new SelectionAdapter() {62		@Override63		public void widgetSelected(SelectionEvent e) {64			refactoring.setUpdateReferences(true);65		}66	});67	refactoring.setUpdateReferences(true);68			
69	methodName.setFocus();
70	methodName.selectAll();
71	methodeNameChanged();		
72 }

Die Methode addOptionGroup konstruiert die GUI-Elemente Textfeld und Checkbox.
In Zeile 35 bis 43 wird zuerst eine Group (eine Umrandung mit Titel) konstruiert, die das Textfeld und die Auswahlelemente aufnehmen soll.
In Zeile 37 wird ein GridData Objekt erzeugt, welches auf das GridLayout angewendet wird. Die ersten beiden Parameter des Constructors SWT.FILL bewirken, dass die Group den gesamten Bereich des Dialogs ausfüllt. Die letzten beiden Attribute bewirken, dass die Group-Box bei der Vergrößerung der Fensterbreite mitwächst, bei der Vergrößerung der Fensterhöhe dagegen nicht.

In den Zeilen 45 und 46 wird ein Label "Method name" erstellt. Dieses steht über dem Textfeld.

In den Zeilen 48 bis 54 wird ein Textfeld erstellt, welches den neuen Methodennamen aufnehmen soll. Der hinzugefügte Listener liest den Text aus und übergibt diesen als neuen Namen an das Refactoring-Tool

In Zeile 56 bis 67 wird eine Checkbox "Referenzen anpassen" der Group hinzugefügt.
In Zeile 59 wird die Vorbelegung auf true gesetzt. In der nächsten Zeile wird das Element auswählbar gemacht (bei false wäre es ausgegraut).
Mit addSelectionListener wird ein Listener der Checkbox hinzugefügt, der die Klicks auf die Checkbox überwacht und den Wert des Klicks auf ein Attribut der Refactoring-Klasse überträgt. In Zeile 67 wird dieses Attribut zunächst mit true initialisiert.

73 void methodeNameChanged() {
74	RefactoringStatus status = new RefactoringStatus();
75	
76	status.merge(refactoring.setMethodName(methodName.getText()));
77
78	setPageComplete(!status.hasError());
79	int severity = status.getSeverity();
80	String message = status.getMessageMatchingSeverity(severity);
81	if (severity >= RefactoringStatus.INFO) {
82		setMessage(message, severity);
83	} else {
84		setMessage("", NONE); //$NON-NLS-1$
85	}
86 }

Die Methode methodeNameChanged() liest den Text aus und übergibt diesen als neuen Namen an die Refactoring-Klasse.

Der fertige Wizard sieht dann so aus:

Abb. 6: Die erste Seite des Wizards - die WizardInputpage


Unser Beispiel Refactoring-Tool benennt eine Methode um. Die Vorschau sieht dann so aus:

Abb. 7: Die zweite (und letzte) Seite des Wizards, die Vorschau, zeigt das Ergebnis der Umbenennung der Methode


▲___zum Seitenanfang

Download Quellcode

Hier können Sie den Quellcode des Beispiels ExampleRefactoring als Eclipse-Projekt downloaden:

  ExampleRefactoring


  1. Eine weitere Regel der Eclipse Community lautet: "Wherever possible, let others contribute to your contributions."

◄== zurück zur Übersichtsseite
Zuletzt geändert am 19. Juli 2010 um 18:49