Kinesis SoftwareKineticFusion

[Show Table of Contents]

10 RVML Reference

[Hide Table of Contents]



10.6.5 Example of User-Defined Tag Libraries

To clarify how the Tag Library functionality works we will break this section up into 3 parts :
To start, we will examine an RVML document containing extensions, we will then look at how to declare an external tag library to the KineticFusion application, and finally, a full example on creating a Tag Library for simplifying SWC components.

Using External Tag Libraries in RVML

Adding Library Support to KineticFusion

Creating an External Tag Library

10.6.5.1 Using External Tag Libraries in RVML

A example RVML document for adding SWC components to a small movie is shown below:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Movie version='7' width='600' height='550' rate='23' backgroundColor='white' compressed='Yes'
xmlns='http://www.kineticfusion.org/RVML/1.0'
xmlns:c='http://www.kineticfusion.org/components'>
<Components><!-- Add the following SWC components to the output movie -->
<Component id="Button" />
<Component id="ComboBox" />
<Component id="TextInput" />
</Components>
<Timeline>
<Frame>
<c:Button x="100" y="100" id="myButton" enabled="true" toggle="false" label="Hello">
<c:Event name="click"> if ( this.label == "Hello") this.label="Bye"; else this.label="Hello"; </c:Event>
</c:Button>
<c:ComboBox x="100" y="200" id="myCombo" enabled="true" data="1,2,3,4,5" labels="Value1, Value2, Value3, Value4, Value5">
<c:Event name="change">_level0.myTextField.text=this.selectedItem.data;</c:Event>
</c:ComboBox>
<c:TextInput x="100" y="300" id="myTextField" enabled="true"/>
<c:Button x="100" y="400" id="myClearButton" enabled="true" toggle="false" label="Clear">
<c:Event name="click"> _level0.myTextField.text=""; _level0.myCombo.selectedIndex = 0; </c:Event>
</c:Button>
</Frame>
</Timeline>
</Movie>

The first step is to declare the namespaces that will be used for our external tag libraries. In this case the namespace for the example component handler is highlighted in red above as : http://www.kineticfusion.org/components. All subsequent references to elements in this namespace can be abbreviated with the 'c' prefix. In our configuration file, which we will see shortly, we will associate our example Component Handler with this namespace. Therefore all XML elements in this namespace are processed directly by our new handler.

The example component handler uses the element name to locate the component that is to be placed on the stage. It will parse all the properties of the component and extract those properties specified as element attributes. It will finally accept Event child elements for a component ensuring that the specified event is defined for the component.

