MarkupWriter

Instead of rendering from a template, a page or component can render a DOM directly with Tapestry's MarkupWriter as described here.
For components that need a lot of control over the sequence of output it can be easier to use than a template.

A really simple example is Tapestry's TextField component, but its superclass AbstractTextField does most of the work.

Two more examples are JumpStart's SourceCodeDisplay and JodaTimeOutput components. Their source is below.

References: DOM, MarkupWriter.

Home

MarkupWriter.tml


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<head>
    <link rel="stylesheet" type="text/css" href="${context:css/examples/examples.css}"/>
</head>
<body>
    <h1>MarkupWriter</h1>

    Instead of rendering from a <em>template</em>, a page or component can render a DOM directly with 
    Tapestry's <em>MarkupWriter</em> as described <a href="http://tapestry.apache.org/dom.html">here</a>.<br/>
    For components that need a lot of control over the sequence of output it can be easier to use than a template.<br/><br/>
     
    A really simple example is Tapestry's  
    <a href="http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/TextField.java?view=markup">
    TextField</a> component, but its superclass 
    <a href="http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/corelib/base/AbstractTextField.java?view=markup">
    AbstractTextField</a> does most of the work.<br/><br/>
    
    Two more examples are JumpStart's SourceCodeDisplay and JodaTimeOutput components. Their source is below.<br/><br/> 
     
    References:  
    <a href="http://tapestry.apache.org/dom.html">DOM</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/MarkupWriter.html">MarkupWriter</a>.<br/><br/>
    
    <a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>

    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/component/MarkupWriter.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/component/MarkupWriter.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/examples.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/components/SourceCodeDisplay.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/components/JodaTimeOutput.java"/>
</body>
</html>

MarkupWriter.java


package jumpstart.web.pages.examples.component;


public class MarkupWriter {
}

examples.css


body            { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333; line-height: 17px; }
h1              { font-size: 26px; line-height: 20px; } /* For IE 7 */
form            { margin: 0; }                  

