/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.wikitext.renderer.jsf;
import org.jboss.seam.wiki.core.wikitext.renderer.NullWikiTextRenderer;
import org.jboss.seam.wiki.core.plugin.PluginRegistry;
import org.jboss.seam.wiki.core.plugin.WikiPluginMacro;
import org.jboss.seam.wiki.core.plugin.metamodel.MacroPluginModule;
import org.jboss.seam.wiki.core.model.WikiTextMacro;
import org.jboss.seam.core.Events;
import org.jboss.seam.core.ResourceLoader;
import org.jboss.seam.core.Expressions;
import org.jboss.seam.Component;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;
import org.ajax4jsf.component.html.HtmlLoadStyle;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.io.IOException;
import java.net.URL;
import com.sun.facelets.FaceletContext;
import com.sun.facelets.el.VariableMapperWrapper;
import com.sun.facelets.tag.jsf.ComponentSupport;
import javax.faces.component.UIComponent;
import javax.el.VariableMapper;
/**
* Creates the included macro template components as first-class components in the JSF view
* as children of the <tt>UIWikiFormattedText</tt> component.
*
* <p>
* This routine parses the wiki text and for each encountered wiki macro, it tries to
* include an XHTML template. If no template is found, we do nothing. If a template is
* found, we also include its CSS into the document header, then add it to the parent
* component, the <tt>UIWikiFormattedText</tt> we are handling. This parent component
* keeps a map of <tt>WikiMacro</tt> instances, keyed by position in the rendered
* wiki text. This map of macros can be pulled out later, when we render the JSF view
* tree.
* </p>
* <p>
* Macros are never reentrant, that means a macro can not render itself. To avoid this,
* we push a macro onto a stack before including it in the component tree, after checking if
* it is already present on the stack. If it is already present, we log a warning and don't do
* anything. After rendering, we pop the stack. The stack is held in the PAGE context.
* </p>
* <p>
* This code is complicated, because Facelets is complicated. Do not touch it unless you
* are absolutely sure you know what you are doing.
* </p>
*
* @author Christian Bauer
*/
public class MacroIncludeTextRenderer extends NullWikiTextRenderer {
private Log log = Logging.getLog(WikiFormattedTextHandler.class);
public static final String MACRO_STACK_PAGE_VARIABLE = "macroStack";
// A collection of all macros (whether they have templates or not) that we found in this piece of wiki text
private Set<String> macrosFoundInWikiText = new HashSet<String>();
UIWikiFormattedText parent;
FaceletContext context;
boolean enableTransientMacros;
public MacroIncludeTextRenderer(UIWikiFormattedText parent, FaceletContext context, boolean enableTransientMacros) {
this.parent = parent;
this.context = context;
this.enableTransientMacros = enableTransientMacros;
}
@Override
public String renderMacro(WikiTextMacro wikiTextMacro) {
log.debug("=== found macro in wiki text: " + wikiTextMacro);
// Check reentrancy
if (!isMacroOnPageStack(wikiTextMacro)) {
log.debug("adding macro to page macro stack");
getPageMacroStack().push(wikiTextMacro);
} else {
log.warn("macros are not reentrant, duplicate macro on page stack: " + wikiTextMacro);
return null;
}
// Check if the wikiTextMacro actually is registered, we don't build unknown macros
WikiPluginMacro pluginMacro = PluginRegistry.instance().createWikiPluginMacro(wikiTextMacro);
if (pluginMacro == null) {
log.info("macro is not bound in plugin registry: " + wikiTextMacro);
getPageMacroStack().pop();
return null;
}
// Check if we can find the template to include for this wikiTextMacro
String macroIncludePath = getMacroIncludePath(pluginMacro);
if (macroIncludePath == null) {
getPageMacroStack().pop();
return null;
}
// Before we build the nested components, set the WikiMacro instance in the PAGE context under a
// unique name, so we can use a VariableMapper later and alias this as 'currentMacro'
String macroPageVariableName = pluginMacro.getPageVariableName();
log.debug("setting WikiMacro instance in PAGE context as variable named: " + macroPageVariableName);
Contexts.getPageContext().set(macroPageVariableName, pluginMacro);
// Whoever wants to do something before we finally build the XHTML template
log.debug("firing VIEW_BUILD macro event");
Events.instance().raiseEvent(pluginMacro.getCallbackEventName(WikiPluginMacro.CallbackEvent.VIEW_BUILD), pluginMacro);
// This is where the magic happens... the UIWikiFormattedText component should have one child after that, a UIMacro
includeMacroFacelet(pluginMacro, macroIncludePath, context, parent);
// Now get the identifier of the newly created UIMacro instance and set it for future use
Object macroId = parent.getAttributes().get(UIMacro.NEXT_MACRO);
if (macroId != null) {
pluginMacro.setClientId(macroId.toString());
parent.getAttributes().remove(UIMacro.NEXT_MACRO);
} else {
// Best guess based wikiTextMacro renderer, needed during reRendering when we don't build the child
// - only then is NEXT_MACRO set by the MacroComponentHandler
macroId =
parent.getChildren().get(
parent.getChildCount() - 1
).getClientId(context.getFacesContext());
pluginMacro.setClientId(macroId.toString());
}
// Put an optional CSS include in the header of the wiki document we are rendering in.
// (This needs to happen after the clientId is set, as CSS resource path rendering needs to
// know if it occurs in a JSF request (clientId present) or not.
includeMacroCSS(pluginMacro, parent);
// We need to make the UIMacro child transient if we run in the wiki text editor preview. The reason
// is complicated: If we don't make it transient, all value expressions inside the wikiTextMacro templates that
// use 'currentMacro' will refer to the "old" saved ValueExpression and then of course to the "old"
// VariableMapper. In other words: We need to make sure that the subtree is completely fresh every
// time the wiki text preview is reRendered, otherwise we never get a 'currentMacro' binding updated.
// This also means that VariableMapper is a completely useless construct, because it is basically an
// alias that is evaluated just once.
// Note: This means we can't click on form elements of any plugin/wikiTextMacro template in the preview. This
// should be solved by not showing/ghosting any form elements during preview.
if (enableTransientMacros) {
log.debug("setting macro to transient rendering, not storing its state between renderings: " + pluginMacro);
UIMacro uiMacro = (UIMacro) ComponentSupport.findChild(parent, macroId.toString());
uiMacro.setTransient(true);
}
// Finally, pop the wikiTextMacro stack of the page, then transport the finished WikiMacro instance into
// the UIWikiFormattedText component for rendering - we are done building the component tree at this
// point.
getPageMacroStack().pop();
parent.addMacroWithTemplate(pluginMacro);
// Well, we don't render anything here...
return null;
}
private String getMacroIncludePath(WikiPluginMacro pluginMacro) {
// Check singleton configuration
if (pluginMacro.getMetadata().isRenderOptionSet(MacroPluginModule.RenderOption.SINGLETON) &&
macrosFoundInWikiText.contains(pluginMacro.getName())) {
log.warn("macro is a SINGLETON, can not be used twice in the same document area: " + pluginMacro);
return null;
} else {
macrosFoundInWikiText.add(pluginMacro.getName());
}
// Check skin configuration
String currentSkin = (String) Component.getInstance("skin");
if (!pluginMacro.getMetadata().isAvailableForSkin(currentSkin)) {
log.warn("macro is not available for skin '" + currentSkin + "': " + pluginMacro);
return null;
}
// Try to get an XHTML template, our source for building nested components
// Fun with slashes: For some reason, Facelets really needs a slash at the start, otherwise
// it doesn't use my custom ResourceResolver...
String includePath = "/" + pluginMacro.getMetadata().getPlugin().getPackageDefaultTemplatePath(pluginMacro.getName());
URL faceletURL = ResourceLoader.instance().getResource(includePath);
if (faceletURL == null) {
log.debug("macro has no default include file, not building any components: " + pluginMacro);
return null;
} else {
log.debug("using default template include as a resource from package: " + includePath);
}
return includePath;
}
private void includeMacroFacelet(WikiPluginMacro pluginMacro, String includePath, FaceletContext ctx, UIComponent parent) {
VariableMapper orig = ctx.getVariableMapper();
try {
log.debug("setting 'currentMacro' as an EL variable, resolves dynamically to WikiMacro instance in PAGE context");
ctx.setVariableMapper(new VariableMapperWrapper(orig));
ctx.getVariableMapper().setVariable(
WikiPluginMacro.CURRENT_MACRO_EL_VARIABLE,
Expressions.instance().createValueExpression("#{" + pluginMacro.getPageVariableName() + "}").toUnifiedValueExpression()
);
log.debug("including macro facelets file from path: " + includePath);
ctx.includeFacelet(parent, includePath);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
ctx.setVariableMapper(orig);
}
}
private void includeMacroCSS(WikiPluginMacro pluginMacro, UIComponent cmp) {
String cssPath = "/" + pluginMacro.getMetadata().getPlugin().getPackageCSSPath() + "/" + pluginMacro.getName() + ".css";
log.debug("trying to load CSS resource from classpath: " + cssPath);
if (ResourceLoader.instance().getResource(cssPath) != null) {
String cssRequestURIPath = pluginMacro.getRequestStylesheetPath() + "/" + pluginMacro.getName() + ".css";
log.debug("including macro CSS file, rendering URI for document head: " + cssRequestURIPath);
// Use Ajax4JSF loader, it can do what we want - add a CSS <link> to the HTML <head>
HtmlLoadStyle style = new HtmlLoadStyle();
style.setSrc(cssRequestURIPath);
cmp.getChildren().add(style);
// Clear these out in the next build phase
ComponentSupport.markForDeletion(style);
} else {
log.debug("no CSS resource found for macro");
}
}
private Stack<WikiTextMacro> getPageMacroStack() {
if (Contexts.getPageContext().get(MACRO_STACK_PAGE_VARIABLE) == null) {
log.debug("macro page stack is null, creating new stack for this page");
Contexts.getPageContext().set(MACRO_STACK_PAGE_VARIABLE, new Stack<WikiTextMacro>());
}
return (Stack<WikiTextMacro>)Contexts.getPageContext().get(MACRO_STACK_PAGE_VARIABLE);
}
private boolean isMacroOnPageStack(WikiTextMacro macro) {
Stack<WikiTextMacro> macroStack = getPageMacroStack();
for (WikiTextMacro macroOnPageStack : macroStack) {
if (macroOnPageStack.getName().equals(macro.getName())) return true;
}
return false;
}
}