So what does the resulting SWF look like? Have a look (it's about 56K) ....

10.6.5.2 Adding Library Support to KineticFusion

All available external tag libraries are made available to the KineticFusion application by declaring them in the Tag Libraries configuration file. By default, this resides in the installation 'config' folder and an example file looks like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<TagLibraries>
	<!-- A simple tag that supports a Log element that logs any text attribute when it's encountered.
  While this is a simple handler it can be used to verify Attribute Expression values.-->
<TagLibrary namespace="http://www.kineticfusion.org/logging" 
                 factoryClass="org.kinesis.xml.handlers.logging.LogFactory" 
                classLocation="$INSTALL_PATH/lib/ExampleHandlers.jar"  />
  
 <!-- Samples handler for SWC components that converts attributes to ActionScript
  and supports named event handlers. It supports all SWC components and will iterate
  though each component's parameter set building up an initialisation ActionScript block. All element
  names are dynamic and correspond to component names.-->
<TagLibrary namespace="http://www.kineticfusion.org/components" 
                 factoryClass="org.kinesis.xml.handlers.components.ComponentFactory" 
                 classLocation="$INSTALL_PATH/lib/ExampleHandlers.jar"  />
</TagLibraries>

This file is read by KineticFusion on startup. Each external tag library is declared in this file with a <TagLibrary> element. This element takes three attributes:

  • The namespace attribute is the namespace used with the element inside the RVML document
  • The factoryClass attribute is the class that provides ElementHandlers to process elements in the namespace
  • The classLocation is the directory or Jar file containing the factory class.

10.6.5.3 Creating An External Tag Library

This example will look at building a user tag library for the simplified processing of SWC components as illustrated above.

10.6.5.3.1 Creating The Factory Class

To implement an external tag library, the developer must first write a class that inherits from the com.kinesis.xml.libraries.TagHandlerFactoryType interface and must have a default constructor. The interface contains a single method:

/**
 * Factory class for creating new Tag Handlers
 */
public interface TagHandlerFactoryType {

    /**
     * Return a new SAX handler for the specified element in the given context.
     * The factory can add any returned handler to the parent handler to add
     * persistent handling for this element in the specified context.
     * 
     * @param context
     *          The current context. This provides the contextual information
     *          on the movie including the current hierarchy of Sax element
     *          handlers
     * @param namespaceURI
     *          The namespace URI of the element
     * @param localName
     *          The local name of the element
     * @return The handler for the element or null if there is no valid handler
     */
    public KFSaxElementHandlerType getSaxHandler(RVMLSaxContextType context,
                                                String namespaceURI, 
                                                String localName);
}

The factory class is responsible for finding a Handler object (of type KFSaxElementHandlerType) that will process the given element and the Handler object will then be passed the specified element details to process.

The implementation of this handler will create a new ComponentHandler instance and attach it to the a parent Frame handler for persistence:

    public KFSaxElementHandlerType getSaxHandler(RVMLSaxContextType context, String namespace,
            String element)
    {
        KFSaxElementHandlerType[] parentHandlers = context.getParentHandlers();
        
        // Assume the FrameHolder can come from anywhere in the hierarchy
        for( int i=0; i < parentHandlers.length; i ++ )
            if( parentHandlers[i] instanceof FrameHolderType )
            {
			     // Create the new user-defined component handler
                KFSaxElementHandlerType myHandler = new ComponentHandler( context,
                        (FrameHolderType) parentHandlers[i], namespace );
                // Ensure that the current parent element now hold persistent reference to the new handler
                parentHandlers[0].addGeneralSubElementHandler( namespace, element, myHandler );
                return myHandler;
            }
        return null;
    }

The handler ignores the input namespace argument since it is only looking after a single namespace. Element name validation takes place in the actual handler itself.

10.6.5.3.2 Outlining The Tag Handler

It is now necessary to create the actual tag handler class. As mentioned previously, all tag handlers must implement the com.kinesis.xml.KFSaxElementHandlerType interface. Rather than implement all methods of that interface, it is generally easier to extend the com.kinesis.xml.handlers.RVMLHandlerAdapter class. In this example, we need to implement the following methods:

  • handleElement() - to handle each input XML element in this namespace
  • endElement() - to process the element data once the element is closed
  • reset() - inherited from Resetable so that any handler state is reset on error

Our class outline will now look like:

import com.kinesis.xml.handlers.RVMLHandlerAdapter;
class ComponentHandler extends RVMLHandlerAdapter implements EventProcessorType, Resetable {


    /** Holds the current operation frame */
    private FrameHolderType myFrameHolder;
	
    /**
     * @param context
     * @param frameHolder
     *                      Holder for the current active frame
     * @param namespace
     *                      Namespace used to add child handlers
     */
    public ComponentHandler(RVMLSaxContextType context, FrameHolderType frameHolder,
            String namespace)
    {
        super( context );
        myFrameHolder = frameHolder;
		// Add a namespace:Event support child handler
        addGeneralSubElementHandler( namespace, "Event", new EventHandler( context, this ) );
    }
    public boolean handleElement(String namespaceURI, String element, String qname,
            AttributeProcessorType atts) throws Exception
    {
		// Code to handle element here
	 }
	
    public boolean endElement() throws Exception
    {
		// Code to store element here
	 }
	
	 public void reset()
    {
        // code to reset element here
    }

As can be seen from the constructor, the class takes an instance of a FrameHolderType object in the constructor - this will allow us to add display operations to the current frame. The FrameHolderType, as implemented by the normal RVML Frame handler, provides the FrameType object for the currently processed frame.

10.6.5.3.3 Adding a Single Child Handler for 'Event' Element

We also add a single supported child element handler to this handler for the Event element. This handler is responsible for reading any defined ActionScript event handlers.

This child handler is registered using the addGeneralSubElementHandler() method of the RVMLAdapter class. Once the child handler is registered, the RVMLAdapter class methods will automatically dispatch any Event child elements to the specified handler.

The Event handler pushes all defined events to the component handler for storage and processing, so the ComponentHandler implements a new interface EventProcessorType that is used by the EventHandler. The EventProcessorType interface defines methods for the validation and processing on events:

/**
 * Class EventProcessorType
 */
public interface EventProcessorType {

    /**
     * Does the current component support the named event
     * @param eventName
     * @return true if the component supports the named event
     */
    public boolean supportsEvent( String eventName);
    
    /**
     * Add the event script to the component
     * @param eventName
     * @param location
     * @param script
     * @return true if the script was processed successfully
     */
    public boolean addEventScript( String eventName, SourceLocationType location, String script);
}

The full code for the EventHandler class is shown below:

public class EventHandler extends AbstractActionHandler {

    private EventProcessorType myEventProcessor;

    private String myEventName;
 
    /**
     * @param context
     * @param eventProcessor Object to process new event definitions
     */
    public EventHandler(RVMLSaxContextType context, EventProcessorType eventProcessor)
    {
        super( context );
        myEventProcessor = eventProcessor;
    }

    public boolean handleAttributes(AttributeProcessorType atts) throws Exception
    {
        // Check to see if if we parse input from an external URL
        myURL = atts.getAttr( XMLConstants.ACTION_SOURCE_ATTRIBUTE, null );
        myEventName = atts.getAttr(XMLConstants.NAME_ATTRIBUTE, null) ;
        if ( myEventName == null)
        {
            myContext.getLog().error("Cannot find event name" + myContext.getLocationString());
            return false;
        }
        
        if ( !myEventProcessor.supportsEvent(myEventName))
        {
            myContext.getLog().error("Unsupported event '" + myEventName + "'" + myContext.getLocationString());
            return false;
        }
        
        return true;
    }

    public boolean endElement() throws Exception
    {
        InlineTextParameters actionParameters = getInlineTextParameters( "Component" );
        return myEventProcessor.addEventScript(myEventName, actionParameters.getLocation(), actionParameters.getScript());
    }
}

The EventHandler class inherits from the AbstractActionHandler class that is used for several RVML handlers to simplify the resolution of inline and external scripts. It only needs to implement two methods:

  • handleAttributes() - handler-specific attribute processing used to store the event name
  • endElement() - push up the retrieved event action information

10.6.5.3.4 Implement EventProcessorType in ComponentHandler

Our ComponentHandler is capable or supporting mutliple kinds of components, identified bythe element name used. The implementation of the supportsEvent() method must use the current element name to look up the component information to carry out validation. We also want to store all events that are defined for a component until the component is full processed. This means that we persist this information in the ComponentHandler class. The additions to the class will look like:

class ComponentHandler extends RVMLHandlerAdapter implements EventProcessorType, Resetable {

    /** The ActionsObject for this component */
    private ClipActionsType myActions;

    /** The symbol reference for the current component */
    private SymbolReference myReference;

    /** The component currently being processed */
    private ComponentType myComponent;

    /** Set of all events supported by the current component */
    private Set myEventNames = new HashSet();


    public boolean handleElement(String namespaceURI, String element, String qname,
            AttributeProcessorType atts) throws Exception
    {
		// Create empty actions object to store any event actions
        myActions = myContext.getActionsFactory().newClipActions();

        String componentName = element;
		
		// Get a reference to the symbol and make sure it is actually a component
        myReference = myContext.getSymbolManager().getExistingSymbolReference( componentName );
        if( myReference == null )
        {
            myContext.getLog().error( "Cannot find component:" + componentName );
            return false;
        }
        if( myReference.getSymbol().getSymbolClass() != SymbolClassEnum.SWC_COMPONENT )
        {
            myContext.getLog().error( "Specified symbol is not a component:" + componentName );
            return false;
        }

        myComponent = (ComponentType) myReference.getSymbol();
        setupEvents();

        return true;
    }

    public boolean endElement() throws Exception
    {
		// end element processing
        return true;
    }


    /**
     * Read all the supported events from the component and store them in a
     * local set
     */
    private void setupEvents()
    {
        String[] eventNames = myComponent.getSupportedEvents();
        for (int i = 0; i < eventNames.length; i++)
            myEventNames.add( eventNames[i] );
    }

    /**
     * Overridden method:
     * 
     * @see org.kinesis.xml.handlers.components.EventProcessorType#supportsEvent(java.lang.String)
     * @param eventName
     * @return true if the event is supported by the current component
     */
    public boolean supportsEvent(String eventName)
    {
        return myEventNames.contains( eventName );
    }

    /**
     * Overridden method:
     * 
     * @see org.kinesis.xml.handlers.components.EventProcessorType#addEventScript(java.lang.String,
     *             com.kinesis.log.SourceLocationType, java.lang.String)
     * @param eventName
     * @param location
     * @param script
     * @return true if the script was valid
     */
    public boolean addEventScript(String eventName, SourceLocationType location, String script)
    {
        String trimScript = script.trim();
        if( trimScript.length() == 0 ) return true;

		 // Each event handler script is surounded with the ActionScript to 
		 // add the wrap the event listener code as an anonymous function automatically
        trimScript = "onClipEvent (load) { this.addEventListener(\"" + eventName
                + "\", function( event) {\n" + trimScript + "});}";

        try
        {
            myContext.getActionsFactory().appendActionScript( myActions, location, trimScript,
                    "Component" );
        } catch (Exception e)
        {
            myContext.getLog().error( "Error processing event handler: " + e.getMessage() );
            return false;
        }
        return true;
    }

    /**
     * Reset all the internal fields Overridden method:
     * 
     * @see com.kinesis.util.Resetable#reset()
     */
    public void reset()
    {
        myActions = null;
        myComponent = null;
        myEventNames.clear();
    }
}

10.6.5.3.5 Handle Component Initialization Parameters

Each component element can also have initialization attributes defined. This requires augmenting the handleElement() method extract the component-specific attributes:

   /**
    * Transform to be applied to the current component. Current transforms are
    * limited to x,y translations
    */
   private MatrixType myTransform;
	
   public boolean handleElement(String namespaceURI, String element, String qname,
            AttributeProcessorType atts) throws Exception
    {
		// Other code omitted
        if( !(setupConstructScript( atts ) && setupTransform( atts )) ) return false;
			return false;
        return true;
    }
	
    /**
     * Create a TransformType based on specified x and y attributes
     * 
     * @param atts
     * @return
     */
    private boolean setupTransform(AttributeProcessorType atts)
    {
        float x = atts.getAttrFloat( "x", Float.MIN_VALUE );
        float y = atts.getAttrFloat( "y", Float.MIN_VALUE );
        if( x != Float.MIN_VALUE || y != Float.MIN_VALUE )
                myTransform = new Transform( (x != Float.MIN_VALUE) ? x : 0,
                        (y != Float.MIN_VALUE) ? y : 0, 1, 1, 0, 0 );
        return true;
    }
    /**
     * Create the initial actionscript onConstruct script for initialising the
     * component parameters
     * 
     * @param atts
     */
    private boolean setupConstructScript(AttributeProcessorType atts)
    {
        StringBuffer script = new StringBuffer( " on( construct) {" );
        try
        {

            SWCParameterType[] parameters = myComponent.getSWCParameters();
            for (int i = 0; i < parameters.length; i++)
            {
                String name = parameters[i].getParameterName();
                String value = atts.getAttr( name, null );
                if( value == null )
                {
                    // If it doesn't have a default and no value is specified
                    // then it is ignored
                    if( parameters[i].hasDefaultValue() )
                            script.append( name + " = " + parameters[i].renderDefaultValue() + ";" );
                }
                else
                    script.append( name + " = " + parameters[i].renderUserValue( value ) + ";" );
            }
            script.append( '}' );
            myContext.getActionsFactory().appendActionScript( myActions,
                    myContext.getSourceLocation(), script.toString(), myContext.getDocumentID() );

        } catch (IllegalValueException e)
        {
            myContext.getLog().error(
                    "Error initialising parameters, Reason:" + e.getMessage()
                            + myContext.getLocationString() );
            return false;
        } catch (Exception e)
        {
            myContext.getLog().error( "Error processing Init parameters : " + e.getMessage() );
            return false;
        }
        return true;
    }

This will iterate through the list of all the initialization parameters of the component. Where parameters are defined as attributes, they are added to the initialization script. Where they are not defined but a default value is provided within the SWC, the default value is used instead.

10.6.5.3.6 Adding the Component to the Timeline

This is the final stage of the handler. Once the endElement() method is called we can then add the component to the timeline at the next available depth and add the initialization script to the element:

    public boolean endElement() throws Exception
    {
        // Get the next unoccupied depth from the the timeline resolver
        int depth = myContext.getTimelineResolver().getHighestActiveDepth() + 1;

        PlaceOperation operation = new PlaceOperation( myReference, depth, myTransform, null, /* ColorTransform */
                                                       -1, /* ratio */
                                                       myName, -1, /* Clip depth */
                                                       myActions );
        myFrameHolder.getFrame().addDisplayOperation( operation );

        // Update the timeline resolver to indicate that the component has now
        // been added
        myContext.getTimelineResolver().setSymbolAtDepth( myReference, depth );
        return true;
    }

And that's it! The full source code for this example is available in the installation samples/TagLibraries folder along with a simple Ant build file for creating the handler. This tag library is also precompiled and installed in the installation lib folder.

And finally, how long should does it take to implement a tag library? The tag library for the above example was written in about a day. If you move slowly, absorbing the KineticFusion API and learning how the existing RVML handlers work you should be able to be productive very quickly. The RVML handlers can also be extended simply using this mechanism.