Auch das Interface Command hat eine Überarbeitung erfahren. Es definiert
nun die folgenden vier Methoden:
void execute(Collection<Object> args);Bewirkt die tatsächliche Ausführung des Kommands. Früherer Name:doExecute(...).boolean canExecute(Collection<Object> args);Wird genutzt um dasCommandzu befragen, ob es zur Ausführung bereit ist.Bisher waren für diese Funktionalität die zwei Methoden
checkQuickly(...)undcheckDeeply(...)zuständig, die eine schnelle oder detaillierte Prüfung zuließen. Künftig gibt es nur noch eine Methode zu diesem Zweck. Die Idee hintercheckDeeply(...)war es, eine Möglichkeit zur Durchführung ressourcenintensiver Prüfungen anzubieten. Die Methode wurde direkt vor Ausführung desCommands einmalig aufgerufen. Im praktischen Einsatz stellte sich jedoch heraus, dass diese Fälle relativ selten sind. In Version 4 wurde bereits durch die Klassecom.levigo.jadice.AbstractViewerCommanddie MethodecanExecute(...)eingeführt und hat sich bewährt. Daher wurde die API an dieser Stelle auf eine PrüfmethodecanExecute()reduziert und im InterfaceCommandaufgenommen.Sollte eine ressourcenintensive Prüfung für einzelne
Commands notwendig sein, kann diese in dasCommandselbst verlagert werden, von wo aus auch eine gezielte Rückmeldung an den Nutzer erfolgen kann.boolean isSelected(Collection<Object> args);Ersetzt die bisherige MethodeisChecked(...).Es handelt sich dabei lediglich um eine Namensänderung, die besser zum Ausdruck bringen soll, dass es sich um eine Auswahl handelt, jedoch nicht zwingend um einen On-/Off-Schalter im Stil einer Checkbox. Die neue Namensgebung entspricht auch den in Swing allgemein verwendeten Konventionen.
boolean isAvailable();gibt wie bereits in vorhergehenden Versionen an, ob dasCommandzur Verfügung steht und integriert werden kann.
Neben den gelisteten Änderungen wurden, wie die Methodensignaturen bereits
erkennen lassen, die Methodenargumente mit Generics versehen. Die bisher existierende
Klasse AbstractCommand existiert künftig nicht mehr. Einfache Commands müssen daher künftig das Interface selbst
implementieren. Im nahezu allen Fällen empfiehlt es sich jedoch stattdessen, von der
weiter unten beschriebenen Klasse InjectedCommand zu erben, um die Vorteile der
Dependency-Injection zu nutzen.
Die Ausführung eines Commands ist von verschiedenen Objekten abhängig, die
Status und Umfeld des Commands definieren. Dazu zählen einerseits die oben
bereits beschriebenen Inhalte des Contexts, andererseits aber auch Parameter, die in der
commands.properties-Datei für ein Command definiert wurden. Beide werden von nun an
vollautomatisch mittels dependency injection zur Verfügung gestellt.
Die Parameter-Injection bezieht sich auf alle Objekte, die aus den
commands.properties befüllt werden. Diese wurden in früheren
Versionen durchnummeriert und mithilfe der Methode
com.levigo.util.swing.action.AbstractCommand.getCommandParameter(int,
String) zur Verfügung gestellt.
Der neue Parameter Injection Mechanismus geht einen wesentlich komfortableren Weg: Die
Nummerierung in commands.properties wurde durch Benennung der
einzelnen Parameter ersetzt und für die zur Verfügung gestellten Objekte erfolgt nun eine
automatische Typkonvertierung und Befüllung der Klassenvariablen.
Das folgende Beispiel zeigt die Implementation eines Commands, das einen Parameter aus den
commands.properties nutzt. Es muss dazu lediglich eine Annotation
angebracht werden, die festlegt, dass das Feld aus einem Properties-Parameter befüllt
werden soll. Die Zuweisung der korrekten Instanz an someStringParameter
erfolgt dann automatisch durch die Command Factory. Um zu verdeutlichen, dass die
Parameter-Injection auch mit solchen Commands funktioniert, die nicht von InjectedCommand erben, wird im Beispiel das Interface
direkt
implementiert.
public class DemoCommand implements Command {
@Parameter
private String someStringParameter;
@Override
public void execute(Collection<Object> args) {
// create dialog
final JDialog dialog = new JDialog();
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// use injected parameter and argument
dialog.add(new JLabel("parameter: " + someStringParameter));
// show dialog on screen (in this case, we're already on EDT)
dialog.pack();
dialog.setVisible(true);
}
@Override
public boolean canExecute(Collection<Object> args) {
// can always execute!
return true;
}
@Override
public boolean isSelected(Collection<Object> args) {
// no meaningful selection state -- therefore never selected
return false;
}
@Override
public boolean isAvailable() {
// always available
return true;
}
}Damit
das Command zur Verfügung steht und mit dem korrekten
Parameter befüllt wird, müssen in der commands.properties folgende
Inhalte hinzugefügt werden:
# specify which class to use demoCommand=com.levigo.jadice.demo.commands.DemoCommand # parameter to be injected demoCommand.param.someStringParameter=this is the injected parameter string.
Für Parameter, die auf diese Weise angefordert werden, stellt das Framework sicher, dass diese nach Instanziierung der Klasse bereitstehen.
Die Annotation @Parameter kann, wie das Beispiel bereits zeigt,
an einer Klassenvariablen angebracht werden, unabhängig von deren Sichtbarkeits-Modifier.
Zu beachten ist jedoch an dieser Stelle, dass in manchen Security-Kontexten (zum Beispiel
innerhalb von Applets) nur public möglich ist, da der Aufruf von
Field.setAccessible(true) in einer
SecurityException resultiert.
Wie es auch in anderen Injection-Frameworks konvention ist, besteht auch die
Möglichkeit, die Annotation @Parameter an einer setter-Methode
anzubringen. Der erwartete Parametername entspricht dann dem Methodennamen ohne das
set-Prefix und mit dem ersten Buchstaben klein geschrieben. Bezüglich
der Methodensichtbarkeit gelten die gleichen Regeln wie für Klassenvariablen.
Für Fälle, in denen der Parametername in commands.properties
nicht dem Variablennamen entsprechen oder aus dem Methodennamen generiert werden soll,
besteht die Möglichkeit, die Annotation @Parameter mit einem
optionalen Argument name zu versehen, das den in der Properties-Datei
verwendeten Namen definiert. Des Weiteren existiert ein Annotations-Argument
optional, mit dem definiert werden kann, dass das Fehlen der
Parameter-Definition in commands.properties nicht zu einem Fehler
führen soll. Das Command wird dann trotzdem instanziiert und annotierte Felder behalten
ihren in der Klasse definierten Default-Wert.
Die Parameter-Injection funktioniert für folgende Datentypen:
String, Color, enums und die
Basis-Datentypen wie int, float oder
boolean.
Die zweite Möglichkeit, Objekte in Kommandos zu injizieren, ist die
Argument-Injection. Während Parameter aus den Properties stammen und in jegliches Command aufgenommen werden können, beziehen sich
Argumente auf im Context vorhandene Objekte, werden aus diesem heraus
injiziert, und können ausschließlich in solchen Kommandos verwendet werden, die InjectedCommand erweitern.
Das folgende Beispiel erweitert die bereits vorgestellte Klasse
DemoCommand um eine Argument-Injection. Die bestehende
Parameter-Injection ist nach wie vor enthalten, um zu zeigen, dass beides gleichzeitig
möglich ist. Da die Klasse jetzt von InjectedCommand erbt, ändert sich auch deren
Implementation. Lediglich die execute()-Methode muss noch zur
Verfügung gestellt werden – für die restlichen Methoden des Interface existieren
Default-Implementationen. Doch auch für execute() ergibt sich eine
Änderung: Der Methode wird keine Collection von Argumenten mehr
übergeben, da diese über die Funktionalität der Superklasse verarbeitet und injiziert
werden. Insgesamt muss das benötigte Feld also lediglich mit
@Argument annotiert werden und es wird daraufhin durch die
Funktionalität aus InjectedCommand automatisch mit den entsprechenden
Kontext-Argumenten
befüllt.
public class DemoCommand extends InjectedCommand {
@Argument
private String someStringArgument;
@Parameter
private String someStringParameter;
@Override
public void execute() {
// create dialog
final JDialog dialog = new JDialog();
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// use injected parameter and argument
dialog.add(new JLabel("parameter: " + someStringParameter + " -- argument: " + someStringArgument));
// show dialog on screen (in this case, we're already on EDT)
dialog.pack();
dialog.setVisible(true);
}
}Im
Beispiel wird ein String als Argumenttyp verwendet, es könnte aber
auch jedes andere Objekt abgefragt werden, sofern es im Context vorliegt.
In realen Szenarien kann es leicht vorkommen, dass der Kontext mehrere Objekte desselben Typs enthält. In solchen Fällen wird jeweils das erste gefundene Objekt dieses Typs injiziert. Alternativ dazu ist es möglich, sämtliche Objekte eines Typs abzufragen. In diesem Fall werden alle gefundenen Objekte in einer Collection des entsprechenden Typs zurückgegeben. So werden beispielsweise mit dem Code-Fragment
@Argument
private Collection<String> someStringArgument;sämtliche String-Instanzen aus dem Kontext angefordert und in das Feld injiziert.
In Zusammenhang mit Collections existiert eine Einschränkung
der InjectedCommands: Es ist nicht möglich, Objekte des
Typs Collection im Kontext abzulegen und mittels Argument-Injection
auszulesen. Grund hierfür ist die Generic Type Erasure: Zur Laufzeit sind die durch
Generics spezifizierten Typinformationen nicht vorhanden, sodass eine korrekte
Typkonvertierung nicht zuverlässig möglich ist. Soll eine Ansammlung von Objekten im
Kontext abgelegt und injiziert werden, so empfehlen wir dafür ein dediziertes Fachobjekt
zu erstellen, das die Objektmenge vorhält.
Wie auch schon bei den Parametern der Fall, ist es auch für Annotationen des Typs
@Argument möglich, statt einer Klassenvariable die entsprechende
setter-Methode zu annotieren. Auch bezüglich der Java Sichtbarkeits-Schlüsselworte gilt
oben gesagtes.
Die Annotation @Argument bietet verschiedene Optionen an, die
den Injektions-Vorgang beeinflussen können. Wie es schon für Parameter der Fall war,
können auch Argumente als optional markiert werden. Des Weiteren existiert eine Option
boolean allowOtherMatches, die festlegt, ob bei der Abfrage nach
einem einzelnen Objekt das Vorhandensein mehrerer gleicher Objekttypen zu einem Fehler
führen soll. Die Option boolean matchSubclasses legt fest, ob bei der
Suche nach Objekten im Kontext auch Subtypen des angeforderten Typs zurückgeliefert werden
sollen. Eine letzte Option, Class<? extends Object> match,
definiert nach welchem Objekttyp der Kontext durchsucht werden soll. Wie aus dem Beispiel
bereits hervorging, wird der gewünschte Typ im Regelfall automatisch erkannt. Dieses
zusätzliche Attribut erlaubt es jedoch, eine nähere Einschränkung – zum Beispiel auf eine
bestimmte Subklasse – zu treffen.
Eine letzte Möglichkeit der Argument-Injizierung bietet die Annotation
@AllArguments. Diese stellt sämtliche Argumente des Kontexts zur
Verfügung. Im Regelfall geschieht dies über einen Rückgabewert des Typs
Collection<Object>, wobei zu bedenken ist, dass die Klasse
Context das Interface Collection
implementiert. Man bekommt daher eigentlich eine Instanz von Context zurück. Um an dieser Stelle keine manuellen
Typ-Checks und -Konvertierungen durchführen zu müssen, kann auf der Annotation
@AllArguments die Option match = Context.class
spezifiziert werden. Ist diese Option vorhanden, so wird direkt eine Instanz von Context zurückgegeben.


