/*
* 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.refactoring.internal.splitter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.xwiki.component.annotation.Component;
import org.xwiki.refactoring.WikiDocument;
import org.xwiki.refactoring.splitter.DocumentSplitter;
import org.xwiki.refactoring.splitter.criterion.SplittingCriterion;
import org.xwiki.refactoring.splitter.criterion.naming.NamingCriterion;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.Block.Axes;
import org.xwiki.rendering.block.BlockFilter;
import org.xwiki.rendering.block.HeaderBlock;
import org.xwiki.rendering.block.IdBlock;
import org.xwiki.rendering.block.LinkBlock;
import org.xwiki.rendering.block.NewLineBlock;
import org.xwiki.rendering.block.SectionBlock;
import org.xwiki.rendering.block.SpaceBlock;
import org.xwiki.rendering.block.SpecialSymbolBlock;
import org.xwiki.rendering.block.WordBlock;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.block.match.ClassBlockMatcher;
import org.xwiki.rendering.listener.reference.DocumentResourceReference;
import org.xwiki.rendering.listener.reference.ResourceReference;
import org.xwiki.rendering.listener.reference.ResourceType;
/**
* Default implementation of {@link DocumentSplitter}.
*
* @version $Id: fc0fdd10dbe96c433b09736622c62de752a4bce5 $
* @since 1.9M1
*/
@Component
@Singleton
public class DefaultDocumentSplitter implements DocumentSplitter
{
/**
* The name of the anchor link parameter.
*/
private static final String ANCHOR_PARAMETER = "anchor";
@Override
public List<WikiDocument> split(WikiDocument rootDoc, SplittingCriterion splittingCriterion,
NamingCriterion namingCriterion)
{
List<WikiDocument> result = new ArrayList<WikiDocument>();
// Add the rootDoc into the result
result.add(rootDoc);
// Recursively split the root document.
split(rootDoc, rootDoc.getXdom().getChildren(), 1, result, splittingCriterion, namingCriterion);
updateAnchors(result);
return result;
}
/**
* A recursive method for traversing the xdom of the root document and splitting it into sub documents.
*
* @param parentDoc the parent {@link WikiDocument} under which the given list of children reside.
* @param children current list of blocks being traversed.
* @param depth the depth from the root xdom to current list of children.
* @param result space for storing the resulting documents.
* @param splittingCriterion the {@link SplittingCriterion}.
* @param namingCriterion the {@link NamingCriterion}.
*/
private void split(WikiDocument parentDoc, List<Block> children, int depth, List<WikiDocument> result,
SplittingCriterion splittingCriterion, NamingCriterion namingCriterion)
{
ListIterator<Block> it = children.listIterator();
while (it.hasNext()) {
Block block = it.next();
if (splittingCriterion.shouldSplit(block, depth)) {
// Split a new document and add it to the results list.
XDOM xdom = new XDOM(block.getChildren());
String newDocumentName = namingCriterion.getDocumentName(xdom);
WikiDocument newDoc = new WikiDocument(newDocumentName, xdom, parentDoc);
result.add(newDoc);
// Remove the original block from the parent document.
it.remove();
// Place a link from the parent to child.
it.add(new NewLineBlock());
it.add(createLink(block, newDocumentName));
// Check whether this node should be further traversed.
if (splittingCriterion.shouldIterate(block, depth)) {
split(newDoc, newDoc.getXdom().getChildren(), depth + 1, result, splittingCriterion,
namingCriterion);
}
} else if (splittingCriterion.shouldIterate(block, depth)) {
split(parentDoc, block.getChildren(), depth + 1, result, splittingCriterion, namingCriterion);
}
}
}
/**
* Creates a {@link LinkBlock} suitable to be placed in the parent document.
*
* @param block the {@link Block} that has just been split into a separate document.
* @param target name of the target wiki document.
* @return a {@link LinkBlock} representing the link from the parent document to new document.
*/
private LinkBlock createLink(Block block, String target)
{
Block firstBlock = block.getChildren().get(0);
if (firstBlock instanceof HeaderBlock) {
DocumentResourceReference reference = new DocumentResourceReference(target);
// Clone the header block and remove any unwanted stuff
Block clonedHeaderBlock = firstBlock.clone(new BlockFilter()
{
@Override
public List<Block> filter(Block block)
{
List<Block> blocks = new ArrayList<Block>();
if (block instanceof WordBlock || block instanceof SpaceBlock
|| block instanceof SpecialSymbolBlock) {
blocks.add(block);
}
return blocks;
}
});
return new LinkBlock(clonedHeaderBlock.getChildren(), reference, false);
} else if (firstBlock instanceof SectionBlock) {
return createLink(firstBlock, target);
} else {
throw new IllegalArgumentException(
"A SectionBlock should either begin with a HeaderBlock or another SectionBlock.");
}
}
/**
* Update the links to internal document fragments after those fragments have been moved as a result of the split.
* For instance the "#Chapter1" anchor will be updated to "ChildDocument#Chapter1" if the document fragment
* identified by "Chapter1" has been moved to "ChildDocument" as a result of the split.
*
* @param documents the list of documents whose anchors to update
*/
private void updateAnchors(List<WikiDocument> documents)
{
// First we need to collect all the document fragments and map them to their new location.
Map<String, String> fragments = collectDocumentFragments(documents);
// Update the anchors.
for (WikiDocument document : documents) {
updateAnchors(document, fragments);
}
}
/**
* @param document the document whose anchors to update
* @param fragments see {@link #collectDocumentFragments(List)}
*/
private void updateAnchors(WikiDocument document, Map<String, String> fragments)
{
for (LinkBlock linkBlock : document.getXdom().<LinkBlock> getBlocks(new ClassBlockMatcher(LinkBlock.class),
Axes.DESCENDANT)) {
ResourceReference reference = linkBlock.getReference();
ResourceType resoureceType = reference.getType();
String fragment = null;
if ((ResourceType.DOCUMENT.equals(resoureceType) || ResourceType.SPACE.equals(resoureceType))
&& StringUtils.isEmpty(reference.getReference())) {
fragment = reference.getParameter(ANCHOR_PARAMETER);
} else if (StringUtils.startsWith(reference.getReference(), "#")
&& (ResourceType.PATH.equals(resoureceType) || ResourceType.URL.equals(resoureceType))) {
fragment = reference.getReference().substring(1);
}
String targetDocument = fragments.get(fragment);
if (targetDocument != null && !targetDocument.equals(document.getFullName())) {
// The fragment has been moved so we need to update the link.
reference.setType(ResourceType.DOCUMENT);
reference.setReference(targetDocument);
reference.setParameter(ANCHOR_PARAMETER, fragment);
}
}
}
/**
* Looks for document fragments in the given documents. A document fragment is identified by an {@link IdBlock} for
* instance.
*
* @param documents the list of documents whose fragments to collect
* @return the collection of document fragments mapped to the document that contains them
*/
private Map<String, String> collectDocumentFragments(List<WikiDocument> documents)
{
Map<String, String> fragments = new HashMap<String, String>();
for (WikiDocument document : documents) {
for (IdBlock idBlock : document.getXdom().<IdBlock> getBlocks(new ClassBlockMatcher(IdBlock.class),
Axes.DESCENDANT)) {
fragments.put(idBlock.getName(), document.getFullName());
}
}
return fragments;
}
}