Lokalizacja (wersje językowe) komunikatów o błędach z wbudowanego w JRE silnika JavaScript-u w OSGi z wykorzystaniem extension:=extclasspath

Aplikacja nad którą pracuję umożliwia użytkownikom rozszerzenia funkcjonalności przez pisanie skryptów w JS (szczegóły nie są istotne). Oczywiście, gdy pisze się kod pojawiają się komunikaty o błędach a wymaganiem klienta było by były w języku polskim. Ponieważ domyślnie dostępne są dwie wersje językowe – angielska i francuska – stworzony został plik sun/org/mozilla/javascript/internal/resources/Messages_pl.properties z polskimi komunikatami który następnie został opakowany w plik jar sun.org.mozilla.javascript.nl.PL_pl.jar. W końcu plik ten został przekazany jako rozszerzenie bootclasspath aplikacji za pomocą opcji

-Xbootclasspath/a:lib\sun.org.mozilla.javascript.nl.PL_pl.jar

i rozwiązanie takie działo przed dłuższy czas (także w OSGi).

W trakcie migracji na Equinox p2 okazało się jednak, że z wielu względow -Xbootclasspath/a musi odejść – a mnie przypadło wymyślenie jak.

Pierwszym krokiem jest oczywiście ustalenie, dlaczego działało dotychczasowe rozwiązanie. Analiza wykazała, że komunikaty wczytywane są w klasie sun.org.mozilla.javascript.internal.ScriptRuntime wywołaniem

ResourceBundle.getBundle("sun.org.mozilla.javascript.internal.resources.Messages", locale)

