22 września 2008

Pakunki częściowe OSGi w akcji z Equinox

Już wiem, że pakunki częściowe (ang. fragment bundles) nie są wspierane przez Apache Felix (więcej o nich i braku wsparcia przez Feliksa w OSGi - 3.14 Pakunki częściowe), więc nie pozostaje mi nic innego jak korzystać z Eclipse Equinox do dalszych testów. Tak długo, jak wymagane będzie wsparcie pakunków częściowych Platformą OSGi nie będzie Apache Felix. To jest właśnie ów techniczny powód, dla którego, w kontekście Spring-DM, często pojawiał się będzie Equinox. Zainteresowanych zmianami w tej materii uprasza się o śledzenie rozwoju zgłoszenia FELIX-29 Implement bundle fragments.

Rozpocznę praktyczne rozpoznanie pakunków częściowych od stworzenia dwóch pakunków - pakunku macierzystego i pakunku częściowego - z pomocą spring-osgi-bundle-archetype.

UWAGA: Użyłem zadania archetype:generate z parametrem -Darchetype.interactive=false, gdyż wcześniejużywany archetype:create został oznaczony jako nieaktualny ([WARNING] This goal is deprecated. Please use mvn archetype:generate instead)

Najpierw pakunek macierzysty springdm-host-bundle:
 mvn archetype:generate \
-DarchetypeGroupId=org.springframework.osgi \
-DarchetypeArtifactId=spring-osgi-bundle-archetype \
-DarchetypeVersion=1.2.0-m2-SNAPSHOT \
-DgroupId=pl.jaceklaskowski.springdm.fragment \
-DartifactId=springdm-host-bundle \
-Dversion=1.0 \
-Darchetype.interactive=false
, po którym tworzę pakunek częściowy springdm-fragment-bundle:
 mvn archetype:generate \
-DarchetypeGroupId=org.springframework.osgi \
-DarchetypeArtifactId=spring-osgi-bundle-archetype \
-DarchetypeVersion=1.2.0-m2-SNAPSHOT \
-DgroupId=pl.jaceklaskowski.springdm.fragment \
-DartifactId=springdm-fragment-bundle \
-Dversion=1.0 \
-Darchetype.interactive=false
Możnaby utworzyć je w ramach większego projektu mavenowego (packaging=pom), ale pozostawiam to dla zaangażowanych.

Na początku zadeklaruję pakunek springdm-fragment-bundle jako pakunek częściowy dla springdm-host-bundle za pomocą nagłówka Fragment-Host. Jako, że projekt pakunku częściowego springdm-fragment-bundle zarządzany jest przez Apache Maven 2 nagłówek dodaję do konfiguracji wtyczki maven-bundle-plugin w pom.xml.
 <Fragment-Host>pl.jaceklaskowski.springdm.fragment.springdm-host-bundle</Fragment-Host>
Nazwę pakunku macierzystego można poznać przez utworzenie docelowego manifestu przez wykonanie polecenia mvn package i odczytanie nagłówka Bundle-SymbolicName w utworzonym META-INF/MANIFEST.MF.

Tworzę pakunki poleceniem mvn package i uruchamiam na Equinoksie (najświeższa wersja do pobrania ze strony equinox osgi downloads, np. Equinox Stable Build: 3.5M2, chociaż adres na stronie nie działa! Można skorzystać z jeszcze innego adresu equinox osgi downloads, gdzie można pobrać Equinox Stable Build: 3.5M1). Początkowo korzystałem z Equinoksa dostarczanego w ramach Eclipse Ganymede - plugins/org.eclipse.osgi_3.4.0.v20080605-1900.jar, ale przy finalnym uruchomieniu przeniosłem się na nowszą wersję - org.eclipse.osgi_3.5.0.v20080804-1730.jar, wyłącznie ze względów bycia na bieżąco.

Dla dociekliwych zaleca się lekturę niewielkiego podręcznika Equinoksa - Equinox QuickStart Guide.
 jlaskowski@work /cygdrive/c/apps/eclipse
$ java -jar plugins/org.eclipse.osgi_3.4.0.v20080605-1900.jar -console

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900

osgi> install file:/C:/projs/sandbox/springdm-fragment-bundle/target/springdm-fragment-bundle-1.0.jar
Bundle id is 1

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900
1 INSTALLED pl.jaceklaskowski.springdm.fragment.springdm-fragment-bundle_1.0.0

