Templating Web Sites

Templating and Web Sites

This document is under construction.

Dealing with pre-formatted text

You may occasionally have HTML-preformatted text that you need to integrate into a Bebop component. For example, in the above HelloDate component, if you took the text for the "Hello world.." string from a message catalog to allow for globalization, and the translator put a string with formatting ("Hola <b>todo el mundo!</b> La fecha de hoy es ..."), you would need to deal with this intelligently.

We handle this situation in the Bebop Label component, and by disabling output escaping in the XSLT processor for the context of the label. The code
Label l = new Label("This is a <b>new</b> label.", false);
l.generateXML(pageState, element);
will generate the XML fragment
<bebop:label escape="yes">This is a &lt;b>new&lt;/b&gt; 
   label.</bebop:label>
where the HTML-preformatted text is a single, text-node child to the bebop:label element. On output, the angle brackets will not be output-escaped, so they will appear in the HTML stream as angle brackets and be interpreted as such by the user's web browser, causing the word "new" to appear in bold. By default, the XSLT processor escapes output according to the output mode (specified in the stylesheet with the xsl:output element), so that angle brackets will appear as angle brackets in the user's browser, which means they are trasmitted as &lt;'s in the HTML stream.

Note that the escape="yes" attribute to bebop:label is confusing because it is shorthand for what will become the XSLT attribute disable-output-escaping="yes".

If you want angle brackets to show up in your output (less-than signs, for example), you want to keep output escaping on, which it is by default.

Label l = new Label("3.141 < pi < 3.142");

