UIX Developer's Guide
Go to Table of Contents
Contents
Go to previous page
Previous
Go to next page
Next

23. Extending UIX

UIX is a powerful framework right out of the box, but UIX cannot provide everything you'll need in your application. At some point, you'll need an extra piece of functionality UIX doesn't provide. Thankfully, UIX is designed to be an extremely extensible framework, and you can plug in your own classes and XML syntax almost anywhere.

You've already seen some ways you can extend UIX:

This chapter shows you how you can go even further. You'll learn how to write custom Renderers that build new user interface components from scratch. You'll also learn how to write XML parsers with the UIX parsing API, not only for user interface components but for all the Java types that UIX uses. Next, you'll learn about an extension of our parsing API that makes it so easy to turn XML into JavaBeans, it's worth using even if you don't use any of the rest of UIX! Then, you'll learn how to wrap up all these pieces into a single UIExtension that you can easily share your work with other developers. Finally, we'll cover the UIX ParserExtension API that lets you add attributes and elements to elements other developers have defined - without changing their code!

This chapter contains the following sections:

For the purposes of the tutorial, we'll focus on the development of a fictitious class library - a UIX extension library, code named "Project Flipper". The Flipper library is intended to be used as a repository of useful beans/Renderers/UIX elements not currently defined by UIX. The process of developing this custom class library is described by a series of steps, which custom component authors can follow when developing their own class libraries. By the end of the tutorial, we have a working class library containing one custom component. This custom component can be used as a custom UINode in a UINode tree, or as a custom tag in an UIX document.

Picking a Namespace

As discussed in Creating Pages in UIX, UIX components are always identified by a namespace. Namespaces are generally defined as URLs, but there's no requirement that any document actually be at that URL. XML chose this approach partly because namespaces need to be unique, and the Internet has already solved the problem of keeping URLs unique.

We'll define a new namespace for the Flipper components. As the namespace is just a project-specific URI, we have chosen "http://flipper.example.org/ui" as our Flipper namespace. We also need to define a "local name" for each new type of UINode defined by the Flipper project. After examining Marlin for missing functionality, we have come up with a variety of possible beans to implement in our Flipper library. For the purposes of this tutorial, we need a bean which would be both simple to implement as well as useful. To that end, we have decided to make the "CopyrightBean" the first addition to the Flipper library. The CopyrightBean serves a significant legal purpose - it renders a Oracle Corporation copyright notice, all rights reserved.

To simplify our Flipper code base, we define a FlipperConstants interface, which defines important constants such as our namespace and local names:

  package org.example.flipper.ui;

  public interface FlipperConstants
  {
    /**
     * Namespace used by the Flipper implementation
     */
    public static final String FLIPPER_NAMESPACE = 
      "http://flipper.example.org/ui";
  
    /**
     * Name of our copyright bean
     */
    public static final String COPYRIGHT_NAME = "copyright";
  }

Writing Renderers

A Renderer is the interface UIX uses to turn a UINode into output. It's the brains behind all of your HTML, but it's about as simple as an API could be:

  public interface Renderer
  {
    public void render(
    RenderingContext context,
    UINode           node) throws IOException;
  }

A Renderer is responsible for generating the results not only for your node, but also for all of the children of your node. (For our CopyrightBean, we don't have any children.) Here's the full Renderer:

  package org.example.flipper.ui;

  import java.io.IOException;
  
  import oracle.cabo.ui.BaseRenderer;
  import oracle.cabo.ui.RenderingContext;
  import oracle.cabo.ui.UINode;
  import oracle.cabo.ui.io.OutputMethod;
  
  public class CopyrightRenderer extends BaseRenderer
  {
    protected void renderContent(
      RenderingContext context,
      UINode           node
      ) throws IOException
    {
      OutputMethod out = context.getOutputMethod();
      out.startElement("span");
      out.writeAttribute("class", "OraCopyright");
      // "a9" is the Unicode copyright symbol.
      out.writeText("Copyright \u00a9 Oracle Corporation.  All Rights Reserved.");
      out.endElement("span");
    }
  }

This will output the following snippet of HTML:

 <span class="OraCopyright">
    Copyright &copy; Oracle Corporation.  All Rights Reserved.
 </span>

There's not much code here, but it does show off one important technique in writing a Renderer: the OutputMethod interface. An OutputMethod abstracts away markup languages, and saves you from work needed to keep your output properly escaped. By using an OutputMethod instead of a PrintWriter, we can automatically:

We'll note one more thing about this example before moving on and adding features to our Renderer. We never actually told the OutputMethod that we were done with the start tag of <span>: we started the element, wrote one attribute, and then just wrote the text that's inside of the element. You don't need to when you're working with OutputMethods, because the implementations know when to close elements automatically.

You can, if you want, bypass all of these methods and directly write out HTML. OutputMethod includes a writeRawText() method that stays out of your way. We recommend staying away from raw text, because you'll lose all the advantages of OutputMethods.

Now, let's add a couple of attributes to our copyright bean. We'll add an integer "year" attribute that identifies the year of our copyright, and we'll also add a "destination" attribute that provides a link. In UIX, we store and retrieve node attributes by AttributeKey, so we should define constants for these attributes. UIX has a built-in DESTINATION_ATTR constant in the oracle.cabo.ui.UIConstants interface, but let's add a YEAR_ATTR constant to FlipperConstants:

  package org.example.flipper.ui;
 
  import oracle.cabo.ui.AttributeKey;

  public interface FlipperConstants
  {
    /**
     * Namespace used by the Flipper implementation
     */
    public static final String FLIPPER_NAMESPACE = 
      "http://flipper.example.org/ui";
  
    /**
     * Name of our copyright bean
     */
    public static final String COPYRIGHT_NAME = "copyright";

    
    /**
     * "Year" attribute key.
     */
    public static final AttributeKey YEAR_ATTR = 
      AttributeKey.getAttributeKey("year");
    
  }

Now, let's add code to our Renderer to get these attributes into our output:

  package org.example.flipper.ui;

  import java.io.IOException;
  
  import oracle.cabo.ui.BaseRenderer;
  import oracle.cabo.ui.RenderingContext;
  import oracle.cabo.ui.UINode;
  import oracle.cabo.ui.io.OutputMethod;
  
  public class CopyrightRenderer extends BaseRenderer
  {
    protected void renderContent(
      RenderingContext context,
      UINode           node
      ) throws IOException
    {
      OutputMethod out = context.getOutputMethod();
      
      Object year = node.getAttributeValue(context,
                                           FlipperConstants.YEAR_ATTR);
      Object destination = node.getAttributeValue(context,
                                                  UIConstants.DESTINATION_ATTR);
      if (destination != null)
      {
        out.startElement("a");
        // URLs should be written out using writeURIAttribute(), because
        // they're escaped differently than other attributes.
        out.writeURIAttribute("href", destination);
      }
      
      out.startElement("span");
      out.writeAttribute("class", "OraCopyright");
      out.writeText("Copyright (c) ");
      
      if (year != null)
        out.writeText(year.toString());
      
      out.writeText(" Oracle Corporation.  All Rights Reserved.");
      out.endElement("span");
      
      if (destination != null)
        out.endElement("a");

    }
  }

This is all pretty simple, but there's not much you can do with just a Renderer. For starters, we should write a CopyrightBean class.

Writing Your Bean Class

Once you've written all of your Renderer code, the bean is just a lot of cookie-cutter style code that sets up your component's namespace and local name and gets and sets attributes:

  package org.example.flipper.ui;

  import oracle.bali.share.util.IntegerUtils;
  import oracle.cabo.ui.UIConstants;
  import oracle.cabo.ui.beans.BaseWebBean;

  public class CopyrightBean extends BaseWebBean implements FlipperConstants
  {
    public CopyrightBean()
    {
      super(FLIPPER_NAMESPACE, COPYRIGHT_NAME, null);
    }

    final public int getYear()
    {
      return BaseWebBean.resolveInteger(
        (Integer) getAttributeValue(YEAR_ATTR));
    }
   
    final public void setYear(int year)
    {
      setAttributeValue(YEAR_ATTR, IntegerUtils.getInteger(year));
    }

    final public String getDestination()
    {
      return (String) getAttributeValue(UIConstants.DESTINATION_ATTR);
    }

    final public void setDestination(String destination)
    {
      setAttributeValue(UIConstants.DESTINATION_ATTR, destination);
    }
  }

There's a few worthwhile things to notice about this code:

Now, we can at last create a CopyrightBean:

  CopyrightBean copyright = new CopyrightBean();
  copyright.setYear(2001);
  copyright.setDestination("http://www.oracle.com");

It's good to remember that CopyrightBean really is just a convenience class, and we could easily have written that last snippet of code just using BaseMutableUINode:

  BaseMutableUINode copyright =
    new BaseMutableUINode(FlipperConstants.FLIPPER_NAMESPACE,
                          FlipperConstants.COPYRIGHT_NAME);
  copyright.setAttributeValue(FlipperConstants.YEAR_ATTR, new Integer(2001));
  copyright.setAttributeValue(UIConstants.DESTINATION_ATTR, "http://www.oracle.com");

If you try compiling all this code as written and trying to render these beans, you won't get much output, but you will get an message sent to the error log, which should look something like:

No UIX Components (Marlin) RendererFactory registered for namespace 
http://flipper.example.org/ui

We're still one step short of getting our bean working. Before we can render this bean, we've got to hook our Renderer into the UIX system, and for that we'll have to learn about the RendererFactory and UIExtension classes.

Registering Renderers: RendererFactory and UIExtension

To register a renderer, you need to take care of the following steps:

  1. Create a RendererFactory.
  2. Create a UIExtension that registers the factory.
  3. Register the UIExtension

UIX finds Renderers by looking for them inside of a RendererManager. If a RendererManager had to handle all the Renderers for all namespaces on its own, it'd get very messy. Instead, a RendererManager divides up its Renderers by namespace, and uses one RendererFactory for each namespace.

So, for our first step, let's create that RendererFactory. We'll use the UIX RendererFactoryImpl to make it easier:

package org.example.flipper.ui;

import oracle.cabo.ui.RendererFactory;
import oracle.cabo.ui.RendererFactoryImpl;

public class FlipperRendererFactory extends RendererFactoryImpl
{
  /**
   * Return the shared instance of this factory.
   */
  static public RendererFactory sharedInstance()
  {
    return _sInstance;
  }

  public FlipperRendererFactory()
  {
    // Register our one renderer.
    registerRenderer(FlipperConstants.COPYRIGHT_NAME, 
                     "org.example.flipper.ui.CopyrightRenderer");    
  }

  static private final RendererFactory _sInstance = 
    new FlipperRendererFactory();
}

Note that we register the renderer by name instead of with the class itself or an instance of our renderer class. UIX won't load the CopyrightRenderer class until it's actually needed.

Now that we've created the RendererFactory, we can move onto step two and register the factory. For that, we'll use the UIExtension interface. A UIExtension always has two methods. One is used to register rendering code, and the other registers parsing code:

package oracle.cabo.ui;
public interface UIExtension
{
  public void registerSelf(LookAndFeel laf);
  public void registerSelf(ParserManager manager);
}

For now, let's just register our renderers:

package org.example.flipper.ui;

import oracle.cabo.ui.RendererFactory;
import oracle.cabo.ui.UIExtension;
import oracle.cabo.ui.laf.LookAndFeel;
import oracle.cabo.share.xml.ParserManager;

public class FlipperUIExtension implements UIExtension
{
  public FlipperUIExtension()
  {
  }
  
  public void registerSelf(LookAndFeel laf)
  {
    // Get the RendererFactory
    RendererFactory factory = FlipperRendererFactory.sharedInstance();

    // And register it on this look-and-feel.
    laf.getRendererManager().registerFactory(FlipperConstants.FLIPPER_NAMESPACE,
                                             factory);
  }
  
  public void registerSelf(ParserManager manager)
  {
    // For now, let's do nothing.
  }
}

Another simple bit of code in yet another class. This is a lot of classes for one little bean, but each subsequent bean you write can reuse these same RendererFactory and UIExtension classes.

Now, step three: registering the UIExtension. You'll register it differently depending on whether you're using the UIX Controller or writing to UIX Components directly.

Registering a UIExtension on the UIX Controller

The UIX Controller makes it very easy to register UIExtensions. They can be registered either programmatically or declaratively with uix-config.xml. To register extensions programatically, use the registerUIExtension() method, defined on the BaseUIPageBroker class. For example, you can subclass UIXPageBroker:

import oracle.cabo.servlet.xml.UIXPageBroker;

public class FlipperPageBroker extends UIXPageBroker
{
  public FlipperPageBroker()
  {
    registerUIExtension(new FlipperUIExtension());
  }
}

But it's even easier to register UIExtensions by using the <extension-class> element in uix-config.xml. If you're using the Oracle Containers For J2EE (OC4J) servlet engine (or any other engine that implements the Servlet 2.2 specification), these go in your <web app>/WEB-INF/uix-config.xml file:


 <?xml version="1.0" encoding="ISO-8859-1"?> 
 <configurations xmlns="http://xmlns.oracle.com/uix/config">
   <application-configuration>
     <ui-extensions>
       <extension-class>org.example.flipper.FlipperUIExtension</extension-class>
       <extension-class>org.example.someOtherPackage.AnotherUIExtension</extension-class>
     </ui-extensions>
   </application-configuration>
 </configurations>

For more information on uix-config.xml, see the Configuration chapter.

Both of these methods of registering UIExtensions work only if you're using BaseUIPageBroker or one of its subclasses - like UIXPageBroker. If you're not using either of these classes, you'll have to register the UIExtension directly.

Registering a UIExtension directly

If the UIX Controller isn't helping you out, you'll need to register the UIExtension yourself onto a LookAndFeelManager. A LookAndFeelManager is the entity that controls all LookAndFeels. In particular, it knows what LookAndFeel should be used for any page.

The easiest way to register a UIExtension is to register it on the default LookAndFeelManager:

import oracle.cabo.ui.laf.LookAndFeelManager;

...

  LookAndFeelManager manager = 
    LookAndFeelManager.getDefaultLookAndFeelManager();

  manager.registerUIExtension(new FlipperUIExtension());

This should happen only once, and before you render your first page. Your servlet's init() method is one good place to put code like this.

The problem with registering the FlipperUIExtension on the default LookAndFeelManager is that this LookAndFeelManager is potentially shared by every web application on your server. Not all of these web applications will necessarily want Flipper. There's another problem: if two web applications both want to use Flipper, it will get registered twice! This becomes wasteful and slow as you add more and more applications.

Instead, you should create your own private LookAndFeelManager using createDefaultLookAndFeelManager(). Then, you'll use the Configuration API to store this LookAndFeelManager:

   // Instead of using the default, create a brand new manager
   LookAndFeelManager manager = 
     LookAndFeelManager.createDefaultLookAndFeelManager();

   // Register the extension just as before
   manager.registerUIExtension(new FlipperUIExtension());

   // And now store the LookAndFeelManager on a Configuration
   ConfigurationImpl config = new ConfigurationImpl("yourConfigKey");
   config.putProperty(Configuration.LOOK_AND_FEEL_MANAGER, manager);
   config.register();

For more information on using the Configuration API and how to use a particular Configuration object when rendering, see the Configuration chapter.

Supporting Your Renderer in XML

Now that we've got our bean up and running from Java, the next step is to add support for your bean to our XML parsing API.

The UIX parsing API is strongly analogous to our rendering API. Instead of RendererManager, RendererFactory, and Renderer, we use ParserManager, ParserFactory, and NodeParser. We'll talk more about these APIs later, but as long as we're parsing UINodes, we can stick to a much simpler API.

For parsing UINodes, you should always use the UINodeParserFactory class. Because nearly all UINodes are parsed in the same way, you won't need to write a new parser for each bean. You do need to provide UINodeParserFactory with metadata that describes your bean. In particular, it needs to know what attributes the bean supports, it needs to know the type of each attribute, and it needs to know what "named children" are supported by the bean.

A UINode's metadata is described by a UINodeType object. You can control many things with a UINodeType, but most developers should just create instances of the BaseUINodeType class, put them in a Dictionary, and hand that dictionary to a UINodeParserFactory. Here's code to create the ParserFactory for our Project Flipper <copyright> element:

package org.example.flipper.ui;

import oracle.bali.share.collection.ArrayMap;
import oracle.cabo.share.xml.ParserFactory;
import oracle.cabo.share.xml.ParserManager;
import oracle.cabo.ui.xml.parse.UINodeParserFactory;
import oracle.cabo.ui.xml.parse.BaseUINodeType;

class FlipperUINodeParserFactory extends UINodeParserFactory
{
  public FlipperUINodeParserFactory()
  {
    super(FlipperConstants.FLIPPER_NAMESPACE,
          null,
          _sFlipperTypes);
  }

  static private final ArrayMap _sFlipperTypes = new ArrayMap();
  static
  {
    BaseUINodeType copyrightType =
      new BaseUINodeType(FlipperConstants.FLIPPER_NAMESPACE,
                         null,
                         new Object[]{"year", Integer.class,
                                      "destination", String.class},
                         BaseUINodeType.getDefaultUINodeType());
    _sFlipperTypes.put(FlipperConstants.COPYRIGHT_NAME, copyrightType);
  }
}

Let's walk through this code:

Now, we just need to modify our UIExtension to register the parsing code in addition to our renderer:

package org.example.flipper.ui;

import oracle.cabo.ui.RendererFactory;
import oracle.cabo.ui.UIExtension;
import oracle.cabo.ui.laf.LookAndFeel;
import oracle.cabo.share.xml.ParserManager;

public class FlipperUIExtension implements UIExtension
{
  public FlipperUIExtension()
  {
  }

  public void registerSelf(LookAndFeel laf)
  {
    // Get the RendererFactory
    RendererFactory factory = FlipperRendererFactory.sharedInstance();

    // And register it on this look-and-feel.
    laf.getRendererManager().registerFactory(FlipperConstants.FLIPPER_NAMESPACE,
                                             factory);
  }

  public void registerSelf(ParserManager manager)
  {
    FlipperUINodeParserFactory factory = new FlipperUINodeParserFactory();
    factory.registerSelf(manager);
  }
}

Not much has changed: we create the parser factory and register it on the ParserManager with the UINodeParserFactory.registerSelf() method. If you're using the UIX Controller and its UIXPageBroker class, your parsers will automatically be registered. Otherwise, if you're parsing UIX files directly, you will need to call the UIExtension.registerSelf(ParserManager) method on the ParserManager you're using.

Now, you can use the <flipper:copyright> element right in UIX:

 <pageLayout xmlns="http://xmlns.oracle.com/uix/ui" 
              xmlns:flipper="http://flipper.example.org/ui">
   <copyright>
     <flipper:copyright year="2001" destination="http://www.oracle.com"/>
   </copyright>
 </pageLayout>

Attributes versus Named Children

It's important to understand exactly what's meant by "named children". UINodes have a special getNamedChild() method that returns UINodes. Usually, beans that support a named child will have get and set methods for that named child:

   public UINode getStart() { ... }
   public void setStart(UINode start) { ... }

Those methods look a lot like the methods used to get and set attributes, for example:

   public String getWidth() { ... }
   public void setWidth(String width) { ... }
   public ClientValidater getOnSubmitValidater() { ... }
   public void setOnSubmitValidater(ClientValideter onSubmit) { ... }

For someone using the Java API, these are all pretty similar. They're not identical - you can databind attributes but not named children (though IncludeBean helps simulate databinding of named children). But by and large, they look the same.

In XML, it gets more confusing. Some attributes, like "width" in the previous example, are what UIX terms "simple" attributes. These attributes can easily be described with a single string. This includes types like strings, numbers, and true/false. Other attributes, like "onSubmitValidater", are what we term "complex" attributes. They require a lot more syntax than just a string to describe their value, and we represent them using two levels of child elements. The first and top level is an "envelope element" with the same name as the attribute. This envelope element identifies to UIX which attribute is being parsed. Then, inside of that element, we look for an element that defines the value of the type:

 <yourElement> 
   <!-- First, an envelope element identifying the attribute name -->
   <onSubmitValidater>
     <!-- Then, an element describing the value;  that is, what
              kind of ClientValidater this is -->
     <ui:decimal/>
   </onSubmitValidater>
 </yourElement>

Later in this chapter, you'll learn how to define elements that describe ClientValidaters or any other type of Java object, including custom types that aren't built into UIX.

Named children have syntax just like complex attributes. They also have an envelope element, though its name is the name of the child. Inside of the envelope element is a single element that defines the UINode.

 <yourElement> 
   <!-- First, an envelope element identifying the child name -->
   <start>
     <!-- Then, an element describing the UINode -->
     <ui:button text="Press Me"/>
   </start>
 </yourElement>

So, from an XML standpoint, complex attributes are a lot like named children! But they are defined differently in your UINodeType definition. Named children are defined in the named children list, and complex attributes are defined in the attributes list:

   UINodeType yourElementType =
     new BaseUINodeType(YOUR_NAMESPACE,
                        new String[]{"start"},
                        new Object[]{"onSubmitValidater", ClientValidater.class,
                                     "width", String.class},
                        BaseUINodeType.getDefaultUINodeType());

This is complicated, but few people need to know this. If you're simply using uiXML, you don't need to know about any of this. You'll simply see that there's a "width" attribute and both <start> and <onSubmitValidater> elements. But if you're adding beans to UIX, you'll need to understand this so that you can describe your own XML syntax.

Writing Parsers for UIX

If the only elements you're adding to UIX are new UINode types, you've learned all you need. But UIX is a lot more extensible than that. Let's show how by adding some more functionality to our CopyrightBean. Instead of supporting just a single year, let's support a range of years with a new "complex" attribute of a custom type:

public class CopyrightBean()
{
  // ...

  public YearRange getYears()
  {
    return (YearRange) getAttributeValue(YEARS_ATTR);
  }

  public void setYears(YearRange years)
  {
    setAttributeValue(YEARS_ATTR, years);
  }
  
  static public class YearRange
  {
    public int getStart()
    {
      return _start;
    }

    public void setStart(int start)
    {
      _start = start;
    }

    public int getEnd()
    {
      return _end;
    }

    public void setEnd(int end)
    {
      _end = end;
    }

    private int _start = -1;
    private int _end = -1;
  }
}

We'd like to support the following bit of XML:

 <flipper:copyright>
   <flipper:years>
     <flipper:yearRange start="1999" end="2001"/>
   </flipper:years>
 </flipper:copyright>

(The changes to the Renderer are left as an exercise for the reader.) Our parsing code doesn't know anything about how to build one of these YearRange objects. To create a YearRange object from XML, you'll have to code with our NodeParser API.

The NodeParser API

The basic principle of the NodeParser is to take an element, its attributes, and its child elements and turn all of them into a single Java object. It's an event driven parser based on the SAX 2.0 standard (see http://www.saxproject.org/), but what makes it so much more powerful than SAX alone is that multiple node parsers can collaborate to build a single object tree. Parsing functionality is neatly factored into small, targeted classes, and parsing logic can easily be extended without pervasive code changes.

Each NodeParser is responsible for turning a subtree of your XML document into a single Java object. But it can in turn ask another NodeParser to handle a smaller subtree to create an object that it needs. This approach gives you very modular code, and it also produces extremely extensible code because of how NodeParsers are found and created.

A ParserFactory is registered not only by namespace, but also by the type of Java object it produces. So, UIX registers multiple ParserFactories in its UI namespace: one for creating UINodes, another for creating BoundValues, one more for ClientValidaters, and so forth. When we registered the FlipperUINodeParserFactory in our FlipperUIExtension, we only registered a factory for creating UINodes in the Flipper namespace.

Whenever a NodeParser decides it wants an element to be parsed into a BoundValue, for example, it asks the ParseContext for a NodeParser that can create BoundValues, but it passes in the namespace and local name of the element:

   NodeParser parser = context.getParser(BoundValue.class,
                                         namespaceOfChild,
                                         localNameOfChild);

The type (BoundValue.class) and namespace identify a ParserFactory, and the ParserFactory uses the local name to create a NodeParser. The caller doesn't need to know anything about the namespace of the element, or communicate with that code in any way. As long as the node parser it gets will correctly create a BoundValue, it doesn't have to care about the structure or attributes of that element.

This means that any UIX developer can create their own BoundValue elements, and those elements will be accepted anywhere we accept a built-in BoundValue element (like<fixed> or <concat>). It also means that even though our UINode parsing code doesn't know a thing about YearRange objects, it can successfully create YearRange objects and set the "years" attribute correctly.

The LeafNodeParser API

One of the simplest ways to write a NodeParser is to subclass the LeafNodeParser class. This class simplifies writing parsers for "leaf" elements - elements that have attributes, but no child elements or plain text. Subclasses only need to override one method:

  abstract protected Object getNodeValue(
    ParseContext context,
    String       namespaceURI,
    String       localName,
    Attributes   attrs)  throws SAXParseException;

This method takes the ParseContext, which provides parse-time context to your code, and also takes the namespace and local name of the element. Finally, it takes the attribute list which names all the attributes of the XML element.

package org.example.flipper.ui;

import org.xml.sax.Attributes;
import org.xml.sax.SAXParseException;

import oracle.cabo.share.xml.LeafNodeParser;
import oracle.cabo.share.xml.ParseContext;

class RangeNodeParser extends LeafNodeParser
{
  protected Object getNodeValue(
    ParseContext context,
    String       namespaceURI,
    String       localName,
    Attributes   attrs)
  {
    CopyrightBean.YearRange range = new CopyrightBean.YearRange();

    String startString = attrs.getValue("start");
    if (startString != null)
    {
      try
      {
        int start = Integer.parseInt(startString);
        range.setStart(start);
      }
      catch (NumberFormatException nfe)
      {
        logWarning(context, "\"start\" attribute could not be parsed.");
      }
    }

    String endString = attrs.getValue("end");
    if (endString != null)
    {
      try
      {
        int end = Integer.parseInt(endString);
        range.setEnd(end);
      }
      catch (NumberFormatException nfe)
      {
        logWarning(context, "\"end\" attribute could not be parsed.");
      }
    }

    return range;
  }
}

This is yet another fairly simple class. We get two attributes, "start" and "end". If either is set, we parse into an int, set it on the YearRange object. Finally, we return the YearRange.

Note that when parsing the strings fail, we've logged warnings instead of throwing a SAXParseException (in fact, we've removed the "throws SAXParseException" declaration altogether). It's generally better to log the errors, because this lets users see all the errors in a page at once. Throwing an exception terminates parsing immediately, so the user only sees the first error. Also, the logWarning() method automatically adds row and column numbers to your message, so the user gets plenty of context. There's few things more annoying to users about a parser than error messages with no context!

