Ostatnio przegladajac informacje nt. przyszlosci i kierunkow rozwoju projektu eclipse natknalem sie na informację, że planowane są spore zmiany w systemie pluginów, z których dla mnie najważniejsze na chwilę obecną były:
- systemu okienek w oparciu o XWT (XUL/SWT)
- interfejsu pluginow dostepnego z poziomu języka JavaScript (JS)
Z oboma technologiami miałem już do czynienia, chociażby podczas budowy mojego pluginu do popularnej przeglądarki Firefox. Jednak od razu pojawiło się pytanie – w jaki sposób programiści eclipse’a będą udostępniać interfejsy w JS’ie? Odpowiedź na to pytanie jest już znana od lat – istnieje bowiem biblioteka, będą interpreterem i kompilatorem języka JavaScript w całości napisana w Javie, nazywa się rhino.
Dokumentacja projektu jest dość dobra i pozwoliła mi w ciągu paru godzin ogarnąć możliwości tego rozwiązania, no i narodził się pomysł, żeby w jednym z projektów udostępnić systemowi pluginów także api JavaScript’owe, co mogłoby znacząco wpłynąc na szybkość ich powstawania, a także wykorzystać możliwości np. operatorów czy warunków.
Obecny system pluginów opiera się o 2 interfejsy w Javie i abstrakcyjną klasę bazową, która udostępnia 2 metody, więc dobrze by było, żeby z poziomu JavaScriptu także był do nich dostęp. Nie jest to takie trudne, trzeba tylko, dodać klasie bazowej funkcjonalność obiektu Scriptable, co pozwoli go wykorzystać jako kontekst danego skryptu i umożliwić wywołanie wspomnianych metod z poziomu skryptu.
Jeżeli metody są statyczne to sprawa jest banalnie prosta, bo wtedy można je dodać jako funkcje i wogóle nie trzeba się wysilać:)
Kod poniżej odpowiada za przykładową implementację.
Interfejs:
public interface MyPlugin{
public void executeMyPlugin();
}
Klasa bazowa:
public abstract class MyBaseClass extends ScriptableObject{
public void doSomething(){
}
public void doSomething2(){
}
public static void executePlugins(List<String> pluginsList){
// stwórz kontekst
Context cx = Context.enter();
MyBaseClass executePlugins = new MyBaseClass();
ScriptableObject scope = (ScriptableObject) cx.initStandardObjects(executePlugins);
// dla każdego pliku
// wczytaj plik z dysku i zapisz go w String'u code
String code = "";
// dodaj własności dostępne jako globale z punktu widzenia skryptu
String[] scriptAvailableFunctions = { "doSomething", "doSomething2" };
scope.defineFunctionProperties(scriptAvailableFunctions, MyBaseClass.class, ScriptableObject.DONTENUM);
// dodaj potrzebne importy
String s = "var context = JavaImporter();\n" +
"context.importClass(Packages.MyPlugin);\n" +
"with (context) {\n " + code + "; p = new MyPlugin({executeMyPlugin:executeMyPlugin});p.executeMyPlugin();\n\n}" + "";
// wykonaj skrypt
cx.evaluateString(scope, scode, "MyScript", 0, null);
}
}
Plugin1.js
function executeMyPlugin(){
out.println("Hello from plugin1!")
}
Jak widać mój kontekst dołącza do pluginu potrzebne importy, czyli wszystko co jest dostępne w pakietach aplikacji, tak żeby żaden z pluginów nie musiał tego robić sam i wszystko wygląda dobrze, do czasu gdy któryś z nich potrzebuje wykonać jakąś metodę np. z pakietu java.io.
Wtedy można:
- zezwolić mu na samodzielne wykonywanie importu
- stworzyć dodatkowy opis np. w XML’u, który poza ciałem pluginu, mógły zawierać także dodatkowe informacje m.in. o zależnośćiach
Przykładowy XML, mogłby wyglądać tak:
<plugin name="Plugin1" id="Plugin1">
<dependencies>
<packages>
<list>
java.io;
java.lang;
</list>
</packages>
<classes>
<list>
java.io.File
</list>
</clases>
</dependencies>
<interfacemethod>
<[[CDATA
function executeMyPlugin(){
out.println("Hello from plugin1!");
}
]]>
</interfacemethod>
</plugin>
I już jest znacznie ładniej, bo poza samym kodem mamy jeszcze trochę metadanych potrzebnych do wykonania pluginu.
Wyobraźmy sobie teraz sytuację, że chcemy w ten sam sposób dodać pluginy korzystające z różnych interfejsów, wtedy jedyne co wystarczy dodać atrybut interface, próbujemy:
<plugin name="Plugin1" id="Plugin1" interface="MyPlugin">
<!-- reszta tak jak byla -->
</plugin>
Co to daje – deklaratywną kontrolę tego co faktycznie zostaje wykonane, do tego możnaby się pokusić o sprawdzenie jakie metody są w interfejsie i sprawdzenie czy aby napewno kod JavaScript’owy zawiera ich deklarację, co w połączeniu z założeniem, że w przyszłości pojawi się do tego jakiś eleganckie gui (np. korzystająć z edytora eclipse’owego w połączeniu z czarodziejem), może dać naprawdę świetne rezultaty.
Pluginy tak naprawdę są elementem pochodnym, mogą być wykorzystane, do wykonywania akcji przewidzianych dla jakichś danych z zwenątrz. Zazwyczaj to jaki plugin ma być wykonany też trzeba w jakiś sposób zadeklarować. Posłużymy się tutaj znowu XML’em:
<map>
<element from="element1" to="element2" plugin="Plugin1" />
</map>
Tutaj mamy prostą postać pluginu, masz element i na nim wykonaj dana operację. Jednak co by było gdybyśmy chcieli stworzyć nowy element na podstawie dwóch innych elementów, trzebaby podać pluginowi skąd ma pobrać wartości, np w ten sposób:
<map>
<element to="element2">
<from plugin="Plugin1" separator=";">
<el>key1</el>
<el>key2</el>
</from>
</element>
</map>
Teraz widać, że plugin będzie miał dostęp do dodatkowych elementów opisywanych przez klucze key1/key2, a wynikiem jego działania ma być połączony string. Idąc dalej tym tropem może się okazać, że przyjdzie potrzeba wykonania na danych operacji matematycznych, np. dodania wartości 2 pól.
<map>
<element to="element2">
<from plugin="Plugin1" operation="a+b">
<el name="a">key1</el>
<el name="b">key2</el>
</from>
</element>
</map>
Jedyna różnica w stosunku do poprzedniego przykładu polega na tym, że wartości pól muszą być zsumowane, aby to zrealizować przy użyciu JS’a, możemy zrobić tak:
var obj = {};
for(el in elements) {
obj[el.getName()] = inputMap.get(el.value())
}
with(obj){
eval('res='+el.getParentNode().getOperation())
}
out.println("Result of operation is: "+res)
I prosze, dynamiczna typizacja załatwi resztę! Jedyne o co się trzeba zatroszczyć to przechwycenie ew. błędów.