Inserting HTML formatting into the Bebop DOM, where it is difficult to style globally, is not generally recommended. An alternative solutions is to parse the string argument to the Label constructor as a DOM fragment, instead of simply disabling output escaping, so that the formatting elements are a full-fledged part of the DOM and eligible for global styling. A drawback to this is the expense of parsing XML. Additionally, most people do not write well-formed XHTML, while browsers are much more tolerant than XML parsers. In other words, it is perfectly legal to write the following:
Label l = new Label("<ul> <li> list item 1 <li> list item 2 
   </ul>", false);
This will be displayed correctly as an unordered list in the browser, even though the label contents are not well-formed [X]HTML.

Site-wide master pages

Often, within the scope of an application, many pages share a common layout. That is, all of the pages within an application may have the same four-panel layout, with the same header, footer, and sidebar content, while the main content of the page varies. Another way of looking at this is that the main content of each page is enclosed in another "master" page.

There are two aspects to making shared-layout pages like this. First, there is the declaration of page components in Java. With Bebop, the top-level Page object must contain the components that generate the dynamic content displayed in each of the header, footer, sidebar, and main panels as DOM fragments. (Note that in any shared-layout page like this, the contents of the header, etc., can be dynamic; for example, a footer can display "this page has been viewed (xxxxx) times.")

Second, you need an XSLT stylesheet to render the page properly. The XSLT stylesheet is responsible for selecting the content for each of the panels on the page, and placing them appropriately in the output, with the right look-and-feel (color scheme, dimensions, etc.) Note that this means whatever DOM is generated by the page components above must allow for the XSLT stylesheet to distinguish between the content that belongs in the header, footer, etc.

The preferred way to implement shared-layout pages like this in Bebop is to subclass Page. Users of this class add components to the page as usual, but the page's constructor and DOM-generation methods are overloaded to pre-fill the page with the appropriate boilerplate components.

The following example is a Page that always contains a header, footer, and sidebar:

package com.arsdigita.bebop.demo;

import com.arsdigita.bebop.*;
import com.arsdigita.xml.*;
import com.arsdigita.dispatcher.RequestContext;
import com.arsdigita.dispatcher.DispatcherHelper;

/**
 * This is a common page for a fictitious site, SockPuppet.com.
 * It includes a common header, a footer, and a main "content"
 * area.  We override the .generateXML method to pre-fill the page
 * with the boilerplate content.
 */
public class SockPuppetPage extends Page {
    
    private Component m_top;
    private Component m_bottom;
    private Component m_side;
    
    public SockPuppetPage() {
        this("");
    }

    public SockPuppetPage(String s) { 
        super("SockPuppet.com: " + s);
        m_top = new SiteHeader();
        m_bottom = new SiteFooter();
        m_side = new SiteSide();
    }

    public void generateXML(PageState ps, Document doc) { 
        Element page = generateXML(doc);
        Element layout = new Element("socksite:layout", SOCKSITE_XML_NS);
        page.addContent(layout);

        addContents(layout, ps);
    }

    protected void addContents(Element layout, PageState ps) {
        Element topPanel = 
           new Element("socksite:top", SOCKSITE_XML_NS);
        layout.addContent(topPanel);
        m_top.generateXML(ps, topPanel);

        Element sidePanel = 
           new Element("socksite:side", SOCKSITE_XML_NS);
        layout.addContent(sidePanel);
        m_side.generateXML(ps, sidePanel);

        Element bottomPanel = 
           new Element("socksite:bottom", SOCKSITE_XML_NS);
        layout.addContent(bottomPanel);
        m_bottom.generateXML(ps, bottomPanel);

        Element mainPanel = 
           new Element("socksite:main", SOCKSITE_XML_NS);
        layout.addContent(mainPanel);
        m_panel.generateXML(ps, mainPanel);
    }

    /**
     * Header component.  Demonstrates dynamic content.
     */
    private class SiteHeader extends Label { 
        public SiteHeader() { 
            super(new PrintListener() {
                public void prepare(PrintEvent pevt) { 
                    Label target = (Label)pevt.getTarget();
                    PageState ps = pevt.getPageState();
                    RequestContext rc = 
                      DispatcherHelper.getRequestContext
                        (ps.getRequest());
                    target.setLabel("SockPuppet.com: 
                      dynamic page header."
                       + "  You requested: " + rc.getOriginalURL());
                }
            }
        }
    }
    
    /**
     * Footer component.  All static.
     */
    private class SiteFooter extends Label { 
        public SiteFooter() { 
            super("SockPuppet.com: static footer.");
        }
    }

    /**
     * Sidebar component.  All static.
     */
    private class SiteSide extends Label { 
        public SiteSide() { 
            super("SockPuppet.com: static sidebar.");
        }
    }
}

You would use this Page like any other, for example, Page p = new SockPuppetPage(); p.add(...); .... However, when you call buildDocument, the generated DOM will include all of the content from the header, footer, etc.:

<bebop:page> 

... some page boilerplate ...

<socksite:top>
  <bebop:label>
    SockPuppet.com: dynamic page header.  You requested: ...
  </bebop:label>
</socksite:top>

<socksite:side>
  <bebop:label>
    SockPuppet.com: static side panel
  </bebop:label>
</socksite:side>

<socksite:bottom>
  <bebop:label>
    SockPuppet.com: static footer
  </bebop:label>
</socksite:bottom>

<socksite:main>
  ... main contents here ...
</socksite:main>

</bebop:page>

Notice that the order of the components in the DOM may have nothing at all to do with the ordering of the output! The page components just ensure that the right content is generated with the right logical structure. It is up to the XSLT stylesheet to ensure that a "socksite:top" element really appears at the top of the page.

You would need an XSLT template rule such as the following to convert the above DOM into meaningful XHTML output:

<!-- match the layout element and put the 
header, footer, etc., in their appropriate places. -->

<xsl:template match="socksite:layout" 
               xmlns:socksite="http://www.sockpuppet.com/xmlns">

 <xsl:apply-templates select="socksite:top"/>

 <table>
   <tr>
     <td width="25%"><xsl:apply-templates 
        select="socksite:side"/></td>
     <td><xsl:apply-templates 
        select="socksite:main"/></td>
   </tr>
 </table>

 <xsl:apply-templates select="bebop:bottom"/>

</xsl:template>

This template must be associated with the appropriate application (for example, the main "SockSite" application) or site node.

Varying a shared layout

Once you have a shared-layout Page subclass, such as SockPuppetPage above, you will likely have single pages that vary the boilerplate content slightly. For example, you might have a single page which uses the same header, footer, and sidebar as all other pages on your site, but which adds an extra boilerplate component above the header, such as a banner ad.

You can accomplish this with further subclassing. You can define a subclass of SockPuppetPage, BannerAdSockPuppetPage, which has all of the same pre-filled components but adds a second header, a BannerAd component:

package com.arsdigita.bebop.demo;

import com.arsdigita.bebop.*;
import com.arsdigita.xml.*;
import com.arsdigita.dispatcher.RequestContext;
import com.arsdigita.dispatcher.DispatcherHelper;
// fictitious banner ad management system
import com.arsdigita.personalization.BannerAdManager;

public class BannerAdSockPuppetPage extends SockPuppetPage {
    
    private Component m_banner_ad;
    
    public BannerAdSockPuppetPage() {
        this("");
    }

    public BannerAdSockPuppetPage(String s) { 
        // calling super() adds default header, footer, and sidebar
        super("SockPuppet.com: " + s);
        m_banner_ad = new BannerAd();
    }

    public void generateXML(PageState ps, Document doc) { 
        Element page = generateXML(doc);
        Element layout = new Element("socksite:layout", SOCKSITE_XML_NS);
        page.addContent(layout);

        // generate XML for header, footer, sidebar, and main panel
        super.addContents(layout, ps); 
        
        // just add the new banner ad here
        Element bannerAd = new Element("socksite:bannerAd", 
           SOCKSITE_XML_NS);
        layout.addContent(bannerAd);
        m_banner_ad.generateXML(ps, bannerAd);
    }

    /**
     * Header component.  Demonstrates dynamic content.
     */
    private class BannerAd extends SimpleComponent { 
        public void generateXML(PageState ps, Element parent) { 
            Element elt = new Element("socksite:bannerAd", 
                SOCKSITE_XML_NS);
            elt.setText(BannerAdManager.getInstance().getBanner());
            parent.addContent(elt);
        }
    }
}

You must also amend the XSLT stylesheet:

<!-- match the layout element and put the 
header, footer, etc., in their appropriate places. -->

<xsl:template match="socksite:layout" 
               xmlns:socksite="http://www.sockpuppet.com/xmlns">

 <xsl:apply-templates select="socksite:top"/>

 <xsl:apply-templates select="socksite:bannerAd"/>

 <table>
   <tr>
     <td width="25%"><xsl:apply-templates 
        select="socksite:side"/></td>
     <td><xsl:apply-templates select="socksite:main"/></td>
   </tr>
 </table>

 <xsl:apply-templates select="bebop:bottom"/>

</xsl:template>

Note that the above stylesheet will work whether or not the input DOM actually contains a banner ad; the <xsl:apply-templates select="socksite:bannerAd"/> code will simply be ignored if no such element exists in the DOM.

Special-case stylesheets

You may occasionally want to override the default stylesheet rules for one particular page. Because of the way XSLT works, you can choose to override only some "template match=..." rules, allowing others to be handled by the default stylesheet.

You should handle this in the dispatcher for your application, or by overriding the getStylesheetTransformer() or servePage methods in a PresentationManager class specific to your application. For example:

public class SockPuppetPresentationManager extends 
    BasePresentationManager {

    public void servePage(Page p, 
                          HttpServletRequest req, 
                          HttpServletResponse resp) 
        throws IOException, ServletException {

        RequestContext rctx = DispatcherHelper.getRequestContext(req);
        String url = rctx.getRemainingURLPart();

        // serve page as usual, except for URLs foo/*; those get a 
        // special-case stylesheet
        if (!url.startsWith("foo/")) {
            super.servePage(p, req, resp);
        } else {
            // use custom stylesheet for foo/*
            // in reality, probably cache Transformer for performance
            Stylesheet fooSS = ... get overridden rules 
               from somewhere ...;
            Transformer xformer = fooSS.newTransformer();
            Document doc = p.buildDocument(req, resp);
            Writer out = resp.getWriter();
            xformer.transform(new DOMSource(doc), new StreamResult(out));
        }
    }
}