Now, we need to write a ParserFactory to create this NodeParser. Remember that we'd decided that we wanted the name of this element to be "yearRange":

package org.example.flipper.ui;

import oracle.cabo.share.xml.NodeParser;
import oracle.cabo.share.xml.ParseContext;
import oracle.cabo.share.xml.ParserFactory;

class RangeParserFactory implements ParserFactory
{
  public NodeParser getParser(
    ParseContext context,
    String       namespaceURI,
    String       localName)
  {
    if ("yearRange".equals(localName))
      return new RangeNodeParser();

    return null;
  }
}

Yet another simple class. Even in these few lines, there are a few things worth noting. First, we don't check the namespace at all. You might assume that this factory should verify not only the element name, but also the namespace. However, factories are already registered by namespace, so this would be an unnecessary check and a waste of time. Second, we also return a new RangeNodeParser. NodeParser instances carry state and generally cannot be shared or reused. Thankfully, they're also extremely lightweight. Finally, if it's an element name we don't know about, we just return "null". The core parsing code will automatically report errors for unknown elements, so there's no need to duplicate that reporting code here.

We need to tweak our UINodeType definitions to report this extra attribute:

class FlipperUINodeParserFactory extends UINodeParserFactory
{
  // Skipping down to the bit that changed...

    BaseUINodeType copyrightType =
      new BaseUINodeType(FlipperConstants.FLIPPER_NAMESPACE,
                         null,
                         new Object[]{"year", Integer.class,
                                      "destination", String.class,
                                      "years", CopyrightBean.YearRange.class},
                         BaseUINodeType.getDefaultUINodeType());

