/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.display.internal;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.context.Execution;
import org.xwiki.model.EntityType;
import org.xwiki.model.ModelContext;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.EntityReferenceProvider;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.parser.ParseException;
import org.xwiki.rendering.parser.Parser;
import org.xwiki.rendering.util.ParserUtils;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.velocity.VelocityEngine;
import org.xwiki.velocity.VelocityManager;
/**
* Displays the title of a document.
*
* @version $Id: b09f692301d048a264a3c72d7d585ff997bf195b $
* @since 3.2M3
*/
public abstract class AbstractDocumentTitleDisplayer implements DocumentDisplayer
{
/**
* The key used to store on the XWiki context map the stack of references to documents whose titles are currently
* being evaluated (in the current execution context). This stack is used to prevent infinite recursion, which can
* happen if the title displayer is called on the current document from the title field or from a script within the
* first content heading.
*/
private static final String DOCUMENT_REFERENCE_STACK_KEY = "internal.displayer.title.documentReferenceStack";
/**
* The object used for logging.
*/
@Inject
private Logger logger;
/**
* The component used to parse the rendered title into an XDOM.
*/
@Inject
@Named("plain/1.0")
private Parser plainTextParser;
/**
* The component used to get the Velocity Engine and the Velocity Context needed to evaluate the Velocity script
* from the document title.
*/
@Inject
private VelocityManager velocityManager;
/**
* The component used to get the current document reference.
*/
@Inject
private DocumentAccessBridge documentAccessBridge;
/**
* The component used to serialize entity references.
*/
@Inject
private EntityReferenceSerializer<String> defaultEntityReferenceSerializer;
/**
* Execution context handler, needed for accessing the XWiki context map.
*/
@Inject
private Execution execution;
@Inject
@Named("xwikicfg")
private ConfigurationSource xwikicfg;
@Inject
private AuthorizationManager authorizationManager;
/**
* Used to get the default document reference, which normally is used to represent the home page of a space.
*
* @see #getStaticTitle(DocumentModelBridge)
*/
@Inject
private EntityReferenceProvider defaultEntityReferenceProvider;
@Inject
private ModelContext modelContext;
/**
* Used to emulate an in-line parsing.
*/
private ParserUtils parserUtils = new ParserUtils();
@Override
public XDOM display(DocumentModelBridge document, DocumentDisplayerParameters parameters)
{
// Protect against infinite recursion which can happen for instance if the document title displayer is called on
// the current document from the title field or from a script within the first content heading.
Map<Object, Object> xwikiContext = getXWikiContextMap();
@SuppressWarnings("unchecked")
Stack<DocumentReference> documentReferenceStack =
(Stack<DocumentReference>) xwikiContext.get(DOCUMENT_REFERENCE_STACK_KEY);
if (documentReferenceStack == null) {
documentReferenceStack = new Stack<DocumentReference>();
xwikiContext.put(DOCUMENT_REFERENCE_STACK_KEY, documentReferenceStack);
} else if (documentReferenceStack.contains(document.getDocumentReference())) {
logger.warn("Infinite recursion detected while displaying the title of [{}]. "
+ "Using the document name as title.", document.getDocumentReference());
return getStaticTitle(document);
}
documentReferenceStack.push(document.getDocumentReference());
try {
return displayTitle(document, parameters);
} finally {
documentReferenceStack.pop();
}
}
private XDOM displayTitle(DocumentModelBridge document, DocumentDisplayerParameters parameters)
{
// 1. Try to use the title provided by the user.
String rawTitle = document.getTitle();
if (!StringUtils.isEmpty(rawTitle)) {
try {
String title = rawTitle;
// Evaluate the title only if the document has script rights, otherwise use the raw title.
if (authorizationManager.hasAccess(Right.SCRIPT, document.getContentAuthorReference(),
document.getDocumentReference())) {
title = evaluateTitle(rawTitle, document.getDocumentReference(), parameters);
}
return parseTitle(title);
} catch (Exception e) {
logger.warn("Failed to interpret title of document [{}].", document.getDocumentReference(), e);
}
}
// 2. Try to extract the title from the document content.
if ("1".equals(this.xwikicfg.getProperty("xwiki.title.compatibility", "0"))) {
try {
XDOM title = extractTitleFromContent(document, parameters);
if (title != null) {
return title;
}
} catch (Exception e) {
logger.warn("Failed to extract title from content of document [{}].", document.getDocumentReference(),
e);
}
}
// 3. The title was not specified or its evaluation failed. Use the document name as a fall-back.
return getStaticTitle(document);
}
/**
* Parses the given title as plain text and returns the generated XDOM.
*
* @param title the title to be parsed
* @return the XDOM generated from parsing the title as plain text
*/
protected XDOM parseTitle(String title)
{
try {
XDOM xdom = plainTextParser.parse(new StringReader(title));
parserUtils.removeTopLevelParagraph(xdom.getChildren());
return xdom;
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* Evaluates the Velocity script from the specified title.
*
* @param title the title to evaluate
* @param documentReference a reference to the document whose title is evaluated
* @param parameters display parameters
* @return the result of evaluating the Velocity script from the given title
*/
protected String evaluateTitle(String title, DocumentReference documentReference,
DocumentDisplayerParameters parameters)
{
StringWriter writer = new StringWriter();
String namespace =
defaultEntityReferenceSerializer.serialize(parameters.isTransformationContextIsolated() ? documentReference
: documentAccessBridge.getCurrentDocumentReference());
// Get the velocity engine
VelocityEngine velocityEngine;
try {
velocityEngine = this.velocityManager.getVelocityEngine();
} catch (Exception e) {
throw new RuntimeException(e);
}
// Execute Velocity code
Map<String, Object> backupObjects = null;
boolean canPop = false;
EntityReference currentWikiReference = this.modelContext.getCurrentEntityReference();
try {
if (parameters.isExecutionContextIsolated()) {
backupObjects = new HashMap<String, Object>();
// The following method call also clones the execution context.
documentAccessBridge.pushDocumentInContext(backupObjects, documentReference);
// Pop the document from the context only if the push was successful!
canPop = true;
// Make sure to synchronize the context wiki with the context document's wiki.
modelContext.setCurrentEntityReference(documentReference.getWikiReference());
}
velocityEngine
.evaluate(velocityManager.getVelocityContext(), writer, namespace, title);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (canPop) {
documentAccessBridge.popDocumentFromContext(backupObjects);
// Also restore the context wiki.
this.modelContext.setCurrentEntityReference(currentWikiReference);
}
}
return writer.toString();
}
/**
* @return the XWiki context map
*/
@SuppressWarnings("unchecked")
private Map<Object, Object> getXWikiContextMap()
{
return (Map<Object, Object>) execution.getContext().getProperty("xwikicontext");
}
/**
* Extracts the title from the document content.
*
* @param document the document to extract the title from
* @param parameters display parameters
* @return the title XDOM
* @deprecated since 7.0M1
*/
@Deprecated
protected abstract XDOM extractTitleFromContent(DocumentModelBridge document,
DocumentDisplayerParameters parameters);
/**
* @param document an XWiki document
* @return the title used as a fall-back when the dynamic title cannot be evaluated
*/
private XDOM getStaticTitle(DocumentModelBridge document)
{
String documentName = document.getDocumentReference().getName();
if (defaultEntityReferenceProvider.getDefaultReference(EntityType.DOCUMENT).getName().equals(documentName)) {
// This document represents a space (it is the home page of a space). Use the space name instead.
documentName = document.getDocumentReference().getParent().getName();
}
return parseTitle(documentName);
}
/**
* @return the object used for logging
*/
protected Logger getLogger()
{
return logger;
}
}