Commit 703f830b authored by Thomas Mortagne's avatar Thomas Mortagne
Browse files

XRENDERING-621: Make rendering errors translatables and extendables

parent d9500467
......@@ -36,6 +36,8 @@
import org.xwiki.rendering.block.GroupBlock;
import org.xwiki.rendering.block.VerbatimBlock;
import org.xwiki.rendering.block.WordBlock;
import org.xwiki.rendering.block.match.ClassBlockMatcher;
import org.xwiki.rendering.block.match.OrBlockMatcher;
import org.xwiki.rendering.listener.Format;
import org.xwiki.rendering.util.ErrorBlockGenerator;
......@@ -50,7 +52,7 @@
public class DefaultErrorBlockGenerator implements ErrorBlockGenerator
{
@Inject
private Logger logger;
protected Logger logger;
@Override
public List<Block> generateErrorBlocks(String message, String description, boolean isInline)
......@@ -79,8 +81,8 @@ public List<Block> generateErrorBlocks(String message, String description, boole
public List<Block> generateErrorBlocks(String messagePrefix, Throwable throwable, boolean isInline)
{
// Note: We're using ExceptionUtils.getRootCause(e).getMessage() instead of getRootCauseMessage()
// below because getRootCauseMessage() adds a technical prefix (the name of the exception), that
// we don't want to display to our users.
// below because getRootCauseMessage() adds a technical prefix (the name of the exception), that
// we don't want to display to our users.
Throwable rootCause = ExceptionUtils.getRootCause(throwable);
if (rootCause == null) {
// If there's no nested exception, fall back to the throwable itself for getting the cause
......@@ -96,4 +98,21 @@ public List<Block> generateErrorBlocks(String messagePrefix, Throwable throwable
return generateErrorBlocks(augmentedMessage, ExceptionUtils.getStackTrace(throwable), isInline);
}
@Override
public boolean containsError(Block parent)
{
boolean foundError = false;
List<Block> groupAndFormatBlocks = parent.getBlocks(
new OrBlockMatcher(new ClassBlockMatcher(GroupBlock.class), new ClassBlockMatcher(FormatBlock.class)),
Block.Axes.DESCENDANT);
for (Block block : groupAndFormatBlocks) {
String classParameter = block.getParameters().get(ErrorBlockGenerator.CLASS_ATTRIBUTE_NAME);
if (classParameter != null && classParameter.contains(ErrorBlockGenerator.CLASS_ATTRIBUTE_MESSAGE_VALUE)) {
foundError = true;
break;
}
}
return foundError;
}
}
......@@ -21,8 +21,16 @@
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.xwiki.component.annotation.Role;
import org.xwiki.logging.LogLevel;
import org.xwiki.logging.LogUtils;
import org.xwiki.logging.event.LogEvent;
import org.xwiki.logging.marker.TranslationMarker;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.stability.Unstable;
/**
* Generates error blocks to render an error in a wiki page.
......@@ -48,6 +56,14 @@
*/
String CLASS_ATTRIBUTE_DESCRIPTION_VALUE = "xwikirenderingerrordescription hidden";
/**
* @param block the block in which to search for rendering errors
* @return true if the passed block contains a rendering error, false otherwise
* @since 14.0RC1
*/
@Unstable
boolean containsError(Block block);
/**
* Generates error blocks to render an error in a wiki page.
*
......@@ -55,18 +71,57 @@
* @param description the description that will be displayed to the user when he clicks on the message
* @param isInline whether the generated blocks should be inline or not
* @return the generated blocks
* @see #generateErrorBlocks(boolean, Marker, String, String, Object...)
* @deprecated since 14.0RC1, use {@link #generateErrorBlocks(boolean, Marker, String, String, Object...)} instead
*/
@Deprecated
List<Block> generateErrorBlocks(String message, String description, boolean isInline);
/**
* Generates error blocks to render an error in a wiki page.
* <p>
* It's generally recommended to also provide a translation key to let the {@link ErrorBlockGenerator}
* implementation search for a localized version of the error message depending on the context locale.
*
* @param messagePrefix the prefix of the short message that will be displayed to the user. This message will be
* augmented with the root cause of the error extracted from the passed throwable and an
* additional text inviting the user to click the message will be added to the message prefix
* augmented with the root cause of the error extracted from the passed throwable and an additional text
* inviting the user to click the message will be added to the message prefix
* @param throwable the exception from which the description will be extracted from
* @param isInline whether the generated blocks should be inline or not
* @return the generated blocks
* @see #generateErrorBlocks(boolean, Marker, String, String, Object...)
* @deprecated since 14.0RC1, use {@link #generateErrorBlocks(boolean, Marker, String, String, Object...)} instead
*/
@Deprecated
List<Block> generateErrorBlocks(String messagePrefix, Throwable throwable, boolean isInline);
/**
* Generates error blocks to render an error in a wiki page.
* <p>
* If a {@link TranslationMarker} is provided, this message and description can be translated based on the current
* context depending on the implementation of this component.
*
* @param inline whether the generated blocks should be inline or not
* @param marker a marker associated to the message. It's recommended to at least pass a {@link TranslationMarker}
* to indicate how to translate the message.
* @param defaultMessage the default message following SLF4J's {@link Logger} syntax
* @param defaultDescription the default description following SLF4J's {@link Logger} syntax
* @param arguments a list arguments to insert in the message and the description and/or a {@link Throwable}
* @return the generated blocks. Return a {@link XDOM} if {@code inline} is false.
* @since 14.0RC1
*/
@Unstable
default List<Block> generateErrorBlocks(boolean inline, Marker marker, String defaultMessage,
String defaultDescription, Object... arguments)
{
LogEvent message = LogUtils.newLogEvent(marker, LogLevel.ERROR, defaultMessage, arguments);
if (message.getThrowable() != null) {
return generateErrorBlocks(message.getFormattedMessage(), message.getThrowable(), inline);
} else {
LogEvent description = LogUtils.newLogEvent(marker, LogLevel.ERROR, defaultDescription, arguments);
return generateErrorBlocks(message.getFormattedMessage(), description.getFormattedMessage(), inline);
}
}
}
......@@ -99,8 +99,7 @@ public XDOM parse(String content, MacroTransformationContext macroContext, boole
throw new MacroExecutionException("Invalid Transformation: missing Syntax");
}
return createXDOM(content, macroContext, transform,
metadata, inline, syntax);
return createXDOM(content, macroContext, transform, metadata, inline, syntax);
}
/**
......@@ -119,8 +118,8 @@ private XDOM createXDOM(String content, MacroTransformationContext macroContext,
if (transform && macroContext.getTransformation() != null) {
TransformationContext txContext = new TransformationContext(result, syntax);
txContext.setId(macroContext.getId());
performTransformation((MutableRenderingContext) this.renderingContext,
macroContext.getTransformation(), txContext, result);
performTransformation((MutableRenderingContext) this.renderingContext, macroContext.getTransformation(),
txContext, result);
}
if (inline) {
......@@ -152,18 +151,21 @@ private void performTransformation(MutableRenderingContext renderingContext, Tra
*/
private XDOM convertToInline(XDOM xdom)
{
List<Block> blocks = new ArrayList<Block>(xdom.getChildren());
List<Block> blocks = new ArrayList<>(xdom.getChildren());
// TODO: use inline parser instead
if (!blocks.isEmpty()) {
this.parserUtils.removeTopLevelParagraph(blocks);
// Make sure included macro is inline when script macro itself is inline
Block block = blocks.get(0);
if (block instanceof MacroBlock) {
MacroBlock macro = (MacroBlock) block;
if (!macro.isInline()) {
blocks.set(0, new MacroBlock(macro.getId(), macro.getParameters(), macro.getContent(), true));
for (int i = 0; i < blocks.size(); ++i) {
Block block = blocks.get(i);
if (block instanceof MacroBlock) {
MacroBlock macro = (MacroBlock) block;
if (!macro.isInline()) {
blocks.set(i, new MacroBlock(macro.getId(), macro.getParameters(), macro.getContent(), true));
}
}
}
......
......@@ -21,14 +21,13 @@
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.xwiki.logging.marker.TranslationMarker;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.FormatBlock;
import org.xwiki.rendering.block.GroupBlock;
import org.xwiki.rendering.block.MacroBlock;
import org.xwiki.rendering.block.MacroMarkerBlock;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.block.match.ClassBlockMatcher;
import org.xwiki.rendering.block.match.OrBlockMatcher;
import org.xwiki.rendering.util.ErrorBlockGenerator;
/**
......@@ -49,56 +48,13 @@ public MacroErrorManager(ErrorBlockGenerator errorBlockGenerator)
this.errorBlockGenerator = errorBlockGenerator;
}
/**
* Generates Blocks to signify that the passed Macro Block has failed to execute.
*
* @param macroToReplace the block for the macro that failed to execute and that we'll replace with Block
* showing to the user that macro has failed
* @param message the message to display to the user in place of the macro result
* @param description the long description of the error to display to the user in place of the macro result
*/
public void generateError(MacroBlock macroToReplace, String message, String description)
{
List<Block> errorBlocks =
this.errorBlockGenerator.generateErrorBlocks(message, description, macroToReplace.isInline());
macroToReplace.getParent().replaceChild(wrapInMacroMarker(macroToReplace, errorBlocks), macroToReplace);
}
/**
* Generates Blocks to signify that the passed Macro Block has failed to execute.
*
* @param macroToReplace the block for the macro that failed to execute and that we'll replace with Block
* showing to the user that macro has failed
* @param message the message to display to the user in place of the macro result
* @param throwable the exception for the failed macro execution to display to the user in place of the macro result
*/
public void generateError(MacroBlock macroToReplace, String message, Throwable throwable)
{
List<Block> errorBlocks =
this.errorBlockGenerator.generateErrorBlocks(message, throwable, macroToReplace.isInline());
macroToReplace.getParent().replaceChild(wrapInMacroMarker(macroToReplace, errorBlocks), macroToReplace);
}
/**
* @param xdom the XDOM on which to check if there's a macro error
* @return true if the passed XDOM contains a macro error or false otherwise
*/
public boolean containsError(XDOM xdom)
{
boolean foundError = false;
List<Block> groupAndFormatBlocks = xdom.getBlocks(
new OrBlockMatcher(
new ClassBlockMatcher(GroupBlock.class),
new ClassBlockMatcher(FormatBlock.class)),
Block.Axes.DESCENDANT);
for (Block block : groupAndFormatBlocks) {
String classParameter = block.getParameters().get(ErrorBlockGenerator.CLASS_ATTRIBUTE_NAME);
if (classParameter != null && classParameter.contains(ErrorBlockGenerator.CLASS_ATTRIBUTE_MESSAGE_VALUE)) {
foundError = true;
break;
}
}
return foundError;
return this.errorBlockGenerator.containsError(xdom);
}
/**
......@@ -113,4 +69,23 @@ private Block wrapInMacroMarker(MacroBlock macroBlockToWrap, List<Block> newBloc
return new MacroMarkerBlock(macroBlockToWrap.getId(), macroBlockToWrap.getParameters(),
macroBlockToWrap.getContent(), newBlocks, macroBlockToWrap.isInline());
}
/**
* Generates Blocks to signify that the passed Macro Block has failed to execute.
*
* @param macroToReplace
* @param marker a marker associated to the message. It's recommended to at least pass a {@link TranslationMarker}
* to indicate how to translate the message.
* @param defaultMessage the default message following SLF4J's {@link Logger} syntax
* @param defaultDescription the default description following SLF4J's {@link Logger} syntax
* @param arguments a list arguments to insert in the message and the description and/or a {@link Throwable}
* @since 14.0RC1
*/
public void generateError(MacroBlock macroToReplace, Marker marker, String defaultMessage,
String defaultDescription, Object... arguments)
{
List<Block> errorBlocks = this.errorBlockGenerator.generateErrorBlocks(macroToReplace.isInline(), marker,
defaultMessage, defaultDescription, arguments);
macroToReplace.getParent().replaceChild(wrapInMacroMarker(macroToReplace, errorBlocks), macroToReplace);
}
}
......@@ -32,6 +32,7 @@
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.logging.marker.TranslationMarker;
import org.xwiki.properties.BeanManager;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.MacroBlock;
......@@ -67,6 +68,16 @@
@Singleton
public class MacroTransformation extends AbstractTransformation implements Initializable
{
private static final TranslationMarker TM_UNKNOWNMACRO = new TranslationMarker("rendering.macro.error.unknown");
private static final TranslationMarker TM_FAILEDMACRO = new TranslationMarker("rendering.macro.error.failed");
private static final TranslationMarker TM_INVALIDMACRO = new TranslationMarker("rendering.macro.error.invalid");
private static final TranslationMarker TM_STANDALONEMACRO = new TranslationMarker("rendering.macro.error.standalone");
private static final TranslationMarker TM_INVALIDMACROPARAMETER = new TranslationMarker("rendering.macro.error.invalidParameter");
private static class MacroLookupExceptionElement
{
private MacroBlock macroBlock;
......@@ -231,15 +242,14 @@ public void transform(Block rootBlock, TransformationContext context) throws Tra
for (MacroLookupExceptionElement error : priorityMacroBlockMatcher.getErrors()) {
if (error.getException() instanceof MacroNotFoundException) {
// Macro cannot be found. Generate an error message instead of the macro execution result.
// TODO: make it internationalized
this.macroErrorManager.generateError(error.getMacroBlock(),
String.format("Unknown macro: %s.", error.getMacroBlock().getId()), String.format(
"The \"%s\" macro is not in the list of registered macros. Verify the spelling or "
+ "contact your administrator.", error.getMacroBlock().getId()));
this.macroErrorManager.generateError(error.getMacroBlock(), TM_UNKNOWNMACRO,
"Unknown macro: {}.",
"The [{}] macro is not in the list of registered macros. Verify the spelling or "
+ "contact your administrator.",
error.getMacroBlock().getId());
} else {
// TODO: make it internationalized
this.macroErrorManager.generateError(error.getMacroBlock(),
String.format("Invalid macro: %s", error.getMacroBlock().getId()), error.getException());
this.macroErrorManager.generateError(error.getMacroBlock(), TM_INVALIDMACRO,
"Invalid macro: {}.", null, error.getMacroBlock().getId(), error.getException());
}
}
}
......@@ -264,13 +274,14 @@ public void transform(Block rootBlock, TransformationContext context) throws Tra
// The macro doesn't support inline mode, raise a warning but continue.
// The macro will not be executed and we generate an error message instead of the macro
// execution result.
this.macroErrorManager.generateError(macroBlock, String.format(
"The [%s] macro is a standalone macro and it cannot be used inline",
macroBlock.getId()),
this.macroErrorManager.generateError(macroBlock, TM_STANDALONEMACRO,
"The [{}] macro is a standalone macro and it cannot be used inline",
"This macro generates standalone content. As a consequence you need to make sure to use a "
+ "syntax that separates your macro from the content before and after it so that it's on a "
+ "line by itself. For example in XWiki Syntax 2.0+ this means having 2 newline characters "
+ "(a.k.a line breaks) separating your macro from the content before and after it.");
+ "syntax that separates your macro from the content before and after it so that it's on a "
+ "line by itself. For example in XWiki Syntax 2.0+ this means having 2 newline characters "
+ "(a.k.a line breaks) separating your macro from the content before and after it.",
macroBlock.getId());
continue;
}
} else {
......@@ -289,8 +300,9 @@ public void transform(Block rootBlock, TransformationContext context) throws Tra
// One macro parameter was invalid.
// The macro will not be executed and we generate an error message instead of the macro
// execution result.
this.macroErrorManager.generateError(macroBlock,
String.format("Invalid macro parameters used for the \"%s\" macro", macroBlock.getId()), e);
this.macroErrorManager.generateError(macroBlock, TM_INVALIDMACROPARAMETER,
"Invalid macro parameters used for the [{}] macro.", null, macroBlock.getId(), e);
continue;
}
......@@ -300,8 +312,9 @@ public void transform(Block rootBlock, TransformationContext context) throws Tra
// The macro will not be executed and we generate an error message instead of the macro
// execution result.
// Note: We catch any Exception because we want to never break the whole rendering.
this.macroErrorManager.generateError(macroBlock,
String.format("Failed to execute the [%s] macro", macroBlock.getId()), e);
this.macroErrorManager.generateError(macroBlock, TM_FAILEDMACRO, "Failed to execute the [{}] macro.",
null, macroBlock.getId(), e);
continue;
} finally {
((MutableRenderingContext) this.renderingContext).setCurrentBlock(null);
......
# ---------------------------------------------------------------------------
# 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.
# ---------------------------------------------------------------------------
###############################################################################
# XWiki Core localization
#
# This contains the translations of the module in the default language
# (generally English).
#
# See https://dev.xwiki.org/xwiki/bin/view/Community/L10N/Conventions/ for more details about about
# translation key naming.
#
# Comments: it's possible to add some detail about a key to make easier to
# translate it by adding a comment before it. To make sure a comment is not
# assigned to the following key use at least three sharps (###) for the comment
# or after it.
#
# Deprecated keys:
# * when deleting a key it should be moved to deprecated section at the end
# of the file (between #@deprecatedstart and #@deprecatedend) and associated to the
# first version in which it started to be deprecated
# * when renaming a key, it should be moved to the same deprecated section
# and a comment should be added with the following syntax:
# #@deprecated new.key.name
# old.key.name=Some translation
###############################################################################
rendering.macro.error.unknown=Unknown macro: {}.
rendering.macro.error.unknown.description=The [{}] macro is not in the list of registered macros. Verify the spelling or contact your administrator.
rendering.macro.error.failed=Failed to execute the [{}] macro.
rendering.macro.error.invalid=Invalid macro: {}.
rendering.macro.error.standalone=The [{}] macro is a standalone macro and it cannot be used inline.
rendering.macro.error.standalone.description=This macro generates standalone content. As a consequence you need to make sure to use a syntax that separates your macro from the content before and after it so that it's on a line by itself. For example in XWiki Syntax 2.0+ this means having 2 newline characters (a.k.a line breaks) separating your macro from the content before and after it.
rendering.macro.error.invalidParameter=Invalid macro parameters used for the [{}] macro.
......@@ -77,7 +77,7 @@ public void containsErrorWhenInlineMacroError() throws Exception
new MacroErrorManager(this.componentManager.getInstance(ErrorBlockGenerator.class));
MacroBlock macroBlock = new MacroBlock("testmacro", Collections.emptyMap(), true);
XDOM xdom = new XDOM(Arrays.asList(macroBlock));
errorManager.generateError(macroBlock, "test message", "test description");
errorManager.generateError(macroBlock, null, "test message", "test description");
Assert.assertTrue(errorManager.containsError(xdom));
}
}
......@@ -279,7 +279,7 @@ public void transformNotExistingMacro() throws Exception
+ "onWord [Unknown macro: notexisting.]\n"
+ "endGroup [[class]=[xwikirenderingerror]]\n"
+ "beginGroup [[class]=[xwikirenderingerrordescription hidden]]\n"
+ "onVerbatim [The \"notexisting\" macro is not in the list of registered macros. "
+ "onVerbatim [The [notexisting] macro is not in the list of registered macros. "
+ "Verify the spelling or contact your administrator.] [false]\n"
+ "endGroup [[class]=[xwikirenderingerrordescription hidden]]\n"
+ "endMacroMarkerStandalone [notexisting] []\n"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment