Archiv nach Monaten: März 2012

Skriptimporte mit sauberer API

Update: Der Patch wurde ins Alfresco JIRA übertragen und kann unter ALF-13631 verfolgt werden.

Im Rahmen meiner Bemühungen, mittels Eclipse JSDT Alfresco JavaScript remote zu debuggen, stellte sich die Art und Weise, wie in Alfresco JavaScript andere Skripte importiert werden, als eines der zentralen Probleme heraus. Aktuell wird hier ein Ansatz ähnlich einem Präprozessor verwendet und Skripte erst unmittelbar vor ihrer Ausführung endgültig zusammengefügt. Die für diesen Mechanismus notwendige Importdirektive sieht sowohl im Repository als in Share wie folgt aus:

<import resource="classpath:/alfresco/templates/org/alfresco/import/alfresco-util.js">
/**
 * Main entrypoint
 */
function main()
{
   var activityFeed = getActivities();
   var activities = [], activity, item, summary, fullName, date, sites = {}, siteTitles = {};
   var dateFilter = args.dateFilter, oldestDate = getOldestDate(dateFilter);
   ...
}
main()</import>

Vor der Ausführung wird ein Skript von Beginn an nach import-Tags durchsucht und alle Fragmente zu einer Gesamtdatei zusammen gefügt. Der Prozess stoppt beim ersten Zeichen (Whitespaces ausgenommen), welches nicht Teil eines import-Tags ist. Dieses Vorgehen hat einschränkende Konsequenzen:

  1. Imports an anderen Stellen außer dem Kopf eines Skripts sind nicht möglich.
  2. Dynamische Imports sind nicht möglich, d.h. es können nicht dynamisch im Laufe der Logikausführung Skripte eingebunden werden.
  3. Syntaxprüfungen von IDEs bemängeln die Importsyntax zu Recht als nicht valide.
  4. Rhino Fehlermeldungen zeigen eine Zeilennummer an, die nicht mit dem Quellcode übereinstimmt – man ist häufig gezwungen, über alle Imports manuell und fehleranfällig die echte Zeilennummer in der Originaldatei auszurechnen, um sich besagte Zeile anschauen zu können.
  5. Breakpoints beim Debuggen können nicht vor der Ausführung eines Skripts gesetzt werden. Das trifft meinen Ansatz mit JSDT härter als die mitgelieferten Rhino Debugger UI, da mir im JSDT die aggregierten Dateien zu keinem Zeitpunkt zur Verfügung stehen.

Ich hatte mir schon länger vorgenommen, hier einen alternativen Ansatz zu finden – nicht nur um sinnvolles Debuggen mit JSDT zu ermöglichen, sondern auch auf eine flexiblere Nutzung / Wiederverwendung von Skripten zurück greifen zu können. Dieses Wochenende blieb mir dann auch endlich die ersehnte Zeit, hier aktiv zu werden. Ziel sollte es sein, eine kleine JavaScript API Erweiterung zur Verfügung zu stellen, mit der sich andere Skripte an beliebigen Stellen im Code importieren lassen. Zusätzlich wollte ich es ermöglichen, dass der Skript-Lookup Mechanismus einfach durch zusätzliche Lookup-Komponenten erweitert werden kann, ohne in die JavaScript API eingreifen zu müssen.

Alfresco selber bietet mit seinen javaScriptExtension Beans Möglichkeiten, neue Root Objekte in die API zu integrieren. Für mein Vorhaben war dieser Ansatz jedoch nicht ausreichend – die Java-basierten Services, die man über diesen Mechanismus zur Verfügung stellen kann, haben nicht den notwendigen Zugriff auf den Ausführungskontext der Rhino Script Engine. Mit einem Patch des RhinoScriptProcesor dagegen lässt sich eine native JavaScript Funktion einfach im globalen Kontext bekannt machen, die zusätzlich den notwendigen Zugriff auf Interna des Prozessors wie u.A. den ScriptCache hat. Die fertige Importfunktion steht in JavaScript wie folgt zur Verfügung:

importScript("legacy", "classpath:/alfresco/templates/webscripts/org/alfresco/repository/forms/pickerresults.lib.js", true);

Die drei Parameter der Funktion werden wie folgt verwendet:

  1. Die zu verwendende Lookup-Komponente für den Import. Mit “legacy” wird der grundsätzliche Ansatz des alten import-Tags verwendet, sodass sich bestehender Code mit RegEx-basiertem Suchen & Ersetzen schnell anpassen lässt. Weiter habe ich zwei dedizierte Komponenten für “classpath” und “xpath” umgesetzt.
  2. Eine textbasierte Referenz auf ein Skript, welches durch die gewählte Importer-Komponente aufzulösen ist.
  3. Ein Boolean-Parameter, welcher bestimmt, ob der Import bei einem nicht auflösbaren Import mit einer ScriptException fehlschlagen soll.

Die Funktion führt das zu importierende Skript im gleichen Kontext und Scope des Aufrufs aus. Das importierte Skript kann somit auf Variablen und Funktionen des aufrufenden Skripts zugreifen und mit diesen interagieren. Die Funktion hat einen Boolean-Rückgabeparameter, über welchen ein erfolgreich durchgeführter Import geprüft werden kann, wenn ein nicht auflösbarer Import nicht automatisch zu einem Fehlschlag führt. Als JavaScript Funktion kann der Import an jeder beliebigen Stelle eines Skripts erfolgen und bei Verwendung von berechneten Variablen dynamisch beliebige Skripte importieren.

