All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy