/** * */ package com.sap.furcas.runtime.textblocks.modifcation; import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.getSubNodesSize; import static com.sap.furcas.runtime.textblocks.TbUtil.getNewestVersion; import static com.sap.furcas.runtime.textblocks.TbUtil.getRelativeOffsetFromNode; import java.util.ArrayList; import java.util.List; import org.eclipse.emf.ecore.util.EcoreUtil; import com.sap.furcas.metamodel.FURCAS.textblocks.AbstractToken; import com.sap.furcas.metamodel.FURCAS.textblocks.DocumentNode; import com.sap.furcas.metamodel.FURCAS.textblocks.TextBlock; import com.sap.furcas.metamodel.FURCAS.textblocks.Version; import com.sap.furcas.runtime.textblocks.TbNavigationUtil; import com.sap.furcas.runtime.textblocks.TbUtil; import com.sap.furcas.runtime.textblocks.shortprettyprint.ShortPrettyPrinter; /** * */ public class TbChangeUtil { /** * Clean up the textblocks model, i.e., delete all other versions except the * current one and store remaining elements as reference version; * * @return Returns the (new) reference version of the rootBlock */ public static DocumentNode cleanUp(DocumentNode rootBlock) { DocumentNode currentVersion = getNewestVersion(rootBlock); deleteOtherVersions(currentVersion); makeReferenceVersion(currentVersion); return currentVersion; } public static void delete(DocumentNode node) { // Only delete the given root node. Remove all its children from their resource, but keep the tree intact. // This is required by the editor environment which might be concurrently iterating // over an outdated tb version. EcoreUtil.delete(node); deleteRecursive(node); } private static void deleteRecursive(DocumentNode node) { if (node.eResource() != null) { node.eResource().getContents().remove(node); } // A simple delete does not work here. It only deletes from // enclosing entities but not from other versions that still reference us. for (DocumentNode remainingVersion : new ArrayList<DocumentNode>(node.getOtherVersions())) { remainingVersion.getOtherVersions().remove(node); } if (node instanceof TextBlock) { for (DocumentNode subNode : ((TextBlock) node).getSubNodes()) { deleteRecursive(subNode); } } } public static void deleteOtherVersions(DocumentNode versionToKeep) { for (DocumentNode oldVersion : new ArrayList<DocumentNode>(versionToKeep.getOtherVersions())) { versionToKeep.getOtherVersions().remove(oldVersion); delete(oldVersion); } } /** * Revert the textblocks model by delete all newer versions. * * @return Returns the (old) newest version of the rootBlock */ public static DocumentNode revertToVersion(DocumentNode rootBlock, Version revertToVersion) { DocumentNode currentVersion = TbVersionUtil.getOtherVersion(rootBlock, revertToVersion); for (DocumentNode outdatedVersion : new ArrayList<DocumentNode>(currentVersion.getOtherVersions())) { if (isNewer(outdatedVersion.getVersion(), revertToVersion)) { delete(outdatedVersion); } } return currentVersion; } /** * checks whether the first given version is newer than the second one. * @param first * @param second * @return <code>true</code> if the first version is newer, <code>false</code> else. */ public static boolean isNewer(Version first, Version second) { if (first.equals(second)) { return false; } if (Version.CURRENT.equals(first)) { return true; } else if (Version.PREVIOUS.equals(first)) { return Version.REFERENCE.equals(second); } else if (Version.REFERENCE.equals(first)) { return false; } //this should never happen!! return false; } /** * Iterates over all elements within the textblocks tree and sets its * version to REFERENCE and resets relexing needed to false. * * @param currentVersion */ public static void makeReferenceVersion(DocumentNode currentVersion) { currentVersion.setVersion(Version.REFERENCE); currentVersion.setRelexingNeeded(false); currentVersion.setChildrenChanged(false); if (currentVersion instanceof TextBlock) { for (DocumentNode subNode : TbNavigationUtil.getSubNodes((TextBlock) currentVersion)) { makeReferenceVersion(subNode); } } } /** * Sets the version of the TextBlock and all contained elements to the given * version. * * @param rootBlock * TextBlock to start setting the version * @param version * Version to use */ public static void makeVersion(TextBlock rootBlock, Version version) { rootBlock.setVersion(version); for (AbstractToken token : rootBlock.getTokens()) { token.setVersion(version); } for (TextBlock subBlock : rootBlock.getSubBlocks()) { makeVersion(subBlock, version); } } /** * Updates the offsets of all nodes to the right of the passed document * node. Updates are executed upwards, but the tree is never descended. * * Important: Does no error checking, as it would be computationally * expensive. Normally do not substract more than the tokens length. * * @param workingCopy * @param lengthToAdd * Length to add. Can be negative. */ public static void updateOffsets(DocumentNode workingCopy, int lengthToAdd) throws IllegalArgumentException { updateOffsetsWithinTextBlock(workingCopy, lengthToAdd); workingCopy = workingCopy.getParent(); TextBlock parentBlock = workingCopy.getParent(); while (parentBlock != null) { updateOffsetsWithinTextBlock(workingCopy, lengthToAdd); workingCopy = workingCopy.getParent(); parentBlock = workingCopy.getParent(); } } /** * Updates the offsets of all nodes to the right of the passed document * node within the given textblock. * * @param workingCopy * @param lengthToAdd * Length to add. Can be negative. */ public static void updateOffsetsWithinTextBlock(DocumentNode workingCopy, int lengthToAdd) throws IllegalArgumentException { TextBlock parentBlock = workingCopy.getParent(); int index = parentBlock.getSubNodes().indexOf(workingCopy); for (int i=index+1 ; i < parentBlock.getSubNodes().size(); i++) { DocumentNode node = parentBlock.getSubNodes().get(i); // always update EOSToken if it is contained within the affected textblock int newOffset = node.getOffset() + lengthToAdd; if (newOffset < 0) { throw new IllegalStateException("BUG: Attempt to set offset to negative int."); } node.setOffset(newOffset); } } /** * Updates the node including its direct and transitive parents by adding * <code>lengthToAdd</code> to its {@link DocumentNode#getLength()} * recursively. Starts at the given node. * * @param node * Node to update * @param lengthToAdd * Length to add. Can be negative. * @exception IllegalArgumentException * cannot substract more than the current length */ public static void updateLengthAscending(DocumentNode node, int lengthToAdd) throws IllegalArgumentException { int newLength = node.getLength() + lengthToAdd; if (newLength < 0) { throw new IllegalArgumentException("cannot substract more than the current length " + node.getLength() + "<" + (-lengthToAdd)); } node.setLength(newLength); TextBlock parent = node.getParent(); if (parent != null) { updateLengthAscending(parent, lengthToAdd); } } /** * Removes the given token from the textblock and ensures that the lengths of its transitive parents * are also adjusted. * @param currentTextBlock * @param oldTokenInCurrentBlock */ public static void removeFromBlockConsistent(TextBlock currentTextBlock, AbstractToken oldTokenInCurrentBlock) { currentTextBlock.getSubNodes().remove(oldTokenInCurrentBlock); //reduce by length of PREVIOUS version as parernt block still compute with this length AbstractToken previousVersion = TbVersionUtil.getOtherVersion(oldTokenInCurrentBlock, Version.PREVIOUS); TbChangeUtil.updateLengthAscending(currentTextBlock, -previousVersion.getLength()); } /** * replaces in the tokens value the specified region by the new text. * Updates all other offsets and length in the tree consistently. Marks all * parents with the childrenChanged flag. * * @param firstToken * @param newText * @param offsetInNode * @param replacedRegionLength * @exception IllegalArgumentException * replace region invalid */ public static void replaceTokenContents(AbstractToken firstToken, String newText, int offsetInNode, final int replacedRegionLength, ShortPrettyPrinter shortPrettyPrinter) throws IllegalArgumentException { if (replacedRegionLength < 0) { throw new IllegalArgumentException("replace region length invalid : " + replacedRegionLength); } if (offsetInNode < 0) { // throw new IllegalArgumentException("replace region invalid"); // this means the node has to be moved to its left, because there is a // gap between the new content and the start of the node // we have to analyse the gap to see whether there even is one and whether it is big enough AbstractToken previous = TbNavigationUtil.previousToken(firstToken); int absoluteOffset = TbUtil.getAbsoluteOffset(firstToken); if (previous != null) { if (absoluteOffset + offsetInNode < TbUtil.getAbsoluteOffset(previous) + previous.getLength()) { throw new IllegalArgumentException("BUG: Attempt to move token to overlap with previous token."); } // now recursively shift nodes left if possible throw new RuntimeException("BUG: TODO Gaps in TextBlocksmodel replace not implemented yet."); } else { // previous is null, we hold the first node of the whole tree if (absoluteOffset + offsetInNode < 0) { // should never happen throw new RuntimeException("BUG: Attempt to move token before position 0 of text."); } // now recursively shift nodes left if possible throw new RuntimeException("BUG: TODO Gaps in TextBlocksmodel replace not implemented yet."); } } // update token value String originalText = firstToken.getValue(); if (originalText == null) { originalText = shortPrettyPrinter.resynchronizeToEditableState(firstToken); } if (originalText == null) { if (offsetInNode != 0 || replacedRegionLength != 0) { throw new IllegalArgumentException("replace region invalid:" + offsetInNode + "," + replacedRegionLength); } firstToken.setValue(newText); } else { if (offsetInNode + replacedRegionLength > originalText.length()) { throw new IllegalArgumentException("replace region invalid " + (offsetInNode + "+" + replacedRegionLength) + ">" + originalText.length()); } firstToken.setValue(originalText.substring(0, offsetInNode) + newText + originalText.substring(offsetInNode + replacedRegionLength, originalText.length())); } // a deletion is the replacement where length > text.length int lengthToUpdate = newText.length() - replacedRegionLength; int newLength = firstToken.getLength() + lengthToUpdate; if (newLength < 0) { throw new IllegalArgumentException("cannot substract more than the current length " + firstToken.getLength() + "<" + (-lengthToUpdate)); } firstToken.setLength(newLength); } public static void replaceTokenContentsAndUpdateParents(AbstractToken firstToken, String newText, int offsetInNode, int replacedRegionLength, ShortPrettyPrinter shortPrettyPrinter) throws IllegalArgumentException { replaceTokenContents(firstToken, newText, offsetInNode, replacedRegionLength, shortPrettyPrinter); // a deletion is the replacement where length > text.length int lengthToUpdate = newText.length() - replacedRegionLength; // update length and offsets updateLengthAscending(firstToken.getParent(), lengthToUpdate); updateOffsets(firstToken, lengthToUpdate); // mark as changed TbMarkingUtil.mark(firstToken); TbChangeUtil.markAscending(firstToken.getParent()); } /** * Removes the node from the parent TextBlock, and any other jmi reference. * * Recursively removes now empty parent TextBlocks in the same manner. * * @param token * token to be removed */ public static void removeNode(DocumentNode node) { if (node != null) { TextBlock parent = node.getParent(); TbChangeUtil.delete(node); removeTextBlockIfEmpty(parent); } } /** * If the TextBlock has no children (tokens or subblocks), removes it from * the locationMap, from the parent TextBlock, and any other jmi reference. * * Recursively removes now empty parent TextBlocks in the same manner. * * @param textblock * the TextBlock to be removed */ public static void removeTextBlockIfEmpty(TextBlock textblock) { if (textblock != null) { if (getSubNodesSize(textblock) == 0) { TextBlock parent = textblock.getParent(); TbChangeUtil.delete(textblock); removeTextBlockIfEmpty(parent); } } } public static void replaceLeftIntersectionWithText(AbstractToken workingCopy, int replacedRegionOffsetTo, String newText, ShortPrettyPrinter shortPrettyPrinter) { int offsetInTokenTo = getRelativeOffsetFromNode(replacedRegionOffsetTo, workingCopy); // replace intersecting region with an empty string // since we replace from start, offsetInTokenTo == length replaceTokenContents(workingCopy, newText, 0, /*length*/offsetInTokenTo, shortPrettyPrinter); } /** * Marks all transitive parents of the starting {@link TextBlock} <code>start</code> as * modified by setting {@link DocumentNode#setChildrenChanged(boolean)} to <code>true</code>; * @param start The {@link TextBlock} to begin the marking with. */ public static void markAscending(TextBlock start) { if (start != null) { start.setChildrenChanged(true); TextBlock parent; if ((parent = start.getParent()) != null) { markAscending(parent); } } } /** * Updates the length of the given textblock by computing it from its subnodes. * If the length really changed it is also propagated to its parent blocks. * * @param tb */ public static void updateBlockLength(TextBlock tb) { int oldLength = tb.getLength(); List<? extends DocumentNode> nodes = TbNavigationUtil.getSubNodes(tb); DocumentNode firstNode = nodes.get(0); DocumentNode lastNode = nodes.get(nodes.size() - 1); int newLength = 0; if (!firstNode.isOffsetRelative() || !lastNode.isOffsetRelative()) { newLength = TbUtil.getAbsoluteOffset(lastNode) + lastNode.getLength() - TbUtil.getAbsoluteOffset(firstNode); } else { newLength = lastNode.getOffset() + lastNode.getLength() - firstNode.getOffset(); } if (oldLength != newLength) { tb.setLength(newLength); if (tb.getParent() != null) { updateLengthAscending(tb.getParent(), newLength - oldLength); } } } /** * Adds the newModel to the given position within the subNodes of the parentBlock. * @param parentBlock * @param index * @param element */ public static void addToBlockAt(TextBlock parentBlock, int index, DocumentNode element) { parentBlock.getSubNodes().add(index, element); } }