Monthly Archives: March 2012

Script imports with a cleaner API

Update: The patch was transferred to the Alfresco JIRA and can be tracked as ALF-13631.

In my attempts to remote debug Alfresco JavaScript using Eclipse JSDT , the way script imports are handled in Alfresco proved to be one of the key obstacles. The current approach merges scripts in a pre-processor style stage just before they are actually executed. The directive used in this mechanism in both the Repository and Share can be used as follows:

<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()

Prior to execution, a script is scanned for import tags starting at the very first line, collecting dependencies – even transitive ones – for the merging step. The scan process stops at the first non-whitespace character that cannot be matched to an import directive. This approach leads to some restrictions that apply to scripts:

  1. Imports are only possible in the head segment of a script before any actual processing logic.
  2. Scripts cannot be dynamically imported as the import tags resource information can not be altered by JavaScript processing logic.
  3. Syntax checks performed in IDEs rightfully complain about the syntactically invalid import directive.
  4. Rhino exceptions show a line number that does not match the source code – developers are often forced to manually calculate the line offsets of imports to find the affected line in the original source file.
  5. Breakpoints for debugging cannot be reliably set prior to the first execution and thus merging of a script. This affects my preferred choice of the Eclipse JSDT more than the embedded Rhino Debugger UI, since the former currently does not allow interaction with the merged scripts unlike the latter.

I had planned for a while to find an alternative solution to script imports – not only to allow for remote debugging using JSDT but to be able to make use of a more flexible means of using / reusing for scripts within Alfresco. This weekend I finally had the time to work on this. My goal was to provide a small extension to the JavaScript API that allows importing of scripts in arbitrary places within a script. Additionally I wanted to allow for extensibility of the lookup mechanism involved without requiring extenders to dive into the actual JavaScript API.

Alfresco itself already provides a means to add additional root objects in the JavaScript API through its javaScriptExtension beans. This feature was not sufficient for what I had in mind – the Java-based services that could be provided that way do not have the necessary level of access to the execution context of the Rhino engine. A patch of the RhinoScriptProcesor on the other hand easily allowed for adding a native JavaScript function in the global context, that – as a side effect of implementing it in the script processor – also has access to important processor internals like the script cache. The final import function can be used in JavaScript in the following manner:

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

The three parameters of the function are defined as follows:

  1. The unique identifier of lookup component to be used for the import. Using “legacy” reuses the current lookup concept that is used by the pre-processor merging and allows developers to easily adapt existing code by running a simple RegEx search & replace. Additional lookup components I’ve implemented are for dedicated “classpath” and “xpath” based resolution.
  2. A text-based reference to a script that can be resolved by the chosen lookup component.
  3. A boolean parameter that specifies wether the import should fail with a ScriptException if the script reference cannot be resolved.

The function resolves and executes the imported script in the same context and scope of the caller. The imported script has access to variables and functions defined by the importing script and can interact with those. The boolean return value of the function can be used to check for a successful resolution of the import in use-cases a failed resolution does not automatically raise an exception. As a JavaScript function it can be used at any time in the execution of a script and be used in conjunction with variables as parameters for dynamically importing arbitrary scripts.

Additional lookup components can be added by implementing a trivial Java interface and linking it with the RhinoScriptProcessor via Spring. The interface defines a single method for the resolution of the text-based script reference. A context parameter providing the resolved script location of the script performing the import is provided – if available – to allow for resolution of relative references.

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">
        <property name="services" ref="ServiceRegistry"/>
        <property name="storeUrl">
            <value>${spaces.store}</value>
        </property>
        <property name="storePath">
            <value>${spaces.company_home.childname}</value>
        </property>
    </bean>

So far we have dealt with improving script imports for the Repository tier. I originally planned to use the same concept for Share / Spring Surf, but soon realized that Spring Surf / Web Scripts already provide a utility for loading of dependencies from different sources. This utility is already being used in the pre-processor stage when scripts are merged. Using a so-called “Store” allows for the resolution of abstract document paths within the classpath of an application or a remote store, such as the Alfresco Repository. This mechanism is sufficiently extensible that it would make no sense to add another.

I have provided a reduced JavaScript API within Share / Spring Surf which makes use of the existing mechanism – the “legacy” mode of the Repository can be seen as always / implicitly enforced.

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

The function is available for web scripts and template controllers, and support both explicit classpath resolution – as in the example above – as well as abstract document and relative paths. Relative resolution is only supported when the importing script is loaded from the classpath. In such a case relative resolution is attempted first, falling back to resolving an abstract path via the Store-concept (relative paths cannot in all instances be distinguished from abstract paths).

To test the new API I have applied search & replace to the my local Alfresco 4.0 Enterprise installation and replaced ALL instances of the old import directive with the new function. The migration worked without any issues so far. The Rhino Debugger UI shows all scripts formerly merged as individual scripts and breakpoints can not be set prior to the execution of any script. Line numbers in JavaScript exception are finally correct in all cases I was able to test.