/*
* 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 antlr.ANTLRException;
import antlr.RecognitionException;
import org.jboss.seam.Component;
import org.jboss.seam.core.Events;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;
import org.jboss.seam.ui.util.JSF;
import org.jboss.seam.wiki.core.model.WikiFile;
import org.jboss.seam.wiki.core.model.WikiUploadImage;
import org.jboss.seam.wiki.core.model.WikiTextMacro;
import org.jboss.seam.wiki.core.wikitext.renderer.DefaultWikiTextRenderer;
import org.jboss.seam.wiki.util.WikiUtil;
import org.jboss.seam.wiki.core.plugin.WikiPluginMacro;
import org.jboss.seam.wiki.core.wikitext.editor.WikiFormattedTextValidator;
import org.jboss.seam.wiki.core.wikitext.engine.WikiLink;
import org.jboss.seam.wiki.core.wikitext.engine.WikiLinkResolver;
import org.jboss.seam.wiki.core.wikitext.engine.WikiTextParser;
import org.jboss.seam.wiki.core.wikitext.renderer.WikiTextRenderer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Uses WikiTextParser and WikiLinkResolver to render Seam Text markup with wiki links.
*
* <p>
* Any lexer/parser error results in WARN level log message, you can disable this in your logging
* configuration by raising the log level for this class to ERROR.
* </p>
*
* @author Christian Bauer
*/
public class UIWikiFormattedText extends UIOutput {
Log log = Logging.getLog(UIWikiFormattedText.class);
public static final String ATTR_LINK_STYLE_CLASS = "linkStyleClass";
public static final String ATTR_BROKEN_LINK_STYLE_CLASS = "brokenLinkStyleClass";
public static final String ATTR_ATTACHMENT_LINK_STYLE_CLASS = "attachmentLinkStyleClass";
public static final String ATTR_THUMBNAIL_LINK_STYLE_CLASS = "thumbnailLinkStyleClass";
public static final String ATTR_INTERNAL_TARGET_FRAME = "internalTargetFrame";
public static final String ATTR_EXTERNAL_TARGET_FRAME = "externalTargetFrame";
public static final String ATTR_LINK_BASE_FILE = "linkBaseFile";
public static final String ATTR_CURRENT_AREA_NUMBER = "currentAreaNumber";
public static final String ATTR_ENABLE_MACRO_RENDERING = "enableMacroRendering";
public static final String ATTR_ENABLE_TRANSIENT_MACROS = "enableTransientMacros";
private Map<Integer, WikiPluginMacro> macrosWithTemplateByPosition;
public static final String COMPONENT_FAMILY = "org.jboss.seam.wiki.core.ui.UIWikiFormattedText";
public static final String COMPONENT_TYPE = "org.jboss.seam.wiki.core.ui.UIWikiFormattedText";
public UIWikiFormattedText() {
super();
macrosWithTemplateByPosition = new HashMap<Integer, WikiPluginMacro>();
}
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
@Override
public boolean getRendersChildren() {
return true;
}
@Override
public String getRendererType() {
return null;
}
@Override
public void encodeBegin(FacesContext facesContext) throws IOException {
if (!isRendered() || getValue() == null) return;
log.debug(">>> ENCODE BEGIN of WikiFormattedText component");
// Use the WikiTextParser to resolve macros
WikiTextParser parser = new WikiTextParser((String) getValue(), true, true);
// Resolve the base document and directory we are resolving against
final WikiFile baseFile = (WikiFile)getAttributes().get(ATTR_LINK_BASE_FILE);
final Long currentAreaNumber = (Long)getAttributes().get(ATTR_CURRENT_AREA_NUMBER);
parser.setCurrentAreaNumber(currentAreaNumber);
parser.setResolver((WikiLinkResolver)Component.getInstance("wikiLinkResolver"));
// TODO: Externalize this to separate class, extensible
// Set a customized renderer for parser macro callbacks
class WikiFormattedTextRenderer extends DefaultWikiTextRenderer {
@Override
public String renderInternalLink(WikiLink internalLink) {
return "<a href=\""
+ (
internalLink.isBroken()
? internalLink.getUrl()
: wikiURLRenderer.renderURL(internalLink.getFile())
)
+ (
internalLink.getFragment() != null
? "#"+internalLink.getEncodedFragment()
: ""
)
+ "\" target=\""
+ (getAttributes().get(ATTR_INTERNAL_TARGET_FRAME) != null ? getAttributes().get(ATTR_INTERNAL_TARGET_FRAME) : "")
+ "\" class=\""
+ (internalLink.isBroken() ? getAttributes().get(ATTR_BROKEN_LINK_STYLE_CLASS)
: getAttributes().get(ATTR_LINK_STYLE_CLASS)) + "\">"
+ internalLink.getDescription() + "</a>";
}
@Override
public String renderExternalLink(WikiLink externalLink) {
return "<a href=\""
+ WikiUtil.escapeEmailURL(externalLink.getUrl())
+ "\" target=\""
+ (getAttributes().get(ATTR_EXTERNAL_TARGET_FRAME) != null ? getAttributes().get(ATTR_EXTERNAL_TARGET_FRAME) : "")
+ "\" class=\""
+ (externalLink.isBroken() ? getAttributes().get(ATTR_BROKEN_LINK_STYLE_CLASS)
: getAttributes().get(ATTR_LINK_STYLE_CLASS)) + "\">"
+ WikiUtil.escapeEmailURL(externalLink.getDescription()) + "</a>";
}
@Override
public String renderFileAttachmentLink(int attachmentNumber, WikiLink attachmentLink) {
return "<a href=\""
+ wikiURLRenderer.renderURL(baseFile)
+ "#attachment" + attachmentNumber
+ "\" target=\""
+ (getAttributes().get(ATTR_INTERNAL_TARGET_FRAME) != null ? getAttributes().get(ATTR_INTERNAL_TARGET_FRAME) : "")
+ "\" class=\""
+ getAttributes().get(ATTR_ATTACHMENT_LINK_STYLE_CLASS) + "\">"
+ attachmentLink.getDescription() + "[" + attachmentNumber + "]" + "</a>";
}
@Override
public String renderThumbnailImageLink(WikiLink link) {
// TODO: This is not typesafe and clean, need different rendering strategy for WikiUpload subclasses
WikiUploadImage image = (WikiUploadImage)link.getFile();
if (image.getThumbnail() == WikiUploadImage.Thumbnail.FULL.getFlag()) {
// Full size display, no thumbnail
//TODO: Make sure we really don't need this - but it messes up the comment form conversation:
//String imageUrl = WikiUtil.renderURL(image) + "&cid=" + Conversation.instance().getId();
String imageUrl = wikiURLRenderer.renderURL(image);
return "<img src='"+ imageUrl + "'" +
" width='"+ image.getSizeX()+"'" +
" height='"+ image.getSizeY() +"'/>";
} else {
// Thumbnail with link display
//TODO: Make sure we really don't need this - but it messes up the comment form conversation:
// String thumbnailUrl = WikiUtil.renderURL(image) + "&thumbnail=true&cid=" + Conversation.instance().getId();
String thumbnailUrl = wikiURLRenderer.renderURL(image) + "?thumbnail=true";
return "<a href=\""
+ (link.isBroken() ? link.getUrl() : wikiURLRenderer.renderURL(image))
+ "\" target=\""
+ (getAttributes().get(ATTR_INTERNAL_TARGET_FRAME) != null ? getAttributes().get(ATTR_INTERNAL_TARGET_FRAME) : "")
+ "\" class=\""
+ getAttributes().get(ATTR_THUMBNAIL_LINK_STYLE_CLASS) + "\"><img src=\""
+ thumbnailUrl + "\"/></a>";
}
}
@Override
public String renderMacro(WikiTextMacro macro) {
WikiPluginMacro pluginMacroWithTemplate = macrosWithTemplateByPosition.get(macro.getPosition());
if (pluginMacroWithTemplate == null) {
log.debug("macro does not have an XHTML template/include, skipping: " + macro);
return "";
}
log.debug("firing BEFORE_VIEW_RENDER macro event");
Events.instance().raiseEvent(
pluginMacroWithTemplate.getCallbackEventName(WikiPluginMacro.CallbackEvent.BEFORE_VIEW_RENDER),
pluginMacroWithTemplate
);
log.debug("preparing include rendering for macro: " + pluginMacroWithTemplate);
UIComponent child = findComponent( pluginMacroWithTemplate.getClientId() );
log.debug("JSF child client identifier: " + child.getClientId(getFacesContext()));
ResponseWriter originalResponseWriter = getFacesContext().getResponseWriter();
StringWriter stringWriter = new StringWriter();
ResponseWriter tempResponseWriter = originalResponseWriter
.cloneWithWriter(stringWriter);
getFacesContext().setResponseWriter(tempResponseWriter);
try {
log.debug("rendering template of macro: " + pluginMacroWithTemplate);
JSF.renderChild(getFacesContext(), child);
log.debug("firing AFTER_VIEW_RENDER macro event");
Events.instance().raiseEvent(
pluginMacroWithTemplate.getCallbackEventName(WikiPluginMacro.CallbackEvent.AFTER_VIEW_RENDER),
pluginMacroWithTemplate
);
}
catch (Exception ex) {
throw new RuntimeException(ex);
} finally {
getFacesContext().setResponseWriter(originalResponseWriter);
}
return stringWriter.getBuffer().toString();
}
@Override
public void setAttachmentLinks(List<WikiLink> attachmentLinks) {
// Put attachments (wiki links...) into the event context for later rendering
String contextVariable = "attachmentLinksByWikiFile";
// We need to key them by WikiFile identifier, so we put a map in the context
Map<Long, List<WikiLink>> linksByFile;
if ((linksByFile = (Map)Contexts.getEventContext().get(contextVariable)) == null) {
linksByFile = new HashMap();
}
// This method may be called multiple times when we render a "base file", e.g. header, content, footer, comments
List<WikiLink> linksForBaseFile;
if ((linksForBaseFile = linksByFile.get(baseFile.getId())) != null) {
// Aggregate all links for a base file, don't reset them on each render pass for the same base file
linksForBaseFile.addAll(attachmentLinks);
} else {
linksForBaseFile = attachmentLinks;
}
linksByFile.put(baseFile.getId(), linksForBaseFile);
Contexts.getEventContext().set(contextVariable, linksByFile);
}
@Override
public void setExternalLinks(List<WikiLink> externalLinks) {
// Put external links (to targets not on this wiki) into the event context for later rendering
// TODO: This is actually never used anywhere... nobody is rendering a list of "external links"
// We COULD use this to render a list of links in a print view (like coWiki)
/*
String contextVariable = "wikiTextExternalLinks";
List<WikiLink> contextLinks = (List<WikiLink>)Contexts.getEventContext().get(contextVariable);
if (contextLinks == null || contextLinks.size()==0) {
Contexts.getEventContext().set(contextVariable, externalLinks);
}
*/
}
@Override
protected String getHeadlineId(Headline h, String headline) {
// HTML id attribute has restrictions on valid values... so the easiest way is to make this a WikiLink
return HEADLINE_ID_PREFIX+WikiUtil.convertToWikiName(headline);
// We also need to access it correctly, see WikiLink.java and getHeadLineLink()
}
@Override
protected String getHeadlineLink(Headline h, String headline) {
return "<a href=\""+ wikiURLRenderer.renderURL(baseFile)+"#"+ WikiTextRenderer.HEADLINE_ID_PREFIX+WikiUtil.convertToWikiName(headline)+"\">"
+ headline
+"</a>";
}
}
parser.setRenderer(new WikiFormattedTextRenderer());
try {
log.debug("parsing wiki text for HTML encoding");
parser.parse();
} catch (RecognitionException rex) {
// Log a nice message for any lexer/parser errors, users can disable this if they want to
log.warn( WikiFormattedTextValidator.getErrorMessage((String) getValue(), rex) );
} catch (ANTLRException ex) {
// All other errors are fatal;
throw new RuntimeException(ex);
}
facesContext.getResponseWriter().write(parser.toString());
log.debug("<<< ENCODE END of WikiFormattedText component");
}
protected void addMacroWithTemplate(WikiPluginMacro pluginMacro) {
macrosWithTemplateByPosition.put(pluginMacro.getPosition(), pluginMacro);
}
}