Ein einfaches Beispiel

Eclipse JDT um eigene Quickfixes erweitern

Sven Jeppsson

Quickfixes sind die kleinen Helferchen, die in Form einer Glühbirne auf der linken Seite des Quelltextes auftauchen, wenn der Quelltext einen automatisch behebbaren Fehler enthält. Nicht immer ist unter den Lösungsvorschlägen, die ein Quickfix bietet, das Richtige dabei. Dieser Artikel beschreibt, wie Eclipse um eigene Quickfixes erweitert werden kann.

Wo mir ein QuickFix fehlt

Manchmal fange ich an, eine neue Klasse in derselben Datei zuschreiben, in der ich bereits schon eine Klasse bearbeite. Irgendwann, wenn mir das zu unübersichtlich wird, möchte ich diese in eine andere Datei verschieben.

Beispiel für die beschriebene Struktur
------------------------------------------------------
package testpackage;

//neue Klasse, der Klassenname entspricht nicht dem Dateinamen
class TestKlasse2{
//. (irgendein Inhalt)
}

//bestehende Klasse, der Klassenname entspricht dem Dateinamen
public class TestKlasse {
//. (irgendein Inhalt)
}

In der kommerziellen Java-Entwicklungsumgebung, die ich bisweilen genutzt habe, reicht es aus, das Keyword public vor TestKlasse2 zu setzen und dann den Quickfix zu nutzen, um die Klasse entsprechend zu verschieben.

Abb. 1: Ein einfaches Code-Beispiel für das zu behebende Problem

Bei Eclipse klappt das leider nicht. Abbildung 1 zeigt, dass Eclipse den Fehler erkennt und auch einen Quickfix anbietet (Glühbirne am linken Rand). Leider löst keine der angeboten Aktionen das Problem (Abb. 2). Ich möchte weder die Datei in TestKlasse2.java noch die TestKlasse2 in TestKlasse umbenennen. Ich möchte, dass die TestKlasse2 in eine neue Datei TestKlasse2.java verschoben wird.

Abb. 2: Liste der Lösungsvorschläge, die Eclipse von sich heraus liefert

Anhand dieses einfachen Problems möchte ich erklären, wie man einen Quickfix für JDT von Eclipse bereitstellt. Unser Ziel ist es, dass zukünftig die Liste Lösungsvorschläge wie in Abbildung 3 um den Punkt „Move Class to ‚…'“ erweitert wird.

Abb. 3: Liste der Lösungsvorschläge, wie sie sein sollte
Der Extension Point

Über Extension Points werden in Eclipse Plug-ins verschachtelt. Jedes Plug-in kann eigene Extension Points anbieten und die anderer verwenden. Für unsere Erweiterung wollen wir lediglich den Extension Point „org.eclipse.jdt.ui.quickFixProcessors“ aus dem Plug-in „org.eclipse.jdt.ui“ nutzen, um unseren eigene QuickFixProcessor in JDT einzuhängen. Damit Sie diesem Beispiel folgen können, sollten Sie wissen, wie man in Eclipse Plug-in-Projekte anlegt und einrichtet. Im Folgenden daher eine Anleitung dazu. Erfahrene Plug-in-Entwickler brauchen daraus lediglich die Dependencies.

Entwicklung des QuickFix als Plug-in

