UIX Developer's Guide |
Contents |
Previous |
Next |
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:
<method>
element lets you attach Java code
for serving data or handling events.
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.
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";
}
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 © 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:
<form>
element inside
of another)
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.
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:
Renderer
together. It's technically legal
to just override getRenderer()
to
directly return the renderer, but it's much cleaner to let the
system connect the two.
getAttributeValue()
and setAttributeValue()
. This is critical. If
you try to store values directly as instance variables, databinding will not function, your component cannot be templated, and
your component cannot easily be embedded inside uiXML. You'd
also have to rewrite how your Renderer
gets
those attributes.
getAttributeValue()
or
setAttributeValue
, which would
completely bypass those overrides. By marking these methods
final, we've prevented this problem from ever happening.
IntegerUtils.getInteger()
call:
UINode
attributes must be stored as
objects, so we have to convert the int
into an Integer
. Normally, developers
simply call new Integer(int)
to create
an Integer
object. But Integer
objects are immutable, so they can
be freely shared. IntegerUtils
maintains a cache of frequently used Integer
objects that saves a lot of object
creations, and consequently a lot of time. This technique applies
to a lot more than beans!
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.
To register a renderer, you need to take care of the following steps:
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.
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.
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.
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:
FlipperUINodeParserFactory
extends
UINodeParserFactory
, which is a ParserFactory
.
UINodeParserFactory
.
UINodeType
to use. This node type will get used for any element in this
namespace that isn't explicitly described. You'll usually pass
null, so that elements you don't support trigger parsing errors.
ArrayMap
that
defines all the element names. Despite the name of this class,
ArrayMap
is actually a subclass of the
JDK 1.1 java.util.Dictionary
API, not
the Java2 java.util.Map
API. For
historical reasons, UINodeParserFactory
does not accept Maps
. The ArrayMap
class is optimized for extremely
small data sets, which this clearly is.
UINodeType
for the<copyright>
element. We
pass four parameters to the BaseUINodeType
constructor:
CopyrightBean
doesn't
support any named children, so this is null.
BaseUINodeType.getDefaultUINodeType()
, we
get built-in support for the "rendered" attribute as well as the
<boundAttribute>
child element.
UINodeType
with the correct element name -
"copyright"
. It's essential that this
element name and the element's namespace match the name and
namespace used to register the Renderer
.
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>
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.
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 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.
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 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.
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.
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:
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).
NodeParser.startElement()
is called
to handle the attributes of that top-level element.
NodeParser.startChildElement()
. This method must
identify the NodeParser
needed to process
the child. It can handle the call in one of a few ways.
ParserManager
for a NodeParser
. That NodeParser
will assume responsibility for handling that child element (and all of its children). When it finishes, NodeParser.addCompletedChild()
will be called on the parent NodeParser
to incorporate the
results.
NodeParser
. The "envelope elements"
needed for complex attributes and named children are a common example.
NodeParser
implementation. This may provide a simpler, more factored approach
than simply returning "this".
startChildElement
returned "this",
UIX calls NodeParser.endChildElement()
.
NodeParser.addCompletedChild()
. The object
created by that child's Parser is passed into this method, so the
Parser can store it as needed. This method may get passed
"null" if parsing failed, so implementations must handle this case.
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.
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()
.
NodeParser.endElement()
is
called. This method must return an Object representing the fully
constructed object.
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.
BaseParserExtension
. This base class gives us
default implementations for all the methods and convenience
methods for logging parse warnings.
elementEnded()
.
This method is called once the element we're extending has finished,
and gets passed five parameters:
ParseContext
,
which provides parse-time context to your code.
Dictionary
of extension values.
The value of each extension attribute and element is stored here
with the name of the attribute or element as the key.
MutableUINode
interface. One major
difference between ParserExtensions
and
ParserFactories
is that extensions are
not registered by type. Instead, you have to use a single
ParserExtension
for an entire namespace,
no matter what the types are. Consequently, we check the type
here and log a warning if the type is incorrect.
elementEnded()
always returns
"parsed". This means that we aren't replacing the node, just
modifying it. This is the safest return value. It's legal
to return "null", which will effectively remove the object
altogether.
_extendUINode()
method, we
retrieve the "destination" value, check its value, and set the
destination of our MutableUINode
accordingly.
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.
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:
ErrorLog
that identifies where
to log warnings or errors. If not specified, errors are logged
to the console.
ParseContext
: this should
be just an instance of ParseContextImpl
.
ParserManager
: each needed ParserFactory
and ParserExtension
must be registered.
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
:
DefaultNameResolver
: can locate
files relative to either or both a base file directory or URL.
ServletNameResolver
: can locate
files relative to a Servlet context
ClassResourceNameResolver
: can locate
files relative to a Class (in a JAR, for example)
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.