Zusätzliche Import-Komponenten können durch Implementierung eines kleinen Java Interfaces und Verknüpfung mit dem RhinoScriptProcessor zur Verfügung gestellt werden. Das Interface definiert dabei nur eine Methode zum Auflösen des Referenzparameters. Mit einem zusätzlichen Kontextparameter für den Ablageort des aktuell ausgeführten Skripts lassen sich auch relative Referenzen realisieren.

public interface ScriptLocator {
 
	/**
	 * Resolves a string-based script location to a wrapper instance of the
	 * {@link ScriptLocation} interface usable by the repository's script
	 * processor. Implementations may support relative script resolution - a
	 * reference location is provided in instances an already running script
	 * attempts to import another.
	 *
	 * @param referenceLocation
	 *            a reference script location if a script currently in execution
	 *            attempts to import another, or {@code null} if either no
	 *            script is currently being executed or the script being
	 *            executed is not associated with a script location (e.g. a
	 *            simple script string)
	 * @param locationValue
	 *            the simple location to be resolved to a proper script location
	 * @return the resolved script location or {@code null} if it could not be resolved
	 *
	 */
	ScriptLocation resolveLocation(ScriptLocation referenceLocation,
			String locationValue);
}
    <bean id="javaScriptProcessor" class="org.alfresco.repo.jscript.RhinoScriptProcessor" init-method="register">
        <!-- ... -->
        <property name="scriptLocators">
            <map>
                <entry key="classpath">
                    <ref bean="javaScriptProcessor.classpathScriptLocator"/>
                </entry>
                <entry key="xpath">
                    <ref bean="javaScriptProcessor.xPathScriptLocator"/>
                </entry>
                <entry key="legacy">
                    <ref bean="javaScriptProcessor.legacyScriptLocator"/>
                </entry>
            </map>
        </property>
    </bean>
 
    <bean id="javaScriptProcessor.classpathScriptLocator" class="org.alfresco.repo.jscript.ClasspathScriptLocator" />
 
    <bean id="javaScriptProcessor.xPathScriptLocator" class="org.alfresco.repo.jscript.XPathScriptLocator">
        <property name="serviceRegistry" ref="ServiceRegistry"/>
    </bean>
 
    <bean id="javaScriptProcessor.legacyScriptLocator" class="org.alfresco.repo.jscript.LegacyScriptLocator">
        &lt;property name="services" ref="ServiceRegistry"/>
        &lt;property name="storeUrl">
            <value>${spaces.store}</value>
 
        <property name="storePath">
            <value>${spaces.company_home.childname}</value>
        </property>
    </bean>

Soweit zur Umsetzung auf Seiten des Repository. Den gleichen Ansatz wollte ich für Share bzw. Spring Surf äquivalent umsetzen – allerdings bin ich schnell über die Tatsache gestolpert, dass Spring Surf / Web Scripts grundsätzlich schon über einen Ansatz zum Auflösen von Abhängigkeiten verfügen. Dieser kommt im aktuellen Präprozessorschritt der Skript-Aggregation auch schon zum Tragen. Mit Hilfe eines sog. “Store” lassen sich schon längst abstrakte Pfade entweder auf dem Classpath oder einem Remote-Store, wie z.B. einem Alfresco Repository, auflösen. Dieser Mechanismus ist ausreichend erweiterbar, sodass hier ein neuer Ansatz fehl am Platz wäre.

Für Share / Spring Surf habe ich daher eine abgespeckte API in JavaScript zur Verfügung gestellt, welche auf den bestehenden Mechanismus zurückgreift – der “legacy” Modus aus dem Repository wird hiermit implizit vorgegeben.

importScript("classpath:/alfresco/templates/org/alfresco/import/alfresco-util.js", true);

Die Funktion steht sowohl für Web Scrpts als auch Template Controller zur Verfügung, und unterstützt neben explizitem Classpath wie im obigen Beispiel auch besagte abstrakte Pfade und relative Auflösung. Eine relative Auflösung wird dabei nur unterstützt, wenn das aktuell ausgeführte Skript vom Classpath geladen wurde. In einem solchen Fall wird zuerst eine relative Auflösung versucht, und erst im Anschluss – im Falle eines Fehlschlags – der angegebene Dateipfad versucht als abstrakter Pfad über einen Store zu einem Skript aufzulösen (relative Pfadangaben sind nicht eindeutig von abstrakten zu differenzieren).

Um die neue API zu testen habe ich in meiner lokalen Alfresco 4.0 Enterprise Installation üer Suchen & Ersetzen ALLE Vorkommnisse des alten import-Tags mit der neuen Funktion ersetzt. Die Umstellung verlief dabei reibungslos. In der Rhino Debugger UI tauchen alle Teilskripte fortan als einzelne Skripte auf und Breakpoints lassen sich nun auch vor Ausführung / Zusammenfügen eines Skripts verlässlich setzen. Zeilennummern in JavaScript Exceptions sind endlich überwiegend korrekt.