org.xwiki.rendering.block.AbstractBlock Maven / Gradle / Ivy
/*
* 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.rendering.block;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.xwiki.rendering.block.match.BlockMatcher;
import org.xwiki.rendering.block.match.BlockNavigator;
import org.xwiki.rendering.block.match.CounterBlockMatcher;
import org.xwiki.rendering.block.match.FunctionBlockMatcher;
import org.xwiki.rendering.block.match.MetadataBlockMatcher;
import org.xwiki.rendering.listener.Listener;
import org.xwiki.rendering.listener.MetaData;
import org.xwiki.rendering.syntax.Syntax;
/**
* Implementation for Block operations. All blocks should extend this class. Supports the notion of generic parameters
* which can be added to a block (see {@link #getParameter(String)} for more details.
*
* @version $Id: d54488d4ae6954cb387b7104d3364ad3395f570f $
* @since 1.5M2
*/
public abstract class AbstractBlock implements Block
{
/**
* Store parameters, see {@link #getParameter(String)} for more explanations on what parameters are.
*/
private Map parameters;
/**
* Store attributes, see {@link #getAttribute(String)} for more explanations what attributes are.
*/
private Map attributes;
/**
* The Blocks this Block contains.
*/
private List childrenBlocks;
/**
* The Block containing this Block.
*/
private Block parentBlock;
/**
* The next Sibling Block or null if no next sibling exists.
*/
private Block nextSiblingBlock;
/**
* The previous Sibling Block or null if no previous sibling exists.
*/
private Block previousSiblingBlock;
/**
* Empty constructor to construct an empty block.
*/
public AbstractBlock()
{
// Nothing to do
}
/**
* Construct a block with parameters.
*
* @param parameters the parameters to set
*/
public AbstractBlock(Map parameters)
{
setParameters(parameters);
}
/**
* Constructs a block with a child block.
*
* @param childBlock the child block of this block
* @since 3.0M1
*/
public AbstractBlock(Block childBlock)
{
this(childBlock, Collections.emptyMap());
}
/**
* Constructs a block with children blocks.
*
* @param childrenBlocks the list of children blocks of the block to construct
* @since 3.0M1
*/
public AbstractBlock(List extends Block> childrenBlocks)
{
this(childrenBlocks, Collections.emptyMap());
}
/**
* Construct a block with a child block and parameters.
*
* @param childBlock the child block of this block
* @param parameters the parameters to set
* @since 3.0M1
*/
public AbstractBlock(Block childBlock, Map parameters)
{
this(parameters);
addChild(childBlock);
}
/**
* Construct a block with children blocks and parameters.
*
* @param childrenBlocks the list of children blocks of the block to construct
* @param parameters the parameters to set
* @since 3.0M1
*/
public AbstractBlock(List extends Block> childrenBlocks, Map parameters)
{
this(parameters);
addChildren(childrenBlocks);
}
/**
* Get the position of the provided block in the provided list of blocks.
*
* Can't use {@link List#indexOf(Object)} since it's using {@link Object#equals(Object)} internally which is not
* what we want since two WordBlock with the same text or two spaces are equals for example but we want to be able
* to target one specific Block.
*
* @param block the block for which to find the position
* @param blocks the list of blocks in which to look for the passed block
* @return the position of the block, -1 if the block can't be found
*/
private static int indexOfBlock(Block block, List blocks)
{
int position = 0;
for (Block child : blocks) {
if (child == block) {
return position;
}
++position;
}
return -1;
}
@Override
public void addChild(Block blockToAdd)
{
insertChildAfter(blockToAdd, null);
}
@Override
public void addChildren(List extends Block> blocksToAdd)
{
if (!blocksToAdd.isEmpty()) {
if (this.childrenBlocks == null) {
// Create the list with just the exact required size
this.childrenBlocks = new ArrayList<>(blocksToAdd.size());
}
for (Block blockToAdd : blocksToAdd) {
addChild(blockToAdd);
}
}
}
@Override
public void setChildren(List extends Block> children)
{
if (children.isEmpty()) {
if (this.childrenBlocks != null) {
this.childrenBlocks.clear();
}
} else {
if (this.childrenBlocks != null) {
this.childrenBlocks.clear();
}
addChildren(children);
}
}
@Override
public void setNextSiblingBlock(Block nextSiblingBlock)
{
this.nextSiblingBlock = nextSiblingBlock;
}
@Override
public void setPreviousSiblingBlock(Block previousSiblingBlock)
{
this.previousSiblingBlock = previousSiblingBlock;
}
@Override
public void insertChildBefore(Block blockToInsert, Block nextBlock)
{
blockToInsert.setParent(this);
if (nextBlock == null) {
// Last block becomes last but one
if (this.childrenBlocks != null && !this.childrenBlocks.isEmpty()) {
Block lastBlock = this.childrenBlocks.get(this.childrenBlocks.size() - 1);
blockToInsert.setPreviousSiblingBlock(lastBlock);
lastBlock.setNextSiblingBlock(blockToInsert);
} else {
blockToInsert.setPreviousSiblingBlock(null);
if (this.childrenBlocks == null) {
this.childrenBlocks = new ArrayList<>(1);
}
}
blockToInsert.setNextSiblingBlock(null);
this.childrenBlocks.add(blockToInsert);
} else {
// If there's a previous block to nextBlock then get it to set its next sibling
Block previousBlock = nextBlock.getPreviousSibling();
if (previousBlock != null) {
previousBlock.setNextSiblingBlock(blockToInsert);
blockToInsert.setPreviousSiblingBlock(previousBlock);
} else {
blockToInsert.setPreviousSiblingBlock(null);
}
blockToInsert.setNextSiblingBlock(nextBlock);
nextBlock.setPreviousSiblingBlock(blockToInsert);
if (this.childrenBlocks == null || this.childrenBlocks.isEmpty()) {
this.childrenBlocks = new ArrayList<>(1);
this.childrenBlocks.add(blockToInsert);
} else {
this.childrenBlocks.add(indexOfChild(nextBlock), blockToInsert);
}
}
}
@Override
public void insertChildAfter(Block blockToInsert, Block previousBlock)
{
if (previousBlock == null) {
insertChildBefore(blockToInsert, null);
} else {
// If there's a next block to previousBlock then get it to set its previous sibling
Block nextBlock = previousBlock.getNextSibling();
if (nextBlock != null) {
nextBlock.setPreviousSiblingBlock(blockToInsert);
blockToInsert.setNextSiblingBlock(nextBlock);
} else {
blockToInsert.setNextSiblingBlock(null);
}
blockToInsert.setPreviousSiblingBlock(previousBlock);
previousBlock.setNextSiblingBlock(blockToInsert);
if (this.childrenBlocks == null) {
this.childrenBlocks = new ArrayList<>(1);
}
this.childrenBlocks.add(indexOfChild(previousBlock) + 1, blockToInsert);
}
}
@Override
public void replaceChild(Block newBlock, Block oldBlock)
{
replaceChild(Collections.singletonList(newBlock), oldBlock);
}
@Override
public void replaceChild(List newBlocks, Block oldBlock)
{
int position = indexOfChild(oldBlock);
if (position == -1) {
throw new InvalidParameterException("Provided Block to replace is not a child");
}
List blocks = getChildren();
// Remove old child
blocks.remove(position);
oldBlock.setParent(null);
// Insert new children
Block previousBlock = oldBlock.getPreviousSibling();
if (newBlocks.isEmpty() && previousBlock != null) {
previousBlock.setNextSiblingBlock(oldBlock.getNextSibling());
}
Block lastBlock = null;
for (Block block : newBlocks) {
block.setParent(this);
block.setPreviousSiblingBlock(previousBlock);
if (previousBlock != null) {
previousBlock.setNextSiblingBlock(block);
}
previousBlock = block;
lastBlock = block;
}
Block nextBlock = oldBlock.getNextSibling();
if (nextBlock != null) {
nextBlock.setPreviousSiblingBlock(lastBlock);
}
if (lastBlock != null) {
lastBlock.setNextSiblingBlock(nextBlock);
}
blocks.addAll(position, newBlocks);
oldBlock.setNextSiblingBlock(null);
oldBlock.setPreviousSiblingBlock(null);
}
/**
* Get the position of the provided block in the list of children.
*
* Can't use {@link List#indexOf(Object)} since it's using {@link Object#equals(Object)} internally which is not
* what we want since two WordBlock with the same text or two spaces are equals for example but we want to be able
* to target one specific Block.
*
* @param block the block
* @return the position of the block, -1 if the block can't be found
*/
private int indexOfChild(Block block)
{
return indexOfBlock(block, getChildren());
}
/**
* Find the index of the block in the tree.
*
* @param child the block for which to find the index
* @return the index of the passed block in the tree, 0 is the current block and -1 means that it was not found
* @since 10.10RC1
*/
public long indexOf(Block child)
{
CounterBlockMatcher counter = new CounterBlockMatcher(child);
Block found = getFirstBlock(counter, Axes.DESCENDANT_OR_SELF);
return found != null ? counter.getCount() : -1;
}
@Override
public List getChildren()
{
return this.childrenBlocks == null ? Collections.emptyList() : this.childrenBlocks;
}
@Override
public Block getParent()
{
return this.parentBlock;
}
@Override
public Map getParameters()
{
return this.parameters == null ? Collections.emptyMap()
: Collections.unmodifiableMap(this.parameters);
}
@Override
public String getParameter(String name)
{
return this.parameters == null ? null : this.parameters.get(name);
}
@Override
public void setParameter(String name, String value)
{
if (this.parameters == null) {
this.parameters = new LinkedHashMap<>(1);
}
this.parameters.put(name, value);
}
@Override
public void setParameters(Map parameters)
{
if (this.parameters == null) {
this.parameters = new LinkedHashMap<>(parameters);
} else {
this.parameters.clear();
this.parameters.putAll(parameters);
}
}
@Override
public Map getAttributes()
{
return this.attributes == null ? Collections.emptyMap()
: Collections.unmodifiableMap(this.attributes);
}
@Override
public Object getAttribute(String name)
{
return this.attributes == null ? null : this.attributes.get(name);
}
@Override
public void setAttribute(String name, Object value)
{
if (this.attributes == null) {
this.attributes = new LinkedHashMap<>(1);
}
this.attributes.put(name, value);
}
@Override
public void setAttributes(Map attributes)
{
if (this.attributes == null) {
this.attributes = new LinkedHashMap<>(attributes);
} else {
this.attributes.clear();
this.attributes.putAll(attributes);
}
}
@Override
public void setParent(Block parentBlock)
{
this.parentBlock = parentBlock;
}
@Override
public Block getRoot()
{
Block block = this;
while (block.getParent() != null) {
block = block.getParent();
}
return block;
}
@Override
public Block getNextSibling()
{
return this.nextSiblingBlock;
}
@Override
public Block getPreviousSibling()
{
return this.previousSiblingBlock;
}
@Override
public void removeBlock(Block childBlockToRemove)
{
// Remove block
List children = getChildren();
int position = indexOfBlock(childBlockToRemove, children);
if (position == -1) {
throw new InvalidParameterException("Provided Block to remove is not a child");
}
getChildren().remove(position);
// Re-calculate internal links between blocks
if (childBlockToRemove != null) {
Block previousBlock = childBlockToRemove.getPreviousSibling();
if (previousBlock != null) {
previousBlock.setNextSiblingBlock(childBlockToRemove.getNextSibling());
}
Block nextBlock = childBlockToRemove.getNextSibling();
if (nextBlock != null) {
nextBlock.setPreviousSiblingBlock(previousBlock);
}
childBlockToRemove.setNextSiblingBlock(null);
childBlockToRemove.setPreviousSiblingBlock(null);
}
}
@Override
public boolean equals(Object obj)
{
if (obj == this) {
return true;
}
if (obj instanceof Block) {
EqualsBuilder builder = new EqualsBuilder();
builder.append(getChildren(), ((Block) obj).getChildren());
builder.append(getParameters(), ((Block) obj).getParameters());
builder.append(getAttributes(), ((Block) obj).getAttributes());
return builder.isEquals();
}
return false;
}
@Override
public int hashCode()
{
HashCodeBuilder builder = new HashCodeBuilder();
builder.append(this.childrenBlocks);
builder.append(this.parameters);
builder.append(this.attributes);
return builder.toHashCode();
}
@Override
public Block clone()
{
return clone(null);
}
/**
* {@inheritDoc}
*
* @since 1.8RC2
*/
@Override
public Block clone(BlockFilter blockFilter)
{
Block block;
try {
block = (AbstractBlock) super.clone();
} catch (CloneNotSupportedException e) {
// Should never happen
throw new RuntimeException("Failed to clone object", e);
}
if (this.parameters != null) {
((AbstractBlock) block).parameters = new LinkedHashMap<>(this.parameters);
}
// Clone attribute values if possible as documented in getAttribute().
this.getAttributes().forEach((key, value) -> block.setAttribute(key, ObjectUtils.cloneIfPossible(value)));
if (this.childrenBlocks != null) {
((AbstractBlock) block).childrenBlocks = new ArrayList<>(this.childrenBlocks.size());
for (Block childBlock : this.childrenBlocks) {
if (blockFilter != null) {
Block clonedChildBlocks = childBlock.clone(blockFilter);
List filteredBlocks = blockFilter.filter(clonedChildBlocks);
if (filteredBlocks.isEmpty()) {
filteredBlocks = clonedChildBlocks.getChildren();
}
block.addChildren(filteredBlocks);
} else {
block.addChild(childBlock.clone());
}
}
}
return block;
}
@Override
public void traverse(Listener listener)
{
before(listener);
for (Block block : getChildren()) {
block.traverse(listener);
}
after(listener);
}
/**
* Send {@link org.xwiki.rendering.listener.Listener} events corresponding to the start of the block. For example
* for a Bold block, this allows an XHTML Listener (aka a Renderer) to output <b>
.
*
* @param listener the listener that will receive the events sent by this block before its children blocks have
* emitted their own events.
*/
public void before(Listener listener)
{
// Do nothing by default, should be overridden by extending Blocks
}
/**
* Send {@link Listener} events corresponding to the end of the block. For example for a Bold block, this allows an
* XHTML Listener (aka a Renderer) to output </b>
.
*
* @param listener the listener that will receive the events sent by this block before its children blocks have
* emitted their own events.
*/
public void after(Listener listener)
{
// Do nothing by default, should be overridden by extending Blocks
}
@Override
public List getBlocks(BlockMatcher matcher, Axes axes)
{
BlockNavigator navigator = new BlockNavigator(matcher);
return navigator.getBlocks(this, axes);
}
@Override
public T getFirstBlock(BlockMatcher matcher, Axes axes)
{
BlockNavigator navigator = new BlockNavigator(matcher);
return navigator.getFirstBlock(this, axes);
}
@Override
public Optional getSyntaxMetadata()
{
MetaDataBlock metaDataBlock = getFirstBlock(MetadataBlockMatcher.SYNTAX, Axes.ANCESTOR_OR_SELF);
if (metaDataBlock != null) {
return Optional.ofNullable((Syntax) metaDataBlock.getMetaData().getMetaData(MetaData.SYNTAX));
}
return Optional.empty();
}
@Override
public Optional get(Function> searcher, Axes axes)
{
FunctionBlockMatcher matcher = new FunctionBlockMatcher<>(searcher);
getFirstBlock(matcher, Axes.ANCESTOR_OR_SELF);
return matcher.getValue();
}
}