  // ...
}

And, finally, we have to register this ParserFactory inside our UIExtension:

package org.example.flipper.ui;

import oracle.cabo.ui.RendererFactory;
import oracle.cabo.ui.UIExtension;
import oracle.cabo.ui.laf.LookAndFeel;
import oracle.cabo.share.xml.ParserManager;

public class FlipperUIExtension implements UIExtension
{
  public void registerSelf(LookAndFeel laf)
  {
    // Get the RendererFactory
    RendererFactory factory = FlipperRendererFactory.sharedInstance();

    // And register it on this look-and-feel.
    laf.getRendererManager().registerFactory(FlipperConstants.FLIPPER_NAMESPACE,
                                             factory);
  }

  public void registerSelf(ParserManager manager)
  {
    FlipperUINodeParserFactory factory = new FlipperUINodeParserFactory();
    factory.registerSelf(manager);
    RangeParserFactory rangeFactory = new RangeParserFactory();
    manager.registerFactory(CopyrightBean.YearRange.class,
                            FlipperConstants.FLIPPER_NAMESPACE,
                            rangeFactory);
  }
}

Now, anytime the parsing code wants a YearRange object, and the current element is in the Flipper namespace, the RangeParserFactory will be asked to produce a parser.

The BeanParser and BeanDef API

