Return-Path: Delivered-To: apmail-cocoon-docs-archive@www.apache.org Received: (qmail 25198 invoked from network); 6 Sep 2005 20:52:21 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (209.237.227.199) by minotaur.apache.org with SMTP; 6 Sep 2005 20:52:21 -0000 Received: (qmail 14630 invoked by uid 500); 6 Sep 2005 20:52:21 -0000 Delivered-To: apmail-cocoon-docs-archive@cocoon.apache.org Received: (qmail 14591 invoked by uid 500); 6 Sep 2005 20:52:20 -0000 Mailing-List: contact docs-help@cocoon.apache.org; run by ezmlm Precedence: bulk list-help: list-unsubscribe: List-Post: Reply-To: docs@cocoon.apache.org List-Id: Delivered-To: mailing list docs@cocoon.apache.org Received: (qmail 14574 invoked by uid 99); 6 Sep 2005 20:52:20 -0000 X-ASF-Spam-Status: No, hits=0.2 required=10.0 tests=NO_REAL_NAME X-Spam-Check-By: apache.org Received: from [207.7.158.203] (HELO cocoon.zones.apache.org) (207.7.158.203) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 06 Sep 2005 13:52:18 -0700 Message-ID: <22616460.1126039766783.JavaMail.daisy@cocoon.zones.apache.org> Date: Tue, 6 Sep 2005 20:49:26 +0000 (GMT+00:00) From: daisy@cocoon.zones.apache.org To: docs@cocoon.apache.org Subject: [DAISY] Updated: Creating a Generator Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable X-Virus-Checked: Checked by ClamAV on apache.org X-Spam-Rating: minotaur.apache.org 1.6.2 0/1000/N A document has been updated: http://cocoon.zones.apache.org/daisy/documentation/688.html Document ID: 688 Branch: main Language: default Name: Creating a Generator (unchanged) Document Type: Document (unchanged) Updated on: 9/6/05 8:49:16 PM Updated by: Berin Loritsch A new version has been created, state: publish Parts =3D=3D=3D=3D=3D Content ------- This part has been updated. Mime type: text/xml (unchanged) File name: (unchanged) Size: 15565 bytes (previous version: 3565 bytes) Content diff: (24 equal lines skipped) =20

How the Sitemap Treats a Generator

=20 ---

In the eyes of the Sitemap, all XML pipelines start with the Generat= or.=C2=A0 By +++

In the eyes of the Sitemap, all XML pipelines start with the Generat= or. By definition, a Generator is the first XMLProducer<= /a> in the --- pipeline.=C2=A0 It is the source of all SAX events that the pipeline ha= ndles.=C2=A0 It is +++ pipeline. It is the source of all SAX events that the pipeline handles.= It is also a SitemapModelComponent, so it must foll= ow those --- contracts as well.=C2=A0 Lastly, it can be a +++ contracts as well. Lastly, it can be a CacheableProcessingComponent satisfying those= contracts --- as well.=C2=A0 As usual, the order of contracts honored starts with the +++ as well. As usual, the order of contracts honored starts with the SitemapModelComponent, then the CacheableProcessingComponent contracts,= and --- lastly the XMLProducer contracts.=C2=A0 If the results of the Generator= can be --- cached, Cocoon will attempt use the cache and bypass the Generator alto= gether if --- possible.=C2=A0 The Caching mechanism can take the place of the Generat= or because it +++ lastly the XMLProducer contracts. If the results of the Generator can b= e cached, +++ Cocoon will attempt use the cache and bypass the Generator altogether i= f +++ possible. The Caching mechanism can take the place of the Generator bec= ause it can recreate a SAX stream on demand as an XMLProducer.

=20

In the big scheme of things, the Sitemap will setup() the G= enerator, --- and then assemble the XML pipeline.=C2=A0 After Cocoon assembles the pi= peline, +++ and then assemble the XML pipeline. After Cocoon assembles the pipeline= , chaining all XMLProducers to XMLConsumers (remember that an XMLPipe is = both), --- Cocoon will call the generate() method on the Generator.=C2=A0= That is the +++ Cocoon will call the generate() method on the Generator. That = is the signal to start producing results, so send out the SAX events and have = fun.

=20

Building our Own Generator

=20 ---

We are going to keep things easy for our generator.=C2=A0 As usual, = we will make --- the results cacheable because that is just good policy.=C2=A0 In the sp= irit of trying +++

We are going to keep things easy for our generator. As usual, we wil= l make +++ the results cacheable because that is just good policy. In the spirit o= f trying to be useful, as well as trying to keep things manageably simple, let's= create a --- BeanSerializer.=C2=A0 The XML generated will be very simple, utilizing = the JavaBean +++ BeanSerializer. The XML generated will be very simple, utilizing the Ja= vaBean contracts and creating an element for each property, and embedding the = value of --- the property within that element.=C2=A0 There won't be any attributes, = and the --- namespace will match the the JavaBean's fully qualified class name.=C2= =A0 If a +++ the property within that element. There won't be any attributes, and th= e +++ namespace will match the the JavaBean's fully qualified class name. If = a property has something other than a primitive type, a String or an equi= valent (like Integer and Boolean) as its value, that object will be treated as= a Bean.

=20 ---

To realize these requirements we have to find a bean, and then "rend= er it".=C2=A0 --- In this case the XML rendering of the bean will be recursive.

+++

To realize these requirements we have to find a bean, and then "rend= er it". +++ In this case the XML rendering of the bean will be recursive.=C2=A0 The= general +++ approach will use Java's reflection mechanisms, and only worry about +++ properties.=C2=A0 There will be a certain amount of risk involved with = a complex bean +++ that includes references to other beans in that if you have two beans r= eferring +++ to each other you will have an infinite loop.=C2=A0 Detecting these is = outside the +++ scope of what we are trying to do, and that is generally bad design any= way so we +++ won't worry too much about it.=C2=A0 Yes there is overhead with the bea= ns +++ Introspector but we are writing for the general case.

=20 +++

To set up our generator, we need to use a serializer that shows us w= hat the +++ results are, so we will set up our sitemap to use our generator like th= is:

+++=20 +++
<map:match pattern=3D"bean/*.xml">
+++   <map:generate type=3D"bean" src=3D"{1}"/>
+++   <map:serialize type=3D"xml"/>
+++ </map:match>
+++ 
+++=20 +++

Even though it is generally bad design to have a static anything in = a Cocoon +++ application we are going to use a helper class called "BeanPool" with t= he get() +++ and put() methods that are familiar from the HashMap.=C2=A0 So that it = is easier for +++ you to change the behavior of the BeanGenerator, we will provide a nice +++ protected method called findBean() which is meant to be overri= dden with +++ something more robust.

+++=20 +++

The Skeleton

+++=20 +++

Our skeleton code will look like this:

+++=20 +++
import org.apache.avalon.framework.parameters.Parameters;
+++ import org.apache.cocoon.ProcessingException;
+++ import org.apache.cocoon.ResourceNotFoundException;
+++ import org.apache.cocoon.caching.CacheableProcessingComponent;
+++ import org.apache.cocoon.environment.SourceResolver;
+++ import org.apache.cocoon.generation.AbstractGenerator;
+++ import org.apache.excalibur.source.SourceValidity;
+++ import org.apache.excalibur.source.NOPValidity;
+++ import org.xml.sax.Attributes;
+++ import org.xml.sax.SAXException;
+++ import org.xml.sax.helpers.AttributesImpl;
+++=20
+++ import java.beans.Introspector;
+++ import java.beans.BeanInfo;
+++ import java.beans.PropertyDescriptor;
+++ import java.lang.reflect.Method;
+++ import java.io.IOException;
+++ import java.io.Serializable;
+++=20
+++ public class BeanGenerator extends AbstractGenerator implements Cacheab=
leProcessingComponent
+++ {
+++     private static final Attributes ATTR =3D new AttributesImpl();
+++     private Object m_bean;
+++=20
+++     protected Object findBean(String key)
+++     {
+++         // replace this with something more robust.
+++         return BeanPool.get(key);
+++     }
+++=20
+++     public void setup( SourceResolver sourceResolver, Map model, String=
 src, Parameters params )
+++         throws IOException, ProcessingException, SAXException
+++     {
+++         // ... skip setup code for now
+++     }
+++=20
+++     public void generate() throws IOException, SAXException, Processing=
Exception
+++     {
+++         // ... skip generate code for now
+++     }
+++=20
+++     // ... skip other methods later.
+++ }
+++ 
+++=20 +++

As you can see, we have our simplified findBean() method wh= ich can +++ be replaced with something more robust later.=C2=A0 All you need to do = to populate +++ the BeanPool is to call the BeanPool.put(String key, Object bean) +++ method from somewhere else.

+++=20 +++

Setting up to Generate

+++=20 +++

Before we can generate anything let's start with the setup() code: +++

+++=20 +++
    super.setup( sourceResolver, model, src, params );
+++     m_bean =3D findBean(src);
+++=20
+++     if ( null =3D=3D m_bean )
+++     {
+++         throw new ResourceNotFoundException(String.format("Could not fi=
nd bean: %s", source));
+++     }
+++ 
+++=20 +++

What we did is call the setup method from AbstractGenerator which po= pulates +++ some class fields for us (like the source field), then we trie= d to find +++ the bean using the key provided.=C2=A0 If the bean is null, th= en we follow +++ the principle of least surprise and throw the ResourceNotFoundExcep= tion +++ so the Sitemap knows that we simply don't have the bean available = instead +++ of generating some lame 500 server error.=C2=A0 That's all we have to d= o to set up +++ this particular Generator.=C2=A0 Oh, and since we do have the m_bea= n field +++ populated we do want to clean up after ourselves properly.=C2=A0 Let's = add the +++ recycle() method from Recyclable so that we don't give an old result wh= en a bean +++ can't be found:

+++=20 +++
   @Override
+++    public void recycle()
+++    {
+++       super.recycle();
+++       m_bean =3D null;
+++    }
+++ 
+++=20 +++

The Caching Clues

+++=20 +++

We are going to make the caching for the BeanGenerator really simple= .=C2=A0 +++ Ideally we would have something that listens for changes and invalidate= s the +++ SourceValidity if there is a change to the bean we are rendering.=C2=A0= Unfortunately +++ that is outside our scope, and we will set up the key so that it never = expires +++ unless it is done manually.=C2=A0 Since we are using the source propert= y from the +++ Sitemap as our key, let's just use that as our cache key:

+++=20 +++
    public Serializable getKey()
+++     {
+++         return source;
+++     }
+++ 
+++=20 +++

And lastly, our brain-dead validity implementation:

+++=20 +++
    public SourceValidity getValidity()
+++     {
+++         return NOPValidity.SHARED_INSTANCE;
+++     }
+++ 
+++=20 +++

Using this approach is a bit naive in the sense that it is very poss= ible that +++ the beans will have changed.=C2=A0 We could use an ExpiresValidity inst= ead to make +++ things a bit more resilient to change, but that is an excersize for you= , dear +++ reader.

+++=20 +++

Generating Output

+++=20 +++

Now that we have our bean, we are ready to generate our output.=C2= =A0 The +++ AbstractXMLProducer base class (AbstractGenerator inherits from that) s= tores the +++ target in a class field named contentHandler.=C2=A0 Simple eno= ugh.=C2=A0 We'll +++ start by implementing the generate() method, but we already know we nee= d to +++ handle beans differently than the standard String and primitive types.= =C2=A0 So let's +++ stub out the method we will use for recursive serialization.=C2=A0 Here= we go:

+++=20 +++
    public void generate()
+++     {
+++         contentHandler.startDocument();
+++=20
+++         renderBean(source, m_bean);
+++=20
+++         contentHandler.endDocument();
+++     }
+++ 
+++=20 +++

All we did was call the start and end document for the whole XML Doc= ument.=C2=A0 +++ That is enough for a basic XML document with no content.=C2=A0 The +++ renderBean() method is where the magic happens:

+++=20 +++
    public void renderBean(String root, Object bean)
+++     {
+++         String namespace =3D String.format( "java:%s", bean.getClass().=
getName() );
+++         qName =3D String.format( "%s:%s", root,root );
+++=20
+++         contentHandler.startPrefixMapping( root, namespace );
+++         contentHandler.startElement( namespace, root, qName, ATTR );
+++=20
+++         BeanInfo info =3D Introspector.getBeanInfo(bean.getClass());
+++         PropertyDescriptor[] descriptors =3D info.getPropertyDescriptor=
s();
+++=20
+++         for ( PropertyDescriptor property : descriptors )
+++         {
+++             renderProperty( namespace, root, property );
+++         }
+++=20
+++         contentHandler.endElement( namespace, root, qName );
+++         contentHandler.endNamespace( root );
+++     }
+++ 
+++=20 +++

So far we created the root element and started iterating over the +++ properties.=C2=A0 Our root element consists of a namespace a name and a= qName.=C2=A0 Our +++ implementation is using the source for the initial root elemen= t so as +++ long as we never have any special characters like a colon (':') we shou= ld be +++ OK.=C2=A0 Without going through the individual properties, a java.awt.D= imension +++ object with a source of "dim" will be redered like this:

+++=20 +++
<dim:dim xmlns:dim=3D"java:java.awt.Dimension"/>
+++ 
+++=20 +++

Now for the properties:

+++=20 +++
    private void renderProperty( String namespace, String root, Pr=
opertyDescriptor property )
+++     {
+++         Method reader =3D property.getReadMethod();
+++         Class<?> type =3D property.getPropertyType();
+++=20
+++         // only output if there is something to read, and it is not an =
indexed type
+++         if ( null !=3D reader && null !=3D type )
+++         {
+++             String name =3D property.getName();
+++             String qName =3D String.format( "%s:%s", root,name );
+++             Object value =3D reader.invoke( m_bean );
+++=20
+++             contentHandler.startElement( namespace, name, qName, ATTR )=
;
+++=20
+++             if ( isBean(type) )
+++             {
+++                 renderBean( name, value )
+++             }
+++             else if ( null !=3D value )
+++             {
+++                 char[] chars =3D String.valueOf(value).toCharArray();
+++                 contentHandler.characters(chars, 0, chars.length);
+++             }
+++=20
+++             contentHandler.endElement( namespace, name, qName );
+++         }
+++     }
+++ 
+++=20 +++

This method is a little more complex in that we have to figure out i= f the +++ property is readable, and is a type we can handle.=C2=A0 In this case, = we don't read +++ indexed properties (if you want to support that, you'll have to extend = this code +++ to do that), and we don't read any properties where there is no read me= thod.=C2=A0 We +++ use the property name for the elements surrouding the property values.= =C2=A0 We get +++ the value, and then we call the start and end elements for the property= .=C2=A0 Inside +++ of the calls, we determine if the item is a bean, and if so we render t= he bean +++ using the renderBean method (the recursive aspect); otherwise we render= the +++ content as text as long as it is not null.=C2=A0 Once the isBean()<= /tt> method is +++ implemented, our Dimension example above will produce the following res= ult:

+++=20 +++
<dim:dim xmlns:dim=3D"java:java.awt.Dimension">
+++   <dim:width>32</dim:width>
+++   <dim:height>32</dim:height>
+++ </dim:dim>
+++ 
+++=20 +++

Ok, now for the last method to determine if a value is a bean or not= :

+++=20 +++
    private boolean isBean(Class<?> klass)
+++     {
+++         if ( Boolean.TYPE.equals( klass ) ) return false;
+++         if ( Byte.TYPE.equals( klass ) ) return false;
+++         if ( Character.TYPE.equals( klass ) ) return false;
+++         if ( Double.TYPE.equals( klass ) ) return false;
+++         if ( Float.TYPE.equals( klass ) ) return false;
+++         if ( Integer.TYPE.equals( klass ) ) return false;
+++         if ( Long.TYPE.equals( klass ) ) return false;
+++         if ( Short.TYPE.equals( klass ) ) return false;
+++         if ( java.util.Date.class.equals( klass ) ) return false; // tr=
eat dates as value objects
+++         if ( klass.getName().startsWith( "java.lang" ) ) return false;
+++=20
+++         return true;
+++     }
+++ 
+++=20 +++

The isBean() method will treat all primitives, Strings, Dates, and a= nything +++ in "java.lang" as value objects.=C2=A0 This captures the boxed versions= of primitives +++ as well as the unboxed versions.=C2=A0 Everything else is treated as a = bean.

+++=20 +++

Summary

+++=20 +++

Generators aren't too difficult to write, but the tricky parts are t= here due +++ to namespaces.=C2=A0 As long as you are familiar with the SAX API you s= hould not have +++ any problems.=C2=A0 The complexity in our generator is really from the = reflection +++ logic used to discover how to render an object.=C2=A0 You might ask why= we didn't use +++ the XMLEncoder in the java.beans package.=C2=A0 The answer has to do wi= th the fact +++ that the facility is based on IO streams, and can't be easily adapted t= o XML +++ streams.=C2=A0 At any rate, we have something that can work with a wide= range of +++ classes.=C2=A0 Our XML is easy to understand.=C2=A0 Here is a snippet f= rom a more complex +++ example:

+++=20 +++
<line:line xmlns:line=3D"java:com.mycompany.shapes.Line">
+++   <line:name>This line has a name</line:name>
+++   <line:topLeft>
+++     <topLeft:topLeft xmlns:topLeft=3D"java:java.awt.Point">
+++       <topLeft:x>1</topLeft:x>
+++       <topLeft:y>1</topLeft:y>
+++     </topLeft:topLeft>
+++   </line:topLeft>
+++   <line:bottomRight>
+++     <bottomRight:bottomRight xmlns:bottomRight=3D"java:java.awt.Poin=
t">
+++       <bottomRight:x>20</bottomRight:x>
+++       <bottomRight:y>20</bottomRight:y>
+++     </bottomRight:topLeft>
+++   </bottomRight:topLeft>
+++ </line:line>
+++ 
+++=20 +++

Our therhetical line object contained a name and two java.awt.Point = objects +++ which in turn had an x and a y property.=C2=A0 It is easier to understa= nd when you +++ have domain specific beans that are backed to a database.=C2=A0 Neverth= eless, we have +++ a generator that satisfies a general purpose and can be extended later = on to +++ support our needs as they change.

+++=20 Fields =3D=3D=3D=3D=3D=3D no changes Links =3D=3D=3D=3D=3D no changes Custom Fields =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D no changes Collections =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D no changes