/**
*
*/
package com.sap.furcas.runtime.parser.textblocks.observer;
import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.getSubNodeAt;
import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.getSubNodes;
import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.getSubNodesSize;
import static com.sap.furcas.runtime.textblocks.TbUtil.getAbsoluteOffset;
import static com.sap.furcas.runtime.textblocks.TbUtil.isAncestorOf;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import com.sap.furcas.metamodel.FURCAS.textblocks.AbstractToken;
import com.sap.furcas.metamodel.FURCAS.textblocks.DocumentNode;
import com.sap.furcas.metamodel.FURCAS.textblocks.Eostoken;
import com.sap.furcas.metamodel.FURCAS.textblocks.TextBlock;
import com.sap.furcas.runtime.textblocks.modifcation.TbChangeUtil;
import com.sap.furcas.runtime.textblocks.validation.TbValidationUtil;
/**
* utils class performing the relatively complex relocation task for the ParsingObserver
*/
public class TokenRelocationUtil {
// /**
// * Comparator for {@link DocumentNode}s using their offsets.
// * @author C5106462
// *
// */
// private static class DocumentNodeOffsetComparator implements Comparator<DocumentNode> {
//
// @Override
// public int compare(DocumentNode node1, DocumentNode node2) {
// if(node1.isOffsetRelative() != node2.isOffsetRelative()) {
// if(!(node1 instanceof Eostoken || node1 instanceof Bostoken)
// && !(node2 instanceof Eostoken || node2 instanceof Bostoken) ) {
// //if one of them is EOS or BOS it doesn't matter as they are are always relative to 0
// throw new IllegalArgumentException(
// "Cannot compare DocumentNodes where one has a relative and the other an absolute offset");
// }
// }
// if (node1.getOffset() == node2.getOffset()) {
// return 0;
// } else if(node1.getOffset() < node2.getOffset()){
// return -1;
// } else {
// return 1;
// }
// }
//
// }
//
// private static final DocumentNodeOffsetComparator documentNodeOffsetComparator = new DocumentNodeOffsetComparator();
/**
* the actual relocate method, it moves the given tokens to the given textblock,
* updates the offset and length attributes of origin and target textblock, makes
* any subnodes of the given textblock relative to it (also subblocks).
* Will throw IllegalArgumentExceptions if the subnodes are relative or otherwise
* inconsistent (negative offsets, overlaps, gaps).
* @param currentTextBlock
* @param relocationTokens
* @param isNewBlock if true, grants that node only has absolute subnodes (else Exception will be thrown)
* @return
*/
//all other methods should not be visible outside this class, but are made visible for testing.
public static void relocateTokensAndSetLocationAndMakeSubsRelative(TextBlock currentTextBlock,
List<AbstractToken> relocationTokens, boolean isNewBlock) {
if (currentTextBlock == null) {
throw new IllegalArgumentException("null passed as block");
}
boolean tokensMoved = false;
if (relocationTokens != null && relocationTokens.size() != 0) {
// do relocate tokens, this adds tokens with absolute offsets somewhere into the block
tokensMoved = moveTokens(relocationTokens, currentTextBlock);
}
// this may happen even if we do not relocate any tokens, since the block might only have subblocks!
if (getSubNodesSize(currentTextBlock) != 0) {
/* needs to set location before we make the
* subnodes relative, as we cannot set location based on subnodes afterwards,
* however this only works if we can be sure the subnodes have been changed and are not relative.
* We could make the relocate method more flexible to ignore cases when subnodes are relative, but
* then we will be blind for other bugs.
*/
if (isNewBlock ) {
/* if tokens were moved, location is already up to date, and if not, calling this
* might not work because last node might be relative if no node was added
*/
if (! tokensMoved) {
updateTextBlockLocationUsingSubNodesAfterAdding(currentTextBlock);
}
}
makeSubNodesRelative(currentTextBlock);
// for finding bugs, assert is useful
TbValidationUtil.assertTextBlockConsistency(currentTextBlock);
}
}
// /**
// * Sorts the tokens of the given <code>textBlock</code> according to their offsets.
// * @param textBlock
// */
// static void updateTokenOrdering(TextBlock textBlock) {
// if(textBlock != null) {
// sort(textBlock.getTokens(), documentNodeOffsetComparator);
// }
// }
//
// /**
// * Sorts the subBlocks of the given <code>textBlock</code> according to their offsets.
// * @param textBlock
// */
// static void updateSubBlockOrdering(TextBlock textBlock) {
// sort(textBlock.getSubNodes(), documentNodeOffsetComparator);
// }
// /**
// * Cannot use {@link Collections#sort(List, Comparator)} because this directly replaces
// * elements within the target collections which causes a {@link DuplicateException} in
// * MOIN associations. Therefore the algorithm is modified in order to first remove the element
// * and then add the newly sorted one.
// *
// * TODO: Wait for MOIN guys to come up with a better solution, as this implementation
// * may be slow
// *
// * @param list
// * @param c
// */
// private static void sort(List list, DocumentNodeOffsetComparator c) {
// Object[] a = list.toArray();
// Arrays.sort(a, (Comparator)c);
// list.clear();
// for (Object object : a) {
// list.add((DocumentNode)object);
// }
// }
// /**
// * checks for
// * @param currentTextBlock
// */
// static void assertTextBlockConsistency(TextBlock currentTextBlock) {
// if (currentTextBlock.getOffset() < 0) {
// throw new IllegalStateException("TextBlock offset is negative: " + currentTextBlock.getOffset());
// }
// if (currentTextBlock.getLength() < 0) {
// throw new IllegalStateException("TextBlock length is negative: " + currentTextBlock.getOffset());
// }
// List<? extends DocumentNode> subnodes = getSubNodes(currentTextBlock);
// int lastEnd = getAbsoluteOffset(currentTextBlock); // implicitly checks for offset < 0
// int tokenLengthSum = 0;
// for (Iterator<? extends DocumentNode> iterator = subnodes.iterator(); iterator.hasNext();) {
// DocumentNode documentNode = (DocumentNode) iterator.next();
// int nodeOffSet = getAbsoluteOffset(documentNode);
// tokenLengthSum += documentNode.getLength();
// if (documentNode.getLength() <= 0 ) {
// if (! (documentNode instanceof Bostoken) && ! (documentNode instanceof Eostoken) ) {
// throw new IllegalStateException("Subnode of length <= 0 in block: " + documentNode);
// }
// }
// if ( nodeOffSet > lastEnd ) {
// throw new IllegalStateException("TextBlock with gap between subnodes created : " + nodeOffSet +" after " + lastEnd);
// }
// if (nodeOffSet < lastEnd ) {
// if (! (documentNode instanceof Eostoken) ) {
// throw new IllegalStateException("TextBlock with overlapping subnodes created : " + nodeOffSet +" before " + lastEnd);
// }
// }
// lastEnd = nodeOffSet + documentNode.getLength();
// }
// if (tokenLengthSum != currentTextBlock.getLength()) {
// throw new IllegalStateException("TextBlock length " + currentTextBlock.getLength() + " != "+ tokenLengthSum);
// }
//
// }
/**
* Relocated all elements stored in {@link #relocationCandidates} to the
* given {@link TextBlock targetBlock} after relocating the tokens, the
* location information of the affected blocks is updated.
*
* Assumes the List of tokens is sorted by offset and does not have gaps or
* overlaps (else target will become inconsistent).
* target block offset and length attributes will not be changed here
* (because it is done in relocateTokensAndSetLocation...()).
*
* @param tokensToRelocate the tokens to move to the given block
* @param targetBlock may be null, then tokens will get detached from their current parent
* @param isNewBlock
*/
public static boolean moveTokens(List<AbstractToken> tokensToRelocate, TextBlock targetBlock) {
boolean tokensMoved = false;
if (tokensToRelocate.size() > 0) {
// relocate all tokens from their current to their new parent, then empty tokensToRelocate
Set<TextBlock> parentsToBeUpdated = new HashSet<TextBlock>();
for (AbstractToken relocationCandidate : tokensToRelocate) {
TextBlock parentBlock = relocationCandidate.getParent();
if (parentBlock == null || ! parentBlock.equals(targetBlock)) {
// token should actually be moved
tokensMoved = true;
if (parentBlock != null) {
// if parent is ancestor of target, then parent needs no location update from moving the token
if (! isAncestorOf(parentBlock, targetBlock)) {
parentsToBeUpdated.add(parentBlock);
}
}
// make offset absolute to be sure it is correctly updated
// when added to its new block
if (relocationCandidate.isOffsetRelative()) {
// getAbsoluteOffset should be possible if the original tree was consistent
relocationCandidate.setOffset(getAbsoluteOffset(relocationCandidate));
relocationCandidate.setOffsetRelative(false);
}
// TODO: we can save performance in insert by making some general assumptions,
// such that the candidates are ordered, such as if insertposition = last, then
// from now on merely add
//int insertPosition = insertToken(targetBlock, relocationCandidate);
insertToken(targetBlock, relocationCandidate);
}
}
if (tokensMoved && targetBlock != null) {
updateTextBlockLocationUsingSubNodesAfterAdding(targetBlock);
/*
* TODO: this really should rather be
* new TextBlocksModel(root).relocateTokens(tokens, source, target)
* or
* new TextBlocksModel(root1).removeTokens(tokens, source, target)
* new TextBlocksModel(root2).addTokens(tokens, source, target)
* where all offsets and length of all nodes in the whole tree are consistent after the operation
*/
}
for (TextBlock textBlock : parentsToBeUpdated) {
// TODO: in for loop it can happen that parents get traversed before
// their children, meaning parents location could be inconsistent, and
// sometimes parents should be deleted after all their children have been deleted
//TODO check if textblock is allowed to be deleted, since maybe tokens may need to be added to it later?
if (getSubNodesSize(textBlock) == 0) {
TbChangeUtil.delete(textBlock); // do not delete parents, as they might get children added to later on during process
} else {
/* basically we removed some subset of tokens from this textblock,
* what is left is another arbitrary subset of absolute or relative tokens or subblocks.
*/
updateTextBlockLocationAfterRemoval(textBlock);
updateParentsAscendingAfterRemoval(textBlock);
// TODO if old parent really needs to be consistent now (no token gaps?), assert consistency to detect bugs early
}
}
if (tokensMoved && targetBlock != null) {
// -> if length or offset changed, parent and all following siblings need changing of location, unless offsetChange == length change
updateParentsAscendingAfterAdding(targetBlock);
}
}
return tokensMoved;
}
/**
* inserts token into target block at the correct position, does not update parents location info (and is therefore not generic enough to reuse.
* Also does not check that no overlap with existing tokens or block happens. all tokens involved are assumed not to overlap,
* and existing tokens are assumed to be sorted (smallest offset to highest).
* @param targetBlock
* @param relocationCandidate
* @return the index the token was inserted at
*/
public static int insertToken(TextBlock targetBlock, AbstractToken relocationCandidate) {
// get the absoluteOffset before setting parent to null
int targetAbsoluteLocation = getAbsoluteOffset(relocationCandidate);
int insertIndex = 0;
// first set parent to null in any case because else MOIN will try to add second parent
relocationCandidate.setParent(null);
if (targetBlock != null) {
List<DocumentNode> subNodes = targetBlock.getSubNodes();
Iterator<DocumentNode> iterator = subNodes.iterator();
while (iterator.hasNext()) {
DocumentNode subNode = iterator.next();
int existingOffset = getAbsoluteOffset(subNode);
if (existingOffset > targetAbsoluteLocation) {
break;
}
insertIndex++;
}
subNodes.add(insertIndex, relocationCandidate);
}
return insertIndex;
}
/**
* Updates the offset and length of the parent textblocks recursively.
* As the ordering of the tokens is fixed it can not be the case that
* the ordering of the subblocks has to be changed within tthe way to
* the root block, so no reordering needs to be performed.
*
* @param targetBlock
*/
static void updateParentsAscendingAfterAdding(TextBlock targetBlock) {
TextBlock parent = targetBlock.getParent();
if(parent != null) {
updateTextBlockLocationUsingSubNodesAfterAdding(parent);
updateParentsAscendingAfterAdding(parent);
}
}
/**
* Updates the offset and length of the parent textblocks recursively.
* As the ordering of the tokens is fixed it can not be the case that
* the ordering of the subblocks has to be changed within tthe way to
* the root block, so no reordering needs to be performed.
*
* @param targetBlock
*/
public static void updateParentsAscendingAfterRemoval(TextBlock targetBlock) {
TextBlock parent = targetBlock.getParent();
if(parent != null) {
updateTextBlockLocationAfterRemoval(parent);
updateParentsAscendingAfterRemoval(parent);
}
}
/**
* changes location information of a textblock after subnodes have been removed.
* Assumes that the textblock location information was valid before removal, so
* we can build on that, also assumes location information of subblocks is consistent
* @param textBlock
*/
public static void updateTextBlockLocationAfterRemoval(
TextBlock textBlock) {
int subNodesSize = getSubNodesSize(textBlock);
if (subNodesSize == 0) {
// this may happen if this method is called without current subnodes, which may not be a bug.
return;
}
DocumentNode firstSubNode = getSubNodeAt(textBlock, 0);
DocumentNode lastSubNode = getSubNodeAt(textBlock, subNodesSize - 1); // may be equal to firstNode
if (lastSubNode instanceof Eostoken) { // EOS has offset == 0 always
if (subNodesSize > 1 ) {
lastSubNode = getSubNodeAt(textBlock, subNodesSize - 2);
} else {
throw new IllegalArgumentException("Subnodes only consisted of EOSToken");
}
}
// Now either the original first subnode was removed or not, if not no need to update offset
/*
* REGARDLESS of whether TB has absolute or relative offset:
* either shift the textBlock offset by an amount if the new first subnode is relative (default case)
* or make the textBlock offset absolute is firstnode is absolute
*/
if (firstSubNode.isOffsetRelative()) {
int newFirstNodeoffset = firstSubNode.getOffset();
if (newFirstNodeoffset == 0) {
// original first subnode has not been removed, so we do not need to update offset
} else {
int originalOffset = textBlock.getOffset();
textBlock.setOffset(originalOffset+newFirstNodeoffset);
List<? extends DocumentNode> subnodes = getSubNodes(textBlock);
for (DocumentNode documentNode : subnodes) {
if (documentNode.isOffsetRelative()) {
int nodeOriginalOffset = documentNode.getOffset();
documentNode.setOffset(nodeOriginalOffset - newFirstNodeoffset);
}
}
}
} else { // firstSubnodeOffset is absolute we set the textblock to absolute offsets as well, as its easier
int originalOffset = getAbsoluteOffset(textBlock);
textBlock.setOffsetRelative(false);
int newFirstNodeoffset = firstSubNode.getOffset();
int difference = newFirstNodeoffset- originalOffset;
if (difference > 0) {
textBlock.setOffset(newFirstNodeoffset);
List<? extends DocumentNode> subnodes = getSubNodes(textBlock);
for (DocumentNode documentNode : subnodes) {
if (documentNode.isOffsetRelative()) {
int nodeOriginalOffset = documentNode.getOffset();
documentNode.setOffset(nodeOriginalOffset - difference);
}
}
} else if (difference < 0) { // assumption is violated
throw new IllegalStateException("new firstnode absolute offset is smaller than textBlock offset " + newFirstNodeoffset + " < " + originalOffset);
}
}
// override length and column properties even if they are still correct
int firstSubNodeAbsoluteOffset = getAbsoluteOffset(firstSubNode);
int lastSubNodeAbsoluteOffset = getAbsoluteOffset(lastSubNode);
int newlength = lastSubNodeAbsoluteOffset + lastSubNode.getLength() - firstSubNodeAbsoluteOffset;
textBlock.setLength(newlength);
}
/**
* Updates the position information of the textblock by analyzing the
* contained tokens and computing the range of the textblock accordingly.
* Assumes that either all subnodes are new and with absolutes offsets, or absolute Nodes
* have been added (hence an update is required)
* This also works with TextBlocks which had inconsistent location information before
* (such as newly created ones), since previous information is ignored.
*/
public static void updateTextBlockLocationUsingSubNodesAfterAdding(TextBlock textBlock) {
// update location information
int subNodesSize = getSubNodesSize(textBlock);
if (subNodesSize == 0) {
// this may happen if this method is called without current subnodes, which may not be a bug.
return;
}
// fetch first and last subnode
DocumentNode firstSubNode = getSubNodeAt(textBlock, 0);
DocumentNode lastSubNode = getSubNodeAt(textBlock, subNodesSize - 1); // may be equal to firstNode
if (lastSubNode instanceof Eostoken) { // EOS offset is 0, cannot use it
if (subNodesSize > 1 ) {
lastSubNode = getSubNodeAt(textBlock, subNodesSize - 2);
} else {
throw new IllegalArgumentException("Subnodes only consisted of EOSToken");
}
}
// assertions useful for detecting bugs
if (firstSubNode.getOffset() < 0 || lastSubNode.getOffset() < 0 ) {
throw new IllegalArgumentException("BUG: Negative relative offset in subnodes: " +firstSubNode.getOffset() + ", " + lastSubNode.getOffset());
}
if (firstSubNode.isOffsetRelative() ) {
/* this means that even though we added nodes, the original first node of the
* block still exists, meaning also that the offset of the textBlock needs no change.
*/
} else {
if (lastSubNode.isOffsetRelative() ) {
// In this case we assume that nodes were merely inserted before the last node.
//this means that all tokens after this inserted token that have a relative offset need to be made
//all tokens that were relative to absolute so that their offset can be reconputed lateron
List<? extends DocumentNode> subNodes = getSubNodes(textBlock) ;
for (DocumentNode node : subNodes) {
makeOffsetAbsolute(node);
}
}
// the absolute offset of the textblock is the offset of its first subnode
textBlock.setOffset(firstSubNode.getOffset());
textBlock.setOffsetRelative(false);
}
// TB length is the difference between the end of its last node
// and the offset of either its first subnode or itself (depending on whether first subnode had changed)
int newlength = getNewLength(textBlock, lastSubNode);
if(newlength < 0) {
throw new IllegalArgumentException("Tried to set negative length "+ newlength +" for TextBlock.");
}
textBlock.setLength(newlength);
}
public static void makeOffsetAbsolute(DocumentNode node) {
if(node.isOffsetRelative() && node.getParent() != null) {
node.setOffset(getAbsoluteOffset(node));
node.setOffsetRelative(false);
}
}
private static int getNewLength(TextBlock textBlock, DocumentNode lastSubNode) {
int lastEnd;
int tbOffset;
if(lastSubNode.isOffsetRelative() && textBlock.isOffsetRelative()) {
lastEnd = lastSubNode.getOffset() + lastSubNode.getLength();
tbOffset = 0; // as everything is relative the offset of the block that is subtracted at the end is 0
} else {
if(lastSubNode.isOffsetRelative()) {
lastEnd = getAbsoluteOffset(lastSubNode) + lastSubNode.getLength();
} else {
lastEnd = lastSubNode.getOffset() + lastSubNode.getLength();
}
if(textBlock.isOffsetRelative()) {
tbOffset = getAbsoluteOffset(textBlock);
} else {
tbOffset = textBlock.getOffset();
}
}
int newlength = lastEnd - tbOffset;
return newlength;
}
/**
* makes the subnodes relative to the offset of the given {@link TextBlock}
* this is done recursively for all subBlocks of the given block.
* @param tb
* @return
*/
public static void makeRelativeOffsetRecursively(TextBlock tb) {
for (Object subNode : getSubNodes(tb)) {
if(subNode instanceof TextBlock) {
makeRelativeOffsetRecursively((TextBlock) subNode);
}
}
updateTextBlockLocationUsingSubNodesAfterAdding(tb);
makeSubNodesRelative(tb);
//TbValidationUtil.assertTextBlockConsistencyRecursive(tb);
}
/**
* @param textBlock
* @param subNodes
*/
public static void makeSubNodesRelative(TextBlock textBlock) {
List<? extends DocumentNode> subNodes = getSubNodes(textBlock);
// finally update all offsets by making them relative to the new block
int lastOffset = 0;
int lastLength = 0;
for (DocumentNode documentNode : subNodes) {
if (!documentNode.isOffsetRelative() ) {
//int newOffSet = documentNode.getOffset() - getAbsoluteOffset(textBlock);
int newOffSet = lastOffset + lastLength;
if (newOffSet < 0) {
if (! (documentNode instanceof Eostoken)) {
throw new IllegalArgumentException("BUG: Attempt to set negative Offset: " + newOffSet);
} else {
continue;
}
}
documentNode.setOffset(newOffSet);
documentNode.setOffsetRelative(true);
}
lastOffset = documentNode.getOffset();
lastLength = documentNode.getLength();
}
}
public static void relocateToken(AbstractToken subNode, int i, TextBlock tb) {
//make offset absolute before moving it to the new block
//TODO if offset handling externalized this has to be changed
int originalAbsoluteOffset = getAbsoluteOffset(subNode);
TextBlock oldParent = subNode.getParent();
if(oldParent.isOffsetRelative()) {
oldParent.setOffset(getAbsoluteOffset(oldParent));
oldParent.setOffsetRelative(false);
for (DocumentNode child : getSubNodes(oldParent)) {
if(child.isOffsetRelative()){
child.setOffset(getAbsoluteOffset(child));
child.setOffsetRelative(false);
}
}
}
// //if element is at the edge of the subblock change its boundaries
// if(firstTokenWithoutBOS(oldParent).equals(subNode)) {
// //updateOffsetAscending(oldParent, oldParent.getOffset() - subNode.getLength());
// TbChangeUtil.updateLengthAscending(oldParent, - subNode.getLength());
// TbChangeUtil.updateOffsets(oldParent, -subNode.getLength());
// } else if (getSubNodeAt(oldParent, getSubNodesSize(oldParent)-1).equals(subNode)){
// TbChangeUtil.updateLengthAscending(oldParent, - subNode.getLength());
// //TbChangeUtil.updateOffsets(oldParent, -subNode.getLength());
// }
subNode.setOffset(originalAbsoluteOffset);
subNode.setOffsetRelative(false);
subNode.setParent(null);
DocumentNode subNodeAtI = getSubNodeAt(tb, i);
if(subNodeAtI != null && getAbsoluteOffset(subNodeAtI) == originalAbsoluteOffset) {
TextBlock parentToCheck = oldParent;
parentToCheck.setOffset(originalAbsoluteOffset + subNode.getLength());
parentToCheck.setOffsetRelative(false);
updateParentsAscendingAfterRemoval(parentToCheck);
}
//end TODO
TbChangeUtil.addToBlockAt(tb, i, subNode);
updateTextBlockLocationUsingSubNodesAfterAdding(tb);
updateParentsAscendingAfterAdding(tb);
//if the token is added to the parent block of the original block
//we need to make sure that the original block and the relocated token do not have the same
//offsets.
}
}