The RangeNodeParser class wasn't that hard to write. But every time you add a new property to the YearRange class, you have to modify the parser class. And every time you add a new class to the system, you have to code a new parser from scratch. It's not especially difficult, but it's really tedious. There is a better way.

That better way is the oracle.cabo.share.xml.beans.BeanParser API. With BeanParser, UIX can automatically examine a class's code, identify attributes, and figure out how to parse XML into instances of that class. Like UINodeParser, it relies on a separate piece of metadata to supply it with type information about the class. That metadata is supplied by the BeanDef API. BeanDef is a purely abstract class without any implementation, and it's unlikely that you'll need to use it directly. Instead, many developers will use our prewritten implementation, IntrospectionBeanDef. This class is the brains of the BeanParser API, as it automatically scans your beans to locate attributes.

Let's rewrite our ParserFactory to use the BeanParser API:

package org.example.flipper.ui;

import oracle.cabo.share.xml.NodeParser;
import oracle.cabo.share.xml.ParseContext;
import oracle.cabo.share.xml.ParserFactory;

import oracle.cabo.share.xml.beans.BeanParser;
import oracle.cabo.share.xml.beans.IntrospectionBeanDef;

class RangeParserFactory implements ParserFactory
{
  public NodeParser getParser(
    ParseContext context,
    String       namespaceURI,
    String       localName)
  {
    if ("yearRange".equals(localName))
      return new BeanParser(_sYearRangeDef);

    return null;
  }
  
  static private final IntrospectionBeanDef _sYearRangeDef = 
    new IntrospectionBeanDef(CopyrightBean.YearRange.class.getName());
}

That's it. You can throw away the node parser class! Instead of using our handwritten parser, we use a BeanParser. We supply it with an IntrospectionBeanDef that points at the YearRange class. Note that we cache and reuse our BeanDef instance. This is important when using BeanParser because the IntrospectionBeanDefs are expensive to recreate.

The parsing functionality you get is even better than it was before. For example, the hand-coded parser didn't notice if you set an attribute that it didn't know. If a user misspelled "start" or "end", the parser would silently ignore the error, making it very difficult to track down the problem. BeanParser automatically detects these mistakes and warns the user.

BeanParser also supports more than just simple attributes. BeanParser includes built-in support for dealing with "complex" attributes. BeanParser will automatically identify envelope elements and parse the child elements. BeanParser even includes support for arrays of child elements. So, for example, take a bean with the following methods:

public class YourBean
{
  public void setValidaters(ClientValidater[] validaters) { ... }
  public ClientValidater[] getValidaters() { ... }
}

BeanParser will automatically support the following XML:

 <yourElement>
   <validaters>
     <ui:date/>
     <ui:decimal/>
     <ui:date timeStyle="short"/>
     <ui:regExp/>
     <oneOfYourClientValidaters/>
   </validaters>
 </yourElement>

Because BeanParsers are simply a kind of NodeParser, they can collaborate seamlessly with hand-coded node parsers and vice versa. This API makes it so easy to parse complex Java objects that it's worth considering any time you need to parse XML into Java objects, even if those objects or your project as a whole has nothing to do with the rest of UIX.

UIBeanDef

We've got <yearRange> elements parsing correctly, but we don't support databinding inside <yearRange>. It would be a big win to support syntax like:

 <flipper:copyright>
   <flipper:years>
     <flipper:yearRange start="1999" data:end="today@yearSource"/>
   </flipper:years>
 </flipper:copyright>

Or even:

 <flipper:copyright>
   <flipper:years>
     <flipper:yearRange start="1999">
       <ui:boundAttribute name="end">
         <!-- Some really complex set of bound values -->
       </ui:boundAttribute>
      </flipper:yearRange>
   </flipper:years>
 </flipper:copyright>

If we were still using LeafNodeParser, this would entail a lot of work in the parsing code. With BeanParser, it's trivial! First, we do have to fix up our YearRange class to make the "end" attribute support BoundValues:

  static public class YearRange
  {
    public int getStart()
    {
      return _start;
    }

    public void setStart(int start)
    {
      _start = start;
    }

    public int getEnd()
    {
      return _end;
    }

    public void setEnd(int end)
    {
      _end = end;
    }

    public void setEndBinding(BoundValue endBinding)
    {
      _endBinding = endBinding;
    }

    public int getEnd(RenderingContext context)
    {
      if (_endBinding != null)
      {
        Object o = _endBinding.getValue(context);
        if (o instanceof Integer)
          return ((Integer) o).intValue();
        return -1;
      }

      return _end;
    }

    private BoundValue _endBinding;
    private int _start = -1;
    private int _end = -1;
  }

We added two methods. The only one the parser will care about is the setEndBinding() method. It's important that this method have the same name as the property we're databinding plus the string "Binding", and that it take a BoundValue. The second method is the method we'll use from our Renderer to get the "end" attribute.

Now, we'll change our parsing code:

import oracle.cabo.share.xml.NodeParser;
import oracle.cabo.share.xml.ParseContext;
import oracle.cabo.share.xml.ParserFactory;

import oracle.cabo.share.xml.beans.BeanParser;
import oracle.cabo.ui.xml.parse.UIBeanDef;

public class RangeParserFactory
{
  public NodeParser getParser(
    ParseContext context,
    String       namespaceURI,
    String       localName)
  {
    if ("yearRange".equals(localName))
      return new BeanParser(_sYearRangeDef);

    return null;
  }

  static private final UIBeanDef _sYearRangeDef = 
    new UIBeanDef(CopyrightBean.YearRange.class.getName());
}

If we didn't emphasize it, you might miss the change. Instead of using IntrospectionBeanDef, we use UIBeanDef. That's it! We now have full support for "data:" syntax for simple databinding, as well as <ui:boundAttribute> syntax for complex databinding. Both of these syntaxes will support automatic defaulting back to any explicitly set value, for example:

 <flipper:copyright>
   <flipper:years>
     <flipper:yearRange start="1999" end="2001"
                                      data:end="today@yearSource"/>
   </flipper:years>
 </flipper:copyright>

And just as we see warnings when an unknown attribute is set, you'll get warnings anytime a developer tries to databind a property that doesn't support databinding. For example, we didn't add a setStartBinding() method to YearRange. If a developer tries to add a "data:start" attribute, he'll get a warning that will let him know the attribute can't be databound.

NodeParser: The Details

Occasionally, you may find that none of our pre-existing parser implementations - LeafNodeParser, BeanParser, or UINodeParser - quite do the trick. If you get to this point, you'll need to learn about the NodeParser API in detail. We strongly recommend subclassing from BaseNodeParser, which gives you a number of utility methods for logging warnings, getting required attributes, etc., but you'll still have to know how NodeParsers work.

The UIX parsing API iterates through a tree of XML elements as follows:

  1. A NodeParser is retrieved for the root XML node using the ParserFactory responsible for creating UINodes (or whatever type of object the calling code asked for).
  2. NodeParser.startElement() is called to handle the attributes of that top-level element.
  3. For each child element, we call NodeParser.startChildElement(). This method must identify the NodeParser needed to process the child. It can handle the call in one of a few ways.
  4. When the child element finishes, one of two methods is called:
  5. For each sequence of text inside the element, NodeParser.addText() is called once. (These calls are interspersed between calls to start/endChildElement(), in the expected document order.) As with the SAX API, what appears to be a single run of text in the XML document may result in multiple calls to NodeParser.addText(), so developers must accumulate the text until one of NodeParser.endElement(), NodeParser.endChildElement(), or NodeParser.startChildElement() is called.
  6. Spans of white space will be delivered to NodeParser.addWhitespace in a manner identical to NodeParser.addText(). Most parsers will ignore this method, but parsers that care about all white space should call NodeParser.addText() with the arguments passed to addWhitespace().
  7. Finally, NodeParser.endElement() is called. This method must return an Object representing the fully constructed object.
  8. Repeat the process for each subsequent element.

The ParserExtension API

You've already seen that UIX is extensible. It's easy to add new elements for preexisting types and to add new types to the system. But there's one additional form of extensibility that we haven't covered yet. The ParserExtension API lets you add attributes and even child elements to components another developer has already written!

To demonstrate, let's add support for a "flipper:destination" attribute on all link beans, button beans, etc., that points to various web sites for the Flipper Project. We'll accept two values. If "flipper:destination" is set to "internal", the destination should be "http://flipper.example.org". If it's set to "external", the destination should be "http://www.example.org/flipper". So, for example, a developer could write:

 <link text="Go to the Flipper Home" flipper:destination="internal">

A ParserExtension works by noticing attributes and elements that haven't been handled by the default parsing code and gathering their values in a Dictionary. When the parent element is completed (i.e. NodeParser.endElement() has been called) and has returned a Java object, the ParserExtension gets that Java object and its Dictionary of values. It's free to modify the object or even replace it altogether! Here's the code for our ParserExtension:

package org.example.flipper.ui;

import java.util.Dictionary;

import oracle.cabo.share.xml.BaseParserExtension;
import oracle.cabo.share.xml.ParseContext;

import oracle.cabo.ui.MutableUINode;
import oracle.cabo.ui.UIConstants;

public class FlipperParserExtension extends BaseParserExtension
{
  public Object elementEnded(
    ParseContext context,
    String       namespaceURI,
    String       localName,
    Object       parsed,
    Dictionary   attributes)
  {
    if (parsed instanceof MutableUINode)
    {
      
      _extendUINode(context, (MutableUINode) parsed, attributes);
    }
    else
    {
      logWarning(context,
                 "Controller extensions not supported on " +
                   parsed + " objects.");
    }

    return parsed;
  }

  private void _extendUINode(
    ParseContext  context,
    MutableUINode node,
    Dictionary    attributes)
  {
    Object destination = attributes.get("destination");
    if (destination != null)
    {
      if ("external".equals(destination))
        node.setAttributeValue(UIConstants.DESTINATION_ATTR,
                               "http://www.example.org/flipper");
      else if ("internal".equals(destination))
        node.setAttributeValue(UIConstants.DESTINATION_ATTR,
                               "http://flipper.example.org/");
    }
  }
}

Let's walk through this code.

ParserExtensions are registered on ParserManagers, so we'll have to modify our UIExtension again:

package org.example.flipper.ui;

import oracle.cabo.ui.RendererFactory;
import oracle.cabo.ui.UIExtension;
import oracle.cabo.ui.laf.LookAndFeel;
import oracle.cabo.share.xml.ParserManager;

public class FlipperUIExtension implements UIExtension
{
  public void registerSelf(LookAndFeel laf)
  {
    // Get the RendererFactory
    RendererFactory factory = FlipperRendererFactory.sharedInstance();

    // And register it on this look-and-feel.
    laf.getRendererManager().registerFactory(FlipperConstants.FLIPPER_NAMESPACE,
                                             factory);
  }

  public void registerSelf(ParserManager manager)
  {
    FlipperUINodeParserFactory factory = new FlipperUINodeParserFactory();
    factory.registerSelf(manager);
    RangeParserFactory rangeFactory = new RangeParserFactory();
    manager.registerFactory(CopyrightBean.YearRange.class,
                            FlipperConstants.FLIPPER_NAMESPACE,
                            rangeFactory);
    FlipperParserExtension extension = new FlipperParserExtension();
    manager.registerExtension(FlipperConstants.FLIPPER_NAMESPACE,
                              extension);
  }
}

ParserExtension can also support child elements. For example, the current UIX-BC4J integration API supports syntax like the following:

<page xmlns="http://xmlns.oracle.com/uix/controller"
      xmlns:bc4j="http://xmlns.oracle.com/uix/bc4j">
 <bc4j:registryDef>
    ...
 </bc4j:registryDef>
</page>

The UIX Controller <page> element doesn't know anything about the <bc4j:registryDef> element, and it doesn't have to. To support child elements in your ParserExtension, implement the startExtensionElement() method to return the correct NodeParser. The object returned by that parser will be stored in the Dictionary that eventually is passed to elementEnded().

There is one known limitation of the ParserExtension API. If you recall, each NodeParser parses a subtree of the XML document. Extension attributes and elements are only supported on the "root" UIX XML elements of each of those subtrees. So, for instance, consider the following snippet of UIX Components XML:

 <stackLayout>
   <separator>
     <spacer>
       <boundAttribute name="height">
         <fixed text="5"/>
       </boundAttribute>
     </spacer>
   </separator>
   <contents>
     <styledText text="First"/>
     <styledText text="Second"/>
   </contents>
 </stackLayout>

Extension attributes and elements can be added to <stackLayout>, <spacer>, <styledText> and <fixed> elements, but cannot be added to <separator>, <contents>, or <boundAttribute>. Broadly speaking, ParserExtensions can only modify elements that directly correspond to Java objects, and while UINode and BoundValue elements qualify, envelope elements do not.

Using UIX Parsing Outside of UIX

The UIX Parsing API has no dependencies on any of UIX Components or the UIX Controller, and as we mentioned before, its extensibility and built-in introspection capabilities make it a good approach for parsing XML even when your entire application has no need for any of the rest of UIX. You've already seen most of the basics of the XML parsing API, but here we'll walk through the few steps needed to use the API on its own.

To use XML parsing on its own, you need to prepare the following objects:

  1. An ErrorLog that identifies where to log warnings or errors. If not specified, errors are logged to the console.
  2. A ParseContext: this should be just an instance of ParseContextImpl.
  3. A ParserManager: each needed ParserFactory and ParserExtension must be registered.
  4. A NameResolver: This interface defines how to locate the source of the XML file. While a simple SAX InputSource would have been sufficient for parsing single files, the uiXML parsing API needs to support XML files that include other XML files, so we need a way to locate those files relative to the original. We include several implementations of NameResolver:

Once these objects have been gathered, you'll also need to know the name of the file being parsed, and the type of object you want to produce. Then, a single call to XMLUtils.parseSource() does the trick:

  import oracle.cabo.share.xml.XMLUtils;
  import oracle.cabo.share.xml.ParseContext;
  import oracle.cabo.share.xml.ParseContextImpl;
  import oracle.cabo.share.xml.ParserManager;
  import oracle.cabo.share.io.NameResolver;
  import oracle.cabo.share.error.ErrorLog;

  ...

  // Get the objects we need
  ErrorLog log = ...;
  ParseContext context = new ParseContextImpl(log);
  ParserManager manager = ...;
  NameResolver resolver = ...;

  // And parse.
  YourType result = (YourType)
     XMLUtils.parseSource(context,
                          null,
                          manager,
                          resolver,
                          "yourFile.xml",
                          YourType.class);

In your parsing code, if you need the result of an included file, you simply need to call XMLUtils.parseInclude(). This method needs only three parameters: the ParseContext you're already using, the name of the desired file, and the expected object type. The parsing code will take care of the rest.