a oryginalne lokalizacje (angielska i francuska) umieszczone są w <jre>/lib/resources.jar. Dalsza analiza wykazała, że w tym przypadku do wczytywania lokalizacji wykorzystywany jest tzw. classloader aplikacji. Żeby zrozumieć, dlaczego wyszukiwanie lokalizacji powodzi się trzeba pamiętać o trzech podstawowych faktach dotyczących classloader-ów

  • są zorganizowane są w strukturę drzewiastą i powiązane relacją dziecko-rodzic
  • zwykle przed wczytaniem klasy\zasobu samemu następuje próba delegacji do rodzica (parent-first delegation model – ale model child-first też jest popularny. Ważne jest to, że classloader „widzi” klasy i zasoby swoje i przodków – ale nie potomków
  • główne classloadery (boot, ext, app) zorganizowane są następującojvm-classloader-hierarchy

Podczas wyszukiwania zasobu sun/org/mozilla/javascript/internal/resources/Messages_pl.properties Application Classloader deleguje do Extension ClassLoader który z kolei deleguje do Boostrap ClassLoader który znajduje zasób dzięki -Xbootclasspath – i voilà , mamy polskie komunikaty.

Dla osób dobrze znających OSGi naturalnym sposobem na rozwiązanie przyjazne dla OSGi wydają się tzw. framework extensions czyli pakunki częściowe (fragmenty) których hostem jest pakunek systemowy – system.bundle – i które dodatkowo definiują tzw. punkt rozszerzeń. Zgodnie ze specyfikacją OSGi poprawne są dwie wartości

  • framework
  • bootclasspath

opisane np. tutaj i w specyfikacji OSGi
Pierwszy jest dla nas bezużyteczny (pytanie dla uważnych – dlaczego). Drugi wydaje się idealny jako odpowiednik parametru -Xbootclasspath ale w praktyce okazuje się, że frameworki OSGI nie implementują go z przyczyn technicznych (patrz np. Eclipse/127724). Co nam pozostaje? Okazuje się, że Equinox OSGi obsługuje niestandardowy punkt rozszerzeń który idealnie sprawdza się w tym przypadku. Można bowiem stworzyć fragment który rozszerzy classloader aplikacji korzystając ze składni Fragment-Host: system.bundle; extension:=extclasspath. Wystarczy stworzyć fragment z następującym manifestem

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Mozilla Rhino Localization PL
Bundle-SymbolicName: sun.org.mozilla.javascript.nl.PL_pl
Bundle-Version: 1.0.0.qualifier
Fragment-Host: system.bundle; extension:=extclasspath
Bundle-RequiredExecutionEnvironment: JavaSE-1.6

który zawiera zasób sun/org/mozilla/javascript/internal/resources/Messages_pl.properties

Jak Equinox OSGi obsługuje framework extensions

czyli jak mawia Linus Torvalds Talk is cheap. Show me the code. W tym przypadku kluczowe są dwa miejsca. Pierwszym jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorageHook.loadManifest gdzie wczytywany jest manifest pakunku

static void loadManifest(BaseData target, Dictionary<String, String> manifest) throws BundleException {
	try {
		target.setVersion(Version.parseVersion(manifest.get(Constants.BUNDLE_VERSION)));
	} catch (IllegalArgumentException e) {
		target.setVersion(new InvalidVersion(manifest.get(Constants.BUNDLE_VERSION)));
	}
	ManifestElement[] bsnHeader = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, manifest.get(Constants.BUNDLE_SYMBOLICNAME));
	int bundleType = 0;
	if (bsnHeader != null) {
		target.setSymbolicName(bsnHeader[0].getValue());
		String singleton = bsnHeader[0].getDirective(Constants.SINGLETON_DIRECTIVE);
		if (singleton == null)
			singleton = bsnHeader[0].getAttribute(Constants.SINGLETON_DIRECTIVE);
		if ("true".equals(singleton)) //$NON-NLS-1$
			bundleType |= BundleData.TYPE_SINGLETON;
	}
	// check that the classpath is valid
	String classpath = manifest.get(Constants.BUNDLE_CLASSPATH);
	ManifestElement.parseHeader(Constants.BUNDLE_CLASSPATH, classpath);
	target.setClassPathString(classpath);
	target.setActivator(manifest.get(Constants.BUNDLE_ACTIVATOR));
	String host = manifest.get(Constants.FRAGMENT_HOST);
	if (host != null) {
		bundleType |= BundleData.TYPE_FRAGMENT;
		ManifestElement[] hostElement = ManifestElement.parseHeader(Constants.FRAGMENT_HOST, host);
		if (Constants.getInternalSymbolicName().equals(hostElement[0].getValue()) || Constants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(hostElement[0].getValue())) {
			String extensionType = hostElement[0].getDirective("extension"); //$NON-NLS-1$
			if (extensionType == null || extensionType.equals("framework")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_FRAMEWORK_EXTENSION;
			else if (extensionType.equals("bootclasspath")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_BOOTCLASSPATH_EXTENSION;
			else if (extensionType.equals("extclasspath")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_EXTCLASSPATH_EXTENSION;
		}
	} else {
		String composite = manifest.get(COMPOSITE_HEADER);
		if (composite != null) {
			if (COMPOSITE_BUNDLE.equals(composite))
				bundleType |= BundleData.TYPE_COMPOSITEBUNDLE;
			else
				bundleType |= BundleData.TYPE_SURROGATEBUNDLE;
		}
	}
	target.setType(bundleType);
	target.setExecutionEnvironment(manifest.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT));
	target.setDynamicImports(manifest.get(Constants.DYNAMICIMPORT_PACKAGE));
}

drugim jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorage.processExtension gdzie dane rozszerzenie jest obsługiwane

protected void processExtension(BaseData bundleData, byte type) throws BundleException {
	if ((bundleData.getType() & BundleData.TYPE_FRAMEWORK_EXTENSION) != 0) {
		validateExtension(bundleData);
		processFrameworkExtension(bundleData, type);
	} else if ((bundleData.getType() & BundleData.TYPE_BOOTCLASSPATH_EXTENSION) != 0) {
		validateExtension(bundleData);
		processBootExtension(bundleData, type);
	} else if ((bundleData.getType() & BundleData.TYPE_EXTCLASSPATH_EXTENSION) != 0) {
		validateExtension(bundleData);
		processExtExtension(bundleData, type);
	}
}

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s