![JAR search and dependency download from the Maven repository](/logo.png)
com.jsftoolkit.base.renderer.HtmlRenderer Maven / Gradle / Ivy
package com.jsftoolkit.base.renderer;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.Map.Entry;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.context.ResponseWriterWrapper;
import javax.faces.render.Renderer;
import org.xml.sax.SAXException;
import com.jsftoolkit.base.HeadInsert;
import com.jsftoolkit.base.ResourceInfo;
import com.jsftoolkit.gen.info.DecodeInfo;
import com.jsftoolkit.utils.NullWriter;
import com.jsftoolkit.utils.Utils;
import com.jsftoolkit.utils.xmlpull.AttributeEvent;
import com.jsftoolkit.utils.xmlpull.BodyText;
import com.jsftoolkit.utils.xmlpull.EndElement;
import com.jsftoolkit.utils.xmlpull.PullEvent;
import com.jsftoolkit.utils.xmlpull.PullEventSource;
import com.jsftoolkit.utils.xmlpull.StartElement;
/**
* Base class for renderers. An {@link HtmlRenderer} instance parses an XHTML
* fragment when it is created, and when asked to render, it replays the SAX
* events received to the {@link javax.faces.context.ResponseWriter},
* substituting component properties for occurrences of ${propertyName} in
* attributes and body text.
*
* e.g. You might give the constructor a template like:
*
* <div id="${id}">Hello ${name|World}!</div>
* <hr/>
*
* A few things about the template:
*
* - It may contain more than one root element, other than that exception, it
* must be well formed XML.
*
- It uses ${expressions} to insert component properties.
*
- A default value for an expression can be specified using the form
* ${propertyName|Default Value}.
*
*
* The above template would render something like:
*
* <div id="foo">Hello Bob!</div>
<hr/>
*
* Assuming that the component's name and id properties evaluated to Bob and foo
* respectively.
*
* You can also render child components:
*
* <div>Before Children <children/> After Children</div>
*
* When combined with a component that has the child <h:outputText
* value="Children"/> produces:
*
*
* <div>Before Children Children! After Children</div>
*
*
* Note that children elements following the first will be ignored. This is not
* a renderer for {@link UIData} or a {@link NamingContainer}, so rendering
* child components more than once would cause there to be duplicate ids in the
* output.
*
* Lastly, it may be desirable to allow the user to specify what type of tag is
* output by your component. e.g. render a li instead of a div. You can
* accomplish this by including a tag, where the name is prefixed with 'tag-'
* and the rest of the name is the default tag, e.g.
*
*
* <tag-div>Foo</tag-div>
*
*
* If the component has the 'tag' attribute set to 'span', it would render:
*
*
* <span>Foo</span>
*
*
* Otherwise:
*
*
* <div>Foo</div>
*
*
Advanced Templates - Callbacks
* Sometimes a template wont be enough. It may be necessary to have finer
* grained control over certain parts of rendering. The good news is that you
* can still use {@link HtmlRenderer}.
*
* {@link HtmlRenderer} provides a {@link RenderCallback} mechanism to allow
* your code to take over at arbitrary points. Any tag name prefixed with
* {@link #CALLBACK_PREFIX} will be considered a callback. The name of the
* callback is the part of the string following the prefix. e.g. for 'call-foo',
* the callback name is 'foo'.
*
* You callback can do anything that a {@link Renderer} is allowed to do.
* However, because your callback may enclose template text, which cannot be
* directly modified/removed by the callback, a degree of 'creativity' may
* sometimes be necessary. For example, to prevent template text enclosed in the
* callback from being rendered,
* {@link RenderCallback#encodeBegin(FacesContext, UIComponent)} could call
* {@link #setNoOpResponseWriter(FacesContext)} and
* {@link RenderCallback#encodeEnd(FacesContext, UIComponent)} could call
* {@link #removeNoOpResponseWriter(FacesContext)}.
*
* Note that if your callback encloses the {@link #CHILDREN} tag, it is
* responsible for rendering the child components when
* {@link RenderCallback#encodeChildren(FacesContext, UIComponent)} is called.
*
*
Writing in the head element
* If your component is skinnable, has a default stylesheet, makes use of
* javascript, etc. You probably need to write things in the head element.
*
* The first thing you need to distinguish between is resources that need to be
* included only once, no matter how many occurrences of your component are in
* the page, and custom code (preferably just a single function invocation) that
* needs to be included for each occurrence. The former (script libraries and
* style sheets) should be included by adding an instance of
* {@link ResourceInfo} to {@link #resources} (or returning them from
* {@link #getResources(FacesContext, UIComponent)}). The later, head content
* that needs to be written out per instance, can be specified by passing
* {@link #headTemplate} to the constructor. {@link #headTemplate} is the same
* format as the normal component template, although the {@link #CHILDREN}
* element is ignored.
*
* @author noah
*
*/
public abstract class HtmlRenderer extends Renderer implements HeadInsert {
public static final String STYLE_CLASS = "styleClass";
protected DecodeInfo decodeInfo;
/**
* Convenience set of the render attribs. Unmodifiable.
*/
public static final Set PASS_THROUGH = Collections
.unmodifiableSet(Utils.asSet("dir", "lang", "style", STYLE_CLASS,
"title", "accesskey", "charset", "coords", "hreflang",
"onblur", "onclick", "ondblclick", "onfocus", "onkeydown",
"onkeyup", "onkeypress", "onmousedown", "onmousemove",
"onmouseout", "onmouseover", "onmouseup", "rel", "rev",
"shape", "tabindex", "target", "type", "nofollow", "for"));
/**
* Well qualified attribute name that the events iterator is saved under on
* the component.
*/
private static final String STATE = "com.jsftoolkit.base.renderer.HtmlRenderer.STATE";
/**
* Name of the tag that will be replaced with the component's children.
*/
public static final String CHILDREN = "children";
/**
* Component attribute that provides the name of the tag to be rendered for
* this component.
*/
public static final String TAG = "tag";
/**
* Prefix for tag names that may be set by the user.
*/
public static final String TAG_PREFIX = "tag-";
/**
* Prefix for tags that indicate a callback should be invoked.
*/
public static final String CALLBACK_PREFIX = "call-";
// record of the events created by parsing the template
private PullEventSource template;
// set of resources for RequiresResources
protected Set resources;
// map of render callbacks
private Map callbacks = new HashMap();
private RenderEventsCollector headTemplate;
/**
* Creates a renderer that requires no resources and no head template.
*
* @param template
* the template text
* @throws IOException
* @throws SAXException
*/
@SuppressWarnings("unchecked")
public HtmlRenderer(String template) throws IOException, SAXException {
this(template, Collections.EMPTY_SET);
}
/**
* Creates a renderer that has no head template.
*
* @param template
* @param resources
* @throws IOException
* @throws SAXException
*/
public HtmlRenderer(String template, Set resources)
throws IOException, SAXException {
this(template, null, resources);
}
/**
* Creates a renderer that requires the given resources.
*
* @param template
* the template text
* @param headTemplate
* the text to inject into head (per instance)
* @param resources
* the resources that need to be included in the head element
* (for all instances)
* @throws IOException
* @throws SAXException
*/
public HtmlRenderer(String template, String headTemplate,
Set resources) throws IOException, SAXException {
super();
this.template = new RenderEventsCollector(template);
this.resources = resources;
this.headTemplate = new RenderEventsCollector(headTemplate);
}
/**
* Registers the given renderer as a {@link RenderCallback}
*
* @param name
* @param renderer
* @return
*/
public RenderCallback registerCallback(String name, Renderer renderer) {
return registerCallback(name, new RendererCallbackAdaptor(renderer));
}
/**
* Registers the given callback. See the class comment for more information.
*
* @param name
* @param callback
* @return
*/
public RenderCallback registerCallback(String name, RenderCallback callback) {
return callbacks.put(name, callback);
}
/**
* Eliminates the ambiguity between
* {@link #registerCallback(String, RenderCallback)} and
* {@link #registerCallback(String, Renderer)}.
*
* @param name
* @param callback
* @return
*/
public RenderCallback registerCallback(String name,
AbstractRenderCallback callback) {
return registerCallback(name, (RenderCallback) callback);
}
/**
* If {@link #setDecodeInfo(String, String[], String[])} or
* {@link #setDecodeInfo(String)} was called, sets the request parameter
* specified as the component's submitted value.
*/
@Override
public void decode(FacesContext context, UIComponent component) {
super.decode(context, component);
if (decodeInfo != null) {
String[] props = decodeInfo.getProps();
String[] defaults = decodeInfo.getDefaults();
Object[] values = new Object[props.length];
for (int i = 0; i < props.length; i++) {
values[i] = getProperty(context, component, props[i], Utils
.get(defaults, i));
}
String param = String.format(decodeInfo.getFormat(), values);
((EditableValueHolder) component).setSubmittedValue(context
.getExternalContext().getRequestParameterMap().get(param));
}
}
/**
*
* @param param
* the name of the request parameter to decode
*/
public void setDecodeInfo(String param) {
setDecodeInfo(param, new String[] {}, new String[] {});
}
/**
*
* @param paramFormat
* the format string to evaluate to get the request parameter
* @param props
* the component properties to evaluate and pass to format
* @param defaults
* the default values if the properties are null
*/
public void setDecodeInfo(String paramFormat, String[] props,
String[] defaults) {
this.decodeInfo = new DecodeInfo(props, defaults, paramFormat);
}
/**
*
* @param context
* @param component
* @return a set of {@link ResourceInfo} instances describing the resources
* this component needs. They will be included in iteration order,
* so if order is significant, use a {@link LinkedHashSet}.
*/
protected Set getResources(FacesContext context,
UIComponent component) {
return resources;
}
/**
* Takes care of writing the script and stylesheet includes provided by
* {@link #getResources(FacesContext, UIComponent)}.
*
* If {@link #headTemplate} was specified, it will be written out after the
* includes.
*
* For subclasses: function libraries and style sheets should be included by
* adding them to {@link #resources}. Only code that needs to be invoked
* per component should be written directly to head.
*
* Naturally everything should be well qualified to avoid naming collisions
* with other component's library code. TODO add a link to recommendations
* about how to achieve this.
*
* @see HeadInsert#encodeHead(FacesContext, UIComponent, Set)
*/
public void encodeHead(FacesContext context, UIComponent component,
Set resourceIds) throws IOException {
// link the scripts and styles, if necessary
Set resources = getResources(context, component);
ResourceUtils.writeIncludes(context, component, resources, resourceIds);
if (headTemplate != null) {
Iterator it = headTemplate.iterator();
while (it.hasNext()) {
process(new RenderingState(it), context, component);
}
}
}
/**
* @see Renderer#encodeBegin(FacesContext, UIComponent)
*
* Renders the template up to the children element. If there is no children
* element, then the entire template is rendered in this phase.
*/
@Override
public void encodeBegin(FacesContext context, UIComponent component)
throws IOException {
super.encodeBegin(context, component);
RenderingState state = new RenderingState(template.iterator());
// save the state on the component, to be retrieved when we encodeEnd.
component.getAttributes().put(STATE, state);
// process the render events
process(state, context, component);
}
/**
* Encodes the child components. Behavior varies depending on the template.
* If the template contains a {@link #CHILDREN} tag, then this method will
* be called when the children tag is encountered. If it does not contain a
* children tag, this method will be called after the template text has been
* rendered.
*
* If a callback tag encloses the {@link #CHILDREN} tag, then the callback's
* {@link RenderCallback#encodeChildren(FacesContext, UIComponent)} method
* will be called when this method is invoked.
*
* @see RenderCallback#encodeChildren(FacesContext, UIComponent)
*/
@Override
@SuppressWarnings("unchecked")
public void encodeChildren(FacesContext context, UIComponent component)
throws IOException {
RenderingState state = (RenderingState) component.getAttributes().get(
STATE);
RenderCallback callback;
// if there is a callback registered that encloses the children, have it
// encode the children, otherwise have the children render themselves.
if (state.callbackStack.size() > 0
&& (callback = callbacks.get(state.callbackStack.peek())) != null) {
callback.encodeChildren(context, component);
} else {
super.encodeChildren(context, component);
}
}
/**
* Encodes anything following the {@link #CHILDREN} tag, if it exists.
* Otherwise, this method should have no effect.
*/
@Override
@SuppressWarnings("unchecked")
public void encodeEnd(FacesContext context, UIComponent component)
throws IOException {
super.encodeEnd(context, component);
RenderingState state = (RenderingState) component.getAttributes()
.remove(STATE);
// keep going until we process all the events, even if processing stops
// in the middle for some reason
while (state.events.hasNext()) {
process(state, context, component);
}
}
/**
* Returns true.
*
* @see {@link #encodeChildren(FacesContext, UIComponent)}
*/
@Override
public boolean getRendersChildren() {
return true;
}
/**
* Does the rendering work. Events are processed until there are no more
* events, or a {@link #CHILDREN} element is encountered.
*
* @param state
* @param context
* @param component
* @throws IOException
*/
protected void process(RenderingState state, FacesContext context,
final UIComponent component) throws IOException {
if (state == null) {
return;
}
Map attribs = component.getAttributes();
Iterator events = state.events;
// label this loop so we can easily break out of it when we encounter a
// children element
process: while (events.hasNext()) {
PullEvent event = events.next();
// respect decoration, reload the response writer for every event
ResponseWriter writer = context.getResponseWriter();
// for efficiency, switch on the event type
// XXX this may be a preemptive optimization as the compiler should
// be able to optimize an if-else-instanceof into the same thing
switch (event.getType()) {
case StartElement.TYPE: {
StartElement sEv = (StartElement) event;
String name = sEv.getName();
name = getTag(attribs, name);
if (CHILDREN.equalsIgnoreCase(name)) {
// break if we hit the children tag
break process;
}
RenderCallback callback = getCallback(state, context,
component, name);
if (callback == null) {
// otherwise, write the start tag
writer.startElement(name, component);
} else {
// invoke the callback
callback.encodeBegin(context, component);
}
}
break;
case AttributeEvent.TYPE: {
AttributeEvent ae = (AttributeEvent) event;
writer.writeAttribute(ae.getName(), ae.getValue(), null);
}
break;
case PassThrough.TYPE: {
PassThrough pt = (PassThrough) event;
writePassThroughAttribs(writer, attribs, pt.getAllowed());
}
break;
case VarAttribEvent.TYPE: {
// write an attribute that embeds 1 or more component property
// values
VarAttribEvent vae = (VarAttribEvent) event;
List props = vae.getProperties();
List defaults = vae.getDefaultValues();
Object[] values = new Object[props.size()];
for (int i = 0; i < props.size(); i++) {
values[i] = getProperty(context, component, props.get(i),
Utils.toString(defaults.get(i)));
}
String text = String.format(vae.getPattern(), values);
// don't render empty attributes (means they were null)
if (!Utils.isEmpty(text)) {
writer.writeAttribute(vae.getName(), text, props.get(0));
}
}
break;
case BodyText.TYPE: {
// write plain text
BodyText bte = (BodyText) event;
if (bte.getText() != null) {
writer.writeText(bte.getText(), null);
}
}
break;
case VarTextEvent.TYPE: {
// write text from a component property
VarTextEvent vte = (VarTextEvent) event;
Object text = getProperty(context, component,
vte.getProperty(), vte.getDefaultValue());
if (text != null) {
writer.writeText(text, vte.getProperty());
}
}
break;
case EndElement.TYPE: {
// close an element tag
EndElement e = (EndElement) event;
String name = getTag(attribs, e.getName());
RenderCallback callback = getCallback(state, context,
component, name);
if (!CHILDREN.equalsIgnoreCase(name)) {
if (callback == null) {
writer.endElement(name);
} else {
// the XML template would not have parsed if it was not
// well formed, so there must be a closing callback tag
assert state.callbackStack.size() > 0;
assert callback == callbacks.get(state.callbackStack
.pop());
callback.encodeEnd(context, component);
}
}
}
break;
case DecodeEvent.TYPE:
// nothing to do for a decode event here
break;
default:
throw new IllegalArgumentException(
"Unknown event type for event " + event);
}
}
}
protected RenderCallback getCallback(RenderingState state,
FacesContext context, final UIComponent component, String name)
throws IOException {
// see if the tag is a callback
if (name.startsWith(CALLBACK_PREFIX)) {
// lookup the callback from the registered callbacks
String realName = name.substring(CALLBACK_PREFIX.length());
state.callbackStack.push(realName);
return callbacks.get(realName);
}
return null;
}
protected Object getProperty(FacesContext context,
final UIComponent component, String property, String defaultValue) {
Object value;
// id must be handled differently
if ("id".equals(property)) {
value = component.getClientId(context);
} else {
value = Utils.getValue(component.getAttributes().get(property),
defaultValue);
}
return value;
}
/**
* Resolves the name of the tag from the component's attributes. See the
* class comment for details.
*
* @param attribs
* @param name
* the tag name
* @return the name the tag should use.
*/
public static String getTag(Map attribs, String name) {
if (name.startsWith(TAG_PREFIX)) {
name = (String) Utils.getValue(attribs.get(TAG), name
.substring(TAG_PREFIX.length()));
}
return name;
}
/**
* Writes an attribute for each entry in attribs that is also in allowed.
* Also converts styleClass to class (if it is allowed).
*
* @param writer
* the writer to use
* @param attribs
* the component's attributes
* @param allowed
* attributes to allow
* @throws IOException
*/
public static void writePassThroughAttribs(ResponseWriter writer,
Map attribs, Set allowed)
throws IOException {
if (attribs.containsKey(STYLE_CLASS) && allowed.contains(STYLE_CLASS)) {
writer.writeAttribute("class", attribs.get(STYLE_CLASS),
STYLE_CLASS);
}
// pass through the other attributes
for (Entry attrib : attribs.entrySet()) {
String key = attrib.getKey();
if (allowed.contains(key) && !STYLE_CLASS.equals(key)) {
writer.writeAttribute(key, attrib.getValue(), key);
}
}
}
/**
* Writes all the attributes defined in {@link #PASS_THROUGH}.
*
* @param writer
* @param attribs
* @throws IOException
*/
public static void writePassThroughAttribs(ResponseWriter writer,
Map attribs) throws IOException {
writePassThroughAttribs(writer, attribs, PASS_THROUGH);
}
private static class RenderingState {
public RenderingState(Iterator events) {
this.events = events;
}
public Iterator events;
public Stack callbackStack = new Stack();
}
/**
* {@link ResponseWriter} decorator. Redirects all output to a
* {@link NullWriter}.
*
* @author noah
*
*/
public static class NoOpResponseWriter extends ResponseWriterWrapper {
// the wrapper writer
private final ResponseWriter writer;
// the original response writer
private final ResponseWriter original;
public NoOpResponseWriter(ResponseWriter original) {
super();
this.original = original;
this.writer = original.cloneWithWriter(new NullWriter());
}
@Override
protected ResponseWriter getWrapped() {
return writer;
}
public ResponseWriter getOriginal() {
return original;
}
}
/**
* Decorates the current response writer with a {@link NoOpResponseWriter}.
* Can be reversed by calling
* {@link #removeNoOpResponseWriter(FacesContext)}.
*
* @param context
*/
public static void setNoOpResponseWriter(FacesContext context) {
context.setResponseWriter(new NoOpResponseWriter(context
.getResponseWriter()));
}
/**
* Removes a single {@link NoOpResponseWriter} decorating the response
* writer obtained from context. This method will need to be called multiple
* times if multiple {@link NoOpResponseWriter}s have been set.
*
* @param context
* @return true if a {@link NoOpResponseWriter} was removed, false if the
* response writer remained unchanged.
*/
public static boolean removeNoOpResponseWriter(FacesContext context) {
ResponseWriter responseWriter = context.getResponseWriter();
if (responseWriter instanceof NoOpResponseWriter) {
NoOpResponseWriter noop = (NoOpResponseWriter) responseWriter;
context.setResponseWriter(noop.getOriginal());
return true;
}
return false;
}
}