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 dasCommand
zu 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 desCommand
s 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.AbstractViewerCommand
die MethodecanExecute(...)
eingeführt und hat sich bewährt. Daher wurde die API an dieser Stelle auf eine PrüfmethodecanExecute()
reduziert und im InterfaceCommand
aufgenommen.Sollte eine ressourcenintensive Prüfung für einzelne
Command
s notwendig sein, kann diese in dasCommand
selbst 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 dasCommand
zur 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 Command
s 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 Command
s ist von verschiedenen Objekten abhängig, die
Status und Umfeld des Command
s definieren. Dazu zählen einerseits die oben
bereits beschriebenen Inhalte des Context
s, 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 Command
s, 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 Command
s 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 Collection
s existiert eine Einschränkung
der InjectedCommand
s: 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.