Legen Sie sich einen neues Plug-in-Projekt an. In meinem Beispiel ist der Projektname: „de.xthinker.eclipse.example.quickfixes“. Es wird kein Activator benötigt und es sollen auch keine Templates verwendet werden. (Ist das Ihr erstes Plug-in? Dann schauen sie in den Kasten „Anlage des Plug-in Projektes“. Darüber hinaus empfehle ich einen Artikel auf der Eclipse-Website: www.eclipse.org/articles/Article-PDE-does-plugins/PDE-intro.html.

Anlage des Plug-in Projektes

  • Klicken Sie im Menü auf FILE | NEW | PROJECT…
  • Wählen Sie im Dialog PLUG-IN PROJECT aus und klicken dann auf NEXT >.
  • Geben Sie den Projektnamen ein: de.xthinker.eclipse.example.quickfixes. Dann wieder NEXT >
  • Deaktivieren Sie die Checkboxen in „Plug-in Options“. NEXT >.
  • Deaktivieren Sie die Checkbox „Create a plguin using one of the templates“.
  • Klicken Sie auf FINISH.

Nachdem Sie das Projekt neu angelegt haben, sollte automatisch der Plug-in Manifest Editor geöffnet sein. Ist das nicht der Fall, öffnen Sie z.B. die Datei META-INF/ MANIFEST.MF, diese wird standardmäßig mit diesem Editor geöffnet. Wählen Sie zunächst den Reiter „Dependencies“. Dort fügen sie mit „Add…“ bei „Required Plug-ins“ die folgenden Anhängigkeit hinzu:

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

Abb. 4: Dependencies Required Plug-ins

Danach sollte die MANIFEST.MF in etwa so wie folgt aussehen:

META-INF/ MANIFEST.MF
------------------------------------------------------
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Quickfixes Plug-in
Bundle-SymbolicName: de.xthinker.eclipse.example.quickfixes;singleton:=true
Bundle-Version: 1.0.0
Bundle-Localization: plugin
Require-Bundle: org.eclipse.jdt.ui,
 org.eclipse.jface.text,
 org.eclipse.jdt.core,
 org.eclipse.core.runtime,
 org.eclipse.jface

Wechseln Sie nun in den Reiter „Extension“. Fügen sie mit ADD… die Extension org.eclipse.jdt.ui.quickFixProcessors hinzu. Mit rechtem Mausklick auf den neuen Eintrag wählen Sie im Kotextmenü NEW | quickFixProcessor aus. Bearbeiten Sie jetzt die Details für den neuen „quickFixProcessor“. Geben Sie sowohl für die „id“ als auch für die „class“ den Namen de.xthinker.eclipse.example.quickfixes.MoveClassQuickFixProcessor ein. Danach kann mit einem Klick auf das Label CLASS die Klasse automatisch angelegt werden.

Abb. 5: Extensions

Die plugin.xml sollte nun wie folgt aussehen:

plugin.xml
-----------------------------------------------------

Damit ist das Grundgerüst für das QuickFix-Plug-in fertig gestellt. Es geht weiter mit der Implementierung der benötigten Klassen.

Der MoveClassQuickFixProcessor

Ein QuickFixProcessor implementiert das Interface IQuickFixProcessor. Dieses Interface definiert zwei Methoden:

  • public boolean hasCorrections(ICompilationUnit unit, int problemId)
  • public IJavaCompletionProposal[] getCorrections(final IInvocationContext context, IProblemLocation[] locations) throws CoreException

Die eingehängten QuickFixProcessoren werden zu jedem Problem befragt, ob sie dafür eine Lösung anbieten können. Die Art des Problems wird über die problemId beschrieben. Alle von dem JDT erkennbaren Problemarten sind im Interface IProblem definiert. Aber durch welche problemId wird unser Problem identifiziert? IProblem.PublicClassMustMatchFileName! Wie ich diese schlussendlich gefunden habe, ist im Kasten „problemId identifizieren“ beschrieben.

problemId identifizieren
Die Menge der identifizierbaren Probleme ist ganz schön groß. Ich musste eine Weile suchen, bis ich die richtige problemId zu meinen QuickFix gefunden habe. Um diese problemId zu finden, habe ich eine „leere“ Implementierung des QuickFixProzessors verwendet, um die übergebenen problemIds mittels System.out auszugeben. Damit ich auch die richtige problemId ausgegeben bekomme, habe ich Eclipse damit gestartet und mein oben gezeigtes Fehler-Beispiel in einer neuen Datei nachgestellt.

hasCorrections soll dann true liefern, wenn unsere problemId übergeben wird. Siehe das folgende Listing:

Methode hasCorrections(...)
-----------------------------------------------------
public boolean hasCorrections(ICompilationUnit unit, int problemId){
switch (problemId) {
		case IProblem.PublicClassMustMatchFileName:
			return true;
		default:
			return false;
	}
}

Die Korrekturvorschläge werden durch das JDT nur zu Problemen abgeholt, bei denen hasCorrections(…) zuvor true geliefert hat.
Beim Aufruf von getCorrections(…) wird die Menge aller Probleme einer CompilationUnit (hier eine Java-Datei), die dieser QuickFixProcessor behandeln kann, in Form von IProblemLocation übergeben. Über die Menge der Locations muss iteriert werden und entsprechend ihrer problemId die Menge möglicher Korrekturvorschläge (ICompletationProposal) zurückgegeben werden. Die Methode liefert also zu den behandelbaren Problemen einer CompilationUnit alle Korrekturvorschläge auf einmal zurück. Die Implementierung von getCorrections(…) für unser Beispiel, sehen sie im Listing „Methode getCorrections(…)“:

Methode getCorrections(...)
-----------------------------------------------------
public IJavaCompletionProposal[] getCorrections(final IInvocationContext context, IProblemLocation[] locations) throws CoreException{
	ArrayList res = null;  
	for (int i = 0; i 
Das MoveClassProposal

Wie wir gesehen haben, ist unser QuickFixProcessor nicht selbst der Korrekturvorschlag, sondern dient nur zu ihrer Ermittlung und Erzeugung. Der eigentliche Korrekturvorschlag zu unserem Problem ist eine Instanz der Klasse MoveClassProposal, welche die Methode getCorrections(..) zurückgibt. Diese Klasse implementiert das Interface IJavaCompletionProposal.
Ein IJavaCompletionProposal stellt sich in einen QuickFix nicht nur als ein Eintrag in der Liste dar (Foto), sondern führt nach der Selektion durch den Anwender auch das Refactoring durch. Hierzu werden für unser Beispiel folgende Methoden implemtiert:

  • getRelevance()
  • getDisplayString()
  • getAdditionalProposalInfo()
  • getImage
  • apply(…)

Erzeugt wird unser Proposal mit der IProblemLocation und dem IInvocationContext. Aus diesen beiden Information ermitteln wir im Konstruktor die Klasse, die verschoben werden soll (toBeMovedType), den Klassenname (elementName), das Package (packageString) und den Namen der neu zu erzeugenden Datei (newCUName).

Konstruktor
----------------------------------------------------
MoveClassProposal(IProblemLocation location, IInvocationContext context) {
	ICompilationUnit compilationUnit = context.getCompilationUnit();
	toBeMovedType= compilationUnit.getType(location.getProblemArguments()[1]);
	 elementName = toBeMovedType.getElementName();
	 packageString = getPackageString(toBeMovedType);
	 newCUName = elementName+".java";
}

private String getPackageString(IType toBeMovedType) {
	String packageName = toBeMovedType.getPackageFragment().getElementName().trim();
	String packageString="";
	if (packageName.length()>0){
		packageString = "package "+packageName+";";
	}
	return packageString;
}

Damit wir unseren Korrekturvorschlag in der Vorschlagsliste des Quickfixes sehen, müssen wir mindesten die Methode getDisplayString() implementieren. Der zurückgegebene String wird in der Liste angezeigt. Darüber hinaus gibt die Möglichkeit, mit getAdditionalProposalInfo() eine erweiterte Erklärung der Aktion hinzuzufügen. Diese erscheint in dem kleinen Fenster neben der Liste. In der Abbildung sehen wir in der Liste ebenfalls ein kleines Icon das durch getImage() bereitgestellt wird.

Methoden für die Repräsentation im GUI
---------------------------------------------------
public String getDisplayString() {
	return "Move Class to '"+newCUName+"'";
}

public String getAdditionalProposalInfo() {
	return "Creates a new compilation unit with the name '"+
newCUName+
"',if not already exists. And moves the the type '"+
elementName+"' into this compilation unit";
}


public Image getImage() {
	return JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_MOVE); 
}

public int getRelevance() {
	return 10;
}

Durch die getRelevance() wird Einfluss auf die Position des Vorschlages in der Liste genommen. Je höher der Rückgabewert ist, desto weiter rückt er nach oben.

Die Methode apply(…) führt das eigentliche Refactoring durch. In diesem Fall wird eine neue CompilationUnit (target) mit dem Namen der Zieldatei für die neue public class erzeugt. Danach wird toBeMovedType in die neue Datei verschoben.

Methode apply(...)
---------------------------------------------------
public void apply(IDocument document) {
	try {
		ICompilationUnit target = toBeMovedType.getPackageFragment().createCompilationUnit(newCUName, packageString, true, null);
		toBeMovedType.move(target, null, elementName, true, null);
	} catch (JavaModelException e) {
		e.printStackTrace();
	}
}

Die hier nicht aufgeführten Methoden des Interfaces IJavaCompletionProposal sind für unser hier gezeigtes Beispiel nicht relevant und dürfen null zurückgeben.

Wir sind fertig! Wie, Sie glauben das nicht? Starten Sie doch einfach eine Eclipse-Umgebung mit dem Plug-in über das Menü (RUN | RUN… | ECLIPSE APPLICATION) und spielen Sie das oben aufgeführte Szenario nach. Ich hoffe, Sie sind jetzt in der Lage, für die Probleme, die Sie bisher immer genervt haben, Ihre eigenen QuickFixes zu schreiben und Sie der Eclipse-Comunity zu Verfügung zu stellen.

Ich habe selbst vergeblich nach einer Dokumentation im Internet gesucht. Die Analyse der Klasse org.eclipse.jdt.internal.ui.text.correction.QuickFixProcessor hat mir geholfen, dieses Beispiel zu realisieren.

Den Quellcode zum Artikel finden Sie hier.

Klasse MoveClassQuickFixProcessor
--------------------------------------------------
public class MoveClassQuickFixProcessor implements IQuickFixProcessor {
	

	public MoveClassQuickFixProcessor() {
		super();
	}

	public boolean hasCorrections(ICompilationUnit unit, int problemId){
		switch (problemId) {
		case IProblem.PublicClassMustMatchFileName:
			return true;
		default:
			return false;
		}
	}

	public IJavaCompletionProposal[] getCorrections(final IInvocationContext context, IProblemLocation[] locations) throws CoreException{
		ArrayList res = null;  
		for (int i = 0; i 
Klasse MoveClassProposal
-------------------------------------------------
public class MoveClassProposal implements IJavaCompletionProposal {

	private String newCUName;
	private String packageString;
	private String elementName;

	private IType toBeMovedType;

	MoveClassProposal(IProblemLocation location, IInvocationContext context) {
		ICompilationUnit compilationUnit = context.getCompilationUnit();
		toBeMovedType= compilationUnit.getType(location.getProblemArguments()[1]);
		 elementName = toBeMovedType.getElementName();
		 packageString = getPackageString(toBeMovedType);
		 newCUName = elementName+".java";
	}

	public int getRelevance() {
		return 10;
	}

	public void apply(IDocument document) {
		try {
			ICompilationUnit target = toBeMovedType.getPackageFragment().createCompilationUnit(newCUName, packageString, true, null);
			toBeMovedType.move(target, null, elementName, true, null);
		} catch (JavaModelException e) {
			e.printStackTrace();
		}
	}

	private String getPackageString(IType toBeMovedType) {
		String packageName = toBeMovedType.getPackageFragment().getElementName().trim();
		String packageString="";
		if (packageName.length()>0){
			packageString = "package "+packageName+";";
		}
		return packageString;
	}
	
	/**
	 * Ausfuehrliche Beschreibung der Aktion, taucht in einem Fenster rechts neben der QuickfixProposal-Auswahl auf!
	 */
	public String getAdditionalProposalInfo() {
		return "Creates a new compilation unit with the name '"+newCUName+"',if not already exists. And moves the the type '"+elementName+"' into this compilation unit";
	}

	public IContextInformation getContextInformation() {
		return null;
	}

	/**
	 * Kurz Beschreibung der Aktion,taucht in der QuickfixProposal-Auswahl auf!
	 */
	public String getDisplayString() {
		return "Move Class to '"+newCUName+"'";
	}
	
	/**
	 * Icon, taucht in der QuickfixProposal-Auswahl auf!
	 */
	public Image getImage() {
		return image; 
	}

	public Point getSelection(IDocument document) {
		return null;
	}
}
Sven Jeppsson ist Diplom-Ingenieur der Technischen Informatik (FH) und arbeitet seit 2001 mit objektorientierten Programmiersprachen (Java). Zurzeit arbeitet er bei einem Hamburger Softwarehaus. Als Facharchitekt und Entwickler beschäftigt Sven Jeppsson sich dort mit dem Design und der Implementierung einer Softwarekomponente für den Zahlungsverkehr im Versicherungsumfeld. Es ist zu dem mit der Ausbildung von Fachinformatikern betraut und ist stellvertretender Leiter in seinem Entwicklerteam. Mit Eclipse hat er die IDE seiner Wahl gefunden und entwickelt nebenher seit 2005 damit RCP-Anwendungen und nützliche Plug-ins für Eclipse.
Geschrieben von
Sven Jeppsson
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.