osgi> start 1
org.osgi.framework.BundleException: A fragment bundle cannot be started:
file:/C:/projs/sandbox/springdm-fragment-bundle/target/springdm-fragment-bundle-1.0.jar [1]
at org.eclipse.osgi.framework.internal.core.BundleFragment.startWorker(BundleFragment.java:224)
at org.eclipse.osgi.framework.internal.core.AbstractBundle.start(AbstractBundle.java:265)
at org.eclipse.osgi.framework.internal.core.AbstractBundle.start(AbstractBundle.java:257)
at org.eclipse.osgi.framework.internal.core.FrameworkCommandProvider._start(FrameworkCommandProvider.java:257)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.eclipse.osgi.framework.internal.core.FrameworkCommandInterpreter.execute(FrameworkCommandInterpreter.java:150)
at org.eclipse.osgi.framework.internal.core.FrameworkConsole.docommand(FrameworkConsole.java:302)
at org.eclipse.osgi.framework.internal.core.FrameworkConsole.console(FrameworkConsole.java:287)
at org.eclipse.osgi.framework.internal.core.FrameworkConsole.run(FrameworkConsole.java:223)
at java.lang.Thread.run(Thread.java:595)

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900
1 INSTALLED pl.jaceklaskowski.springdm.fragment.springdm-fragment-bundle_1.0.0

osgi> install file:/C:/projs/sandbox/springdm-host-bundle/target/springdm-host-bundle-1.0.jar
Bundle id is 2

osgi> start 2

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900
1 RESOLVED pl.jaceklaskowski.springdm.fragment.springdm-fragment-bundle_1.0.0
Master=2
2 ACTIVE pl.jaceklaskowski.springdm.fragment.springdm-host-bundle_1.0.0
Fragments=1

osgi> bundle 1
file:/C:/projs/sandbox/springdm-fragment-bundle/target/springdm-fragment-bundle-1.0.jar [1]
Id=1, Status=RESOLVED Data Root=C:\apps\eclipse\plugins\configuration\org.eclipse.osgi\bundles\1\data
No registered services.
No services in use.
Exported packages
pl.jaceklaskowski.springdm.fragment; version="0.0.0"[exported]
No imported packages
Host bundles
file:/C:/projs/sandbox/springdm-host-bundle/target/springdm-host-bundle-1.0.jar [2]
No named class spaces
No required bundles

osgi> bundle 2
file:/C:/projs/sandbox/springdm-host-bundle/target/springdm-host-bundle-1.0.jar [2]
Id=2, Status=ACTIVE Data Root=C:\apps\eclipse\plugins\configuration\org.eclipse.osgi\bundles\2\data
No registered services.
No services in use.
Exported packages
pl.jaceklaskowski.springdm.fragment; version="0.0.0"[exported]
No imported packages
Fragment bundles
file:/C:/projs/sandbox/springdm-fragment-bundle/target/springdm-fragment-bundle-1.0.jar [1]
Named class space
pl.jaceklaskowski.springdm.fragment.springdm-host-bundle; bundle-version="1.0.0"[provided]
No required bundles

osgi> close
Dodam do pakunku macierzystego funkcjonalność, która przy każdorazowym zainstalowaniu nowego pakunku będącym rozszerzeniem (pakunkiem częściowym) dla niego, wypisze dostępne pliki wchodzącego w skład przestrzeni pakunku. Dzięki metodzie org.osgi.framework.Bundle.findEntries(String,String,boolean) pakunek ma możliwość przejrzenia wszystkich zasobów zawartych w nim (bezpośrednio w jego pliku jar) oraz wszystkich rozszerzeniach (pakunkach częściowych). Nie należy mylić działania tej metody z metodą Bundle.getResource(String) czy Bundle.getResources(String), które działają na całej przestrzeni klas dostęnych dla pakunku (co może być rozbudowane w porównaniu z przestrzenią pakunku o deklaracje w nagłówku Import-Package).

Definiuję aktywator w pakunku macierzystym, który zarejestruje słuchacza reagującego na zdarzenia rozwiązania (stan RESOLVED) pakunków (podpieram się artykułem Bundle.findEntries() i spring-osgi-bundle-archetype w akcji - monitorowanie pakunków OSGi z określoną strukturą katalogową).
 package pl.jaceklaskowski.springdm.fragment.internal;

import java.util.Dictionary;
import java.util.Enumeration;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;

public class Aktywator implements BundleActivator {

private final class Sluchacz implements BundleListener {
private String nazwaPakunkuMacierzystego;

public Sluchacz(String nazwaPakunkuMacierzystego) {
this.nazwaPakunkuMacierzystego = nazwaPakunkuMacierzystego;
}

public void bundleChanged(BundleEvent event) {
Bundle bundle = event.getBundle();
String nazwaPakunku = bundle.getSymbolicName();
// Sprawdź, czy jest pakunkiem częściowym
Dictionary<?, ?> headers = bundle.getHeaders();
String pakunekMacierzysty = (String) headers.get("Fragment-Host");
if (!nazwaPakunkuMacierzystego.equalsIgnoreCase(pakunekMacierzysty)) {
return;
}
switch (event.getType()) {
case BundleEvent.RESOLVED:
System.out.println("Pakunek czesciowy " + nazwaPakunku + " w stanie RESOLVED");
wyswietlLiczbeDostepnychPlikow(bundle);
break;
case BundleEvent.STOPPED:
System.out.println("Pakunek czesciowy " + nazwaPakunku + " w stanie STOPPED");
wyswietlLiczbeDostepnychPlikow(bundle);
break;
}
}
}

private BundleListener sluchacz;

public void start(BundleContext context) throws Exception {
String nazwaPakunku = context.getBundle().getSymbolicName();
System.out.println("Pakunek macierzysty " + nazwaPakunku + " w stanie STARTING");
sluchacz = new Sluchacz(nazwaPakunku);
System.out.println("...instalacja " + sluchacz);
context.addBundleListener(sluchacz);
wyswietlLiczbeDostepnychPlikow(context.getBundle());
}

public void stop(BundleContext context) throws Exception {
String nazwaPakunku = context.getBundle().getSymbolicName();
System.out.println("Pakunek macierzysty " + nazwaPakunku + " w stanie STOPPING");
System.out.println("...usuniecie " + sluchacz);
context.removeBundleListener(sluchacz);
wyswietlLiczbeDostepnychPlikow(context.getBundle());
}

private void wyswietlLiczbeDostepnychPlikow(Bundle bundle) {
int liczbaPlikow = 0;
Enumeration<?> entries = bundle.findEntries("/", "*", true);
for (; entries.hasMoreElements(); entries.nextElement()) {
liczbaPlikow++;
}
System.out.println("+++ Liczba plikow: " + liczbaPlikow);
}

}
Pierwsze uruchomienie przykładu zakończyło się niepowodzeniem.
 jlaskowski@work /cygdrive/c/apps/eclipse
$ java -jar plugins/org.eclipse.osgi_3.4.0.v20080605-1900.jar -console

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900

osgi> install file:/C:/projs/sandbox/springdm-host-bundle/target/springdm-host-bundle-1.0.jar
Bundle id is 1

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900
1 INSTALLED pl.jaceklaskowski.springdm.fragment.springdm-host-bundle_1.0.0

osgi> start 1
org.osgi.framework.BundleException: The bundle could not be resolved. Reason:
Missing Constraint: Import-Package: pl.jaceklaskowski.springdm.fragment.internal; version="0.0.0"
at org.eclipse.osgi.framework.internal.core.BundleHost.startWorker(BundleHost.java:305)
at org.eclipse.osgi.framework.internal.core.AbstractBundle.start(AbstractBundle.java:265)
at org.eclipse.osgi.framework.internal.core.AbstractBundle.start(AbstractBundle.java:257)
at org.eclipse.osgi.framework.internal.core.FrameworkCommandProvider._start(FrameworkCommandProvider.java:257)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.eclipse.osgi.framework.internal.core.FrameworkCommandInterpreter.execute(FrameworkCommandInterpreter.java:150)
at org.eclipse.osgi.framework.internal.core.FrameworkConsole.docommand(FrameworkConsole.java:302)
at org.eclipse.osgi.framework.internal.core.FrameworkConsole.console(FrameworkConsole.java:287)
at org.eclipse.osgi.framework.internal.core.FrameworkConsole.run(FrameworkConsole.java:223)
at java.lang.Thread.run(Thread.java:595)

osgi> close
O dziwo nie doświadczałem tych problemów podczas pracy z Apache Felix (!) Najwyraźniej Equinox jest bardziej restrykcyjny i sprawdzając poprawność nagłówków, dla każdego Import-Package weryfikuje widoczność pakietów w przestrzeni klas. Jako, że domyślnie pakiet pl.jaceklaskowski.springdm.fragment.internal jest domyślnie wyłączany z Export-Package przez Spring-DM (a właściwie niewprost przez bnd wywoływane przez wtyczkę maven-bundle-plugin, która jest tak konfigurowana przez archetyp spring-osgi-bundle-archetype w pom.xml) pojawia się komunikat błędu o niespełnieniu wymagania nałożonego przez Import-Package. Analizując źródła projektu Spring-DM (dokładniej pomy w spring-osgi-extender oraz spring-osgi) kończę z następującą konfiguracją dla maven-bundle-plugin:
 <plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<version>1.4.0</version>
<configuration>
<manifestLocation>META-INF</manifestLocation>
<instructions>
<Export-Package>!pl.jaceklaskowski.springdm.fragment.*internal*</Export-Package>
<Private-Package>pl.jaceklaskowski.springdm.fragment.*internal*</Private-Package>
<Include-Resource>src/main/resources</Include-Resource>
<Bundle-Activator>pl.jaceklaskowski.springdm.fragment.internal.Aktywator</Bundle-Activator>
<Bundle-Version>2</Bundle-Version>
</instructions>
</configuration>
</plugin>
Kluczem do sukcesu jest nagłówek Private-Package.

Warto pomiędzy uruchomieniami Equinoksa usuwać jego katalog konfiguracyjny - configuration, aby poprzednia konfiguracja nie kolidowała na bieżące testy. Katalog configuration tworzony jest w katalogu, w którym znajduje się uruchomieniowy jar Equinoksa.
 jlaskowski@work /cygdrive/c/apps/equinox
$ rm -rf configuration
Podczas analizy poniższego działania pakunków macierzystego i częściowego na Equinoksie proszę zwrócić uwagę na komunikat +++ Liczba plikow, który informuje o liczbie plików dostępnych w pakunku macierzystym (przypominam, że pakunek częściowy nie jest pełnoprawnym pakunkiem OSGi, tzn. obostrzenia OSGi zugożają go sprowadzając do roli pakunku rozszerzającego).
 jlaskowski@work /cygdrive/c/apps/equinox
$ java -jar org.eclipse.osgi_3.5.0.v20080804-1730.jar -console

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.5.0.v20080804-1730

osgi> install file:/C:/projs/sandbox/springdm-host-bundle/target/springdm-host-bundle-1.0.jar
Bundle id is 1

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.5.0.v20080804-1730
1 INSTALLED pl.jaceklaskowski.springdm.fragment.springdm-host-bundle_2.0.0

osgi> start 1
Pakunek macierzysty pl.jaceklaskowski.springdm.fragment.springdm-host-bundle w stanie STARTING
...instalacja pl.jaceklaskowski.springdm.fragment.internal.Aktywator$Sluchacz@ca470
+++ Liczba plikow: 18

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.5.0.v20080804-1730
1 ACTIVE pl.jaceklaskowski.springdm.fragment.springdm-host-bundle_2.0.0

osgi> install file:/C:/projs/sandbox/springdm-fragment-bundle/target/springdm-fragment-bundle-1.0.jar
Bundle id is 2

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.5.0.v20080804-1730
1 ACTIVE pl.jaceklaskowski.springdm.fragment.springdm-host-bundle_2.0.0
2 INSTALLED pl.jaceklaskowski.springdm.fragment.springdm-fragment-bundle_1.0.0

osgi> refresh 1

osgi> Pakunek macierzysty pl.jaceklaskowski.springdm.fragment.springdm-host-bundle w stanie STOPPING
...usuniecie pl.jaceklaskowski.springdm.fragment.internal.Aktywator$Sluchacz@ca470
+++ Liczba plikow: 18
Pakunek macierzysty pl.jaceklaskowski.springdm.fragment.springdm-host-bundle w stanie STARTING
...instalacja pl.jaceklaskowski.springdm.fragment.internal.Aktywator$Sluchacz@42552c
+++ Liczba plikow: 29


osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.5.0.v20080804-1730
1 ACTIVE pl.jaceklaskowski.springdm.fragment.springdm-host-bundle_2.0.0
Fragments=2
2 RESOLVED pl.jaceklaskowski.springdm.fragment.springdm-fragment-bundle_1.0.0
Master=1

osgi> uninstall 2

osgi> refresh 1

osgi> Pakunek macierzysty pl.jaceklaskowski.springdm.fragment.springdm-host-bundle w stanie STOPPING
...usuniecie pl.jaceklaskowski.springdm.fragment.internal.Aktywator$Sluchacz@42552c
Pakunek macierzysty pl.jaceklaskowski.springdm.fragment.springdm-host-bundle w stanie STARTING
...instalacja pl.jaceklaskowski.springdm.fragment.internal.Aktywator$Sluchacz@1113622
+++ Liczba plikow: 18


osgi> close

Pakunek macierzysty pl.jaceklaskowski.springdm.fragment.springdm-host-bundle w stanie STOPPING
...usuniecie pl.jaceklaskowski.springdm.fragment.internal.Aktywator$Sluchacz@1113622
+++ Liczba plikow: 18
Ciekawe przedstawienie tematu dostępne również w bezpłatnej książce OSGi in Practice w rozdziale The Extender Model.

Pojawił się nowy harmonogram nowej wersji NetBeans 6.5 - NetBeans NB65Milestones. Przyjdzie jeszcze trochę poczekać na finalną wersję NetBeans 6.5 - 12 listopada 2008.