.eg             { margin: 20px 0; padding: 20px; color: #888; 
                    border: 1px solid #ddd; border-radius: 4px; -webkit-border-radius: 4px; -mox-border-radius: 4px; }

a               { text-decoration: none; color: #3D69B6; }
a:hover         { text-decoration: underline; }

/* For BeanDisplay */
.eg dl          { margin: 0; color: #333; }
.eg dl.t-beandisplay dd.id  { display: inline; margin-left: 0px; }  /* IE 7 hack */

SourceCodeDisplay.java


package jumpstart.web.components;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Context;

public class SourceCodeDisplay {
    static private String LINE_SEPARATOR = System.getProperty("line.separator");

    static private String STYLE_BOX = "margin: 10px 0px 0px 0px; background: #adffd6; padding: 8px; "
            + "border: 1px solid #ddd; border-radius: 8px; -webkit-border-radius: 8px; -moz-border-radius: 8px;";

    static private String STYLE_TITLE = "margin: -2px 0 0 0; text-align: left; font-family: Arial, Helvetica, sans-serif; "
            + "font-size: 12px; font-weight: normal; color: #444; line-height: 14px; ";

    static private String STYLE_SOURCE = "text-align: left; tab-stops: 5px; "
            + "font-size: 12px; font-weight: normal; color: #444; line-height: 14px; ";

    static private String STYLE_NOT_FOUND = "margin: 10px 0; font-family: Arial, Helvetica, sans-serif; "
            + "font-size: 12px; font-weight: normal; text-align: left; color: red;";
    
    static private int TAB_STOPS_WIDTH = 4;

    // The source file path from the project root eg. "/web/src/main/jumpstart/web/pages/Start.java"
    @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL)
    private String src;

    @Inject
    private Context context;

    boolean beginRender(MarkupWriter writer) {

        // Print start of the source block

        writer.write(LINE_SEPARATOR);
        writer.writeRaw("<!-- Start of source code inserted by SourceCodeDisplay component. -->");
        writer.write(LINE_SEPARATOR);
        writer.write(LINE_SEPARATOR);

        // Print a div with style info to make a pretty block

        writer.element("div", "style", STYLE_BOX);
        writer.write(LINE_SEPARATOR);
        {
            writer.element("div", "style", STYLE_TITLE);
            writer.write(LINE_SEPARATOR);
            {
                writer.write(extractSimpleName(src));
                writer.write(LINE_SEPARATOR);
            }
            writer.end();
        }
        // writer.element("hr", "style", "color: #ccc; background-color: #ccc;");
        writer.element("hr", "style", "color: #ddd; background-color: #ddd;");
        writer.end();

        // Print the source

        if (src != null) {
            printSourceFromInputStream(writer, src, "/WEB-INF/sourcecode" + src);
        }

        // Print end of div

        writer.write(LINE_SEPARATOR);
        writer.end();

        // Print end of source block

        writer.write(LINE_SEPARATOR);
        writer.write(LINE_SEPARATOR);
        writer.writeRaw("<!-- End of source code inserted by SourceCodeDisplay component. -->");
        writer.write(LINE_SEPARATOR);

        return true;
    }

    private String extractSimpleName(String path) {
        String simpleName = path;

        int i = path.lastIndexOf("/");
        simpleName = path.substring(i + 1);

        return simpleName;
    }

    private void printSourceFromInputStream(MarkupWriter writer, String title, String givenPath) {
        if (givenPath != null) {
            URL url = context.getResource(givenPath);
            try {
                if (url != null) {
                    InputStream templateStream = url.openStream();
                    if (templateStream != null) {
                        BufferedReader templateReader = new BufferedReader(new InputStreamReader(templateStream));
                        printSource(writer, templateReader);
                    }
                    else {
                        printResourceNotFound(writer, givenPath);
                    }
                }
                else {
                    printResourceNotFound(writer, givenPath);
                }
            }
            catch (IOException e) {
                printResourceNotFound(writer, givenPath);
            }
        }
    }

    private void printSource(MarkupWriter writer, BufferedReader sourceReader) {
        writer.element("div", "style", STYLE_SOURCE);
        {
            writer.element("pre", "style", "margin: 0;");
            {
                writer.write(LINE_SEPARATOR);
                writer.element("code");
                {
                    writer.write(LINE_SEPARATOR);

                    String s;
                    try {
                        while ((s = sourceReader.readLine()) != null) {
                            s = replaceTabsWithSpaces(s);
                            writer.write(s);
                            writer.write(LINE_SEPARATOR);
                        }
                    }
                    catch (IOException e) {
                        writer.write("Error reading .....?");
                        e.printStackTrace();
                    }

                    writer.end();
                }
                writer.write(LINE_SEPARATOR);
            }
            writer.end();
        }
        writer.end();
    }

    private void printResourceNotFound(MarkupWriter writer, String resourcePath) {
        writer.element("div", "style", STYLE_NOT_FOUND);
        {
            writer.write("The file was not found. Path given was " + resourcePath);
        }
        writer.end();
    }
    
    private String replaceTabsWithSpaces(String s) {
        StringBuilder sb = new StringBuilder();
        char c;
        int column = 1;
        
        for (int i = 0; i < s.length(); i++, column++) {
            if ((c = s.charAt(i)) == '\t') {
                sb.append(' ');
                while (column % TAB_STOPS_WIDTH != 0) {
                    sb.append(' ');
                    column++;
                }
            }
            else {
                sb.append(c);
            }
        }
        
        return sb.toString();
    }
}

JodaTimeOutput.java


// This is based on Tapestry's Output component.

package jumpstart.web.components;

import java.text.Format;

import org.apache.tapestry5.Binding;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.SupportsInformalParameters;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.services.ComponentDefaultProvider;
import org.joda.time.base.AbstractInstant;
import org.joda.time.base.AbstractPartial;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 * A component for formatting for output of JodaTime objects. It supports subclasses of AbstractInstant and
 * AbstractPartial. If the component is represented in the template using an element, then the element (plus any
 * informal parameters) will be output around the formatted value.
 */
@SupportsInformalParameters
public class JodaTimeOutput {
    /**
     * The value to be output (before formatting). If the formatted value is blank, no output is produced.
     */
    @Parameter(required = true)
    private Object value;

    /** The format to be applied to the object. */
    @Parameter(required = false)
    private DateTimeFormatter formatter;

    /** The format to be applied to the object. */
    @Parameter(required = false, defaultPrefix = BindingConstants.LITERAL)
    private String style;

    /** The format to be applied to the object. */
    @Parameter(required = false, defaultPrefix = BindingConstants.LITERAL)
    private String pattern;

    /** This is declared so we catch slip-ups - an error will point the developer to formatter instead. */
    @Parameter(required = false, defaultPrefix = BindingConstants.LITERAL)
    private Format format;

    /**
     * The element name, derived from the component template. This can even be overridden manually if desired (for
     * example, to sometimes render a surrounding element and other times not).
     */
    @Parameter("componentResources.elementName")
    private String elementName;

    @Inject
    private ComponentDefaultProvider defaultProvider;

    @Inject
    private ComponentResources componentResources;

    Binding defaultValue() {
        return defaultProvider.defaultBinding("value", componentResources);
    }

    void setupRender() {

        if (format != null) {
            throw new IllegalArgumentException(
                    "JodaTimeOutput does not allow \"format\" parameter.  Valid parameters are \"style\", \"formatter\", and \"pattern\".  Formatter type is DateTimeFormatter.");
        }

        int formatParams = 0;

        if (style != null) {
            formatParams += 1;
        }
        if (formatter != null) {
            formatParams += 1;
        }
        if (pattern != null) {
            formatParams += 1;
        }

        if (formatParams > 1) {
            throw new IllegalArgumentException(
                    "JodaTimeOutput can optionally receive \"style\" parameter, \"formatter\" parameter, or \"pattern\" parameter, but no more than one of them.  Received  "
                            + formatParams + " of them.");
        }

    }

    boolean beginRender(MarkupWriter writer) {

        String formatted = (value == null ? "" : format(value));

        if (InternalUtils.isNonBlank(formatted)) {
            if (elementName != null) {
                writer.element(elementName);

                componentResources.renderInformalParameters(writer);
            }

            writer.write(formatted);

            if (elementName != null)
                writer.end();
        }

        return false;
    }

    private String format(Object value) {
        String formatted = "";

        if (value != null) {

            // If value is an AbstractInstant - includes DateTime and DateMidnight

            if (value instanceof AbstractInstant) {
                AbstractInstant ai = ((AbstractInstant) value);
                if (style != null) {
                    formatted = DateTimeFormat.forStyle(style).print(ai);
                }
                else if (formatter != null) {
                    formatted = ai.toString(formatter);
                }
                else if (pattern != null) {
                    formatted = DateTimeFormat.forPattern(pattern).print(ai);
                }
                else {
                    formatted = value.toString();
                }
            }

            // Else if value is an AbstractPartial - includes LocalDate, LocalTime,
            // LocalDateTime, YearMonthDay, and TimeOfDay

            else if (value instanceof AbstractPartial) {
                AbstractPartial ap = ((AbstractPartial) value);
                if (style != null) {
                    formatted = DateTimeFormat.forStyle(style).print(ap);
                }
                else if (formatter != null) {
                    formatted = ap.toString(formatter);
                }
                else if (pattern != null) {
                    formatted = DateTimeFormat.forPattern(pattern).print(ap);
                }
                else {
                    formatted = value.toString();
                }
            }

            // Else value is an unsupported type

            else {
                throw new IllegalArgumentException(
                        "JodaTimeOutput received a value of the wrong type.  Supported types are subclasses of AbstractInstant and AbstractPartial.  Type found is "
                                + value.getClass().getName() + ".");
            }
        }

        return formatted;
    }

    // For testing.

    void setup(Object value, String style, DateTimeFormatter formatter, String pattern, String elementName,
            ComponentResources componentResources) {
        this.value = value;
        this.style = style;
        this.formatter = formatter;
        this.pattern = pattern;
        this.elementName = elementName;
        this.componentResources = componentResources;
    }
}