package com.sap.furcas.runtime.textblocks; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.util.EcoreUtil.Copier; import org.eclipse.ocl.ecore.opposites.DefaultOppositeEndFinder; import org.eclipse.ocl.ecore.opposites.OppositeEndFinder; import com.sap.furcas.metamodel.FURCAS.TCS.ClassTemplate; import com.sap.furcas.metamodel.FURCAS.TCS.Property; import com.sap.furcas.metamodel.FURCAS.TCS.QualifiedNamedElement; import com.sap.furcas.metamodel.FURCAS.textblocks.AbstractToken; import com.sap.furcas.metamodel.FURCAS.textblocks.Bostoken; import com.sap.furcas.metamodel.FURCAS.textblocks.DocumentNode; import com.sap.furcas.metamodel.FURCAS.textblocks.Eostoken; import com.sap.furcas.metamodel.FURCAS.textblocks.LexedToken; import com.sap.furcas.metamodel.FURCAS.textblocks.OmittedToken; import com.sap.furcas.metamodel.FURCAS.textblocks.TextBlock; import com.sap.furcas.metamodel.FURCAS.textblocks.TextblocksPackage; import com.sap.furcas.metamodel.FURCAS.textblocks.Version; import com.sap.furcas.runtime.textblocks.shortprettyprint.ShortPrettyPrinter; public class TbUtil { // Disabled for now. See TbUtil#findTextBlockOf(...). // // /** // * A simple scope provider that can only be used to navigate from the domain // * to the textblocks model. // */ // private static final class TextBlocksPartitonQueryContextProvider implements QueryContextProvider { // // private final Set<URI> scope; // private final ResourceSet resourceSet; // // public TextBlocksPartitonQueryContextProvider(ResourceSet resourceSet, Set<URI> scope) { // this.resourceSet = resourceSet; // this.scope = scope; // } // // @Override // public QueryContext getForwardScopeQueryContext(Notifier context) { // throw new UnsupportedOperationException("Cannot navigate from domain to view"); // } // @Override // public QueryContext getBackwardScopeQueryContext(Notifier context) { // return EcoreHelper.getRestrictedQueryContext(resourceSet, scope); // } // } /** * Computes the absolute offset of the given {@link DocumentNode} by traversing all transitive * parents recursively. * * * @param node * @return the absolute offset of the given {@link DocumentNode} */ public static int getAbsoluteOffset(DocumentNode node) { // implementation without isOffsetRelative /* * * int absoluteOffset = node.getOffset(); TextBlock parent = * TbNavigationUtil.getParentBlock(node); while (parent != null) { absoluteOffset += * parent.getOffset(); parent = parent.getParentBlock(); } * * return absoluteOffset; */ if (node.isOffsetRelative()) { if (node.getParent() != null) { return getAbsoluteOffset(node.getParent()) + node.getOffset(); } else { return node.getOffset(); } } else { return node.getOffset(); } } /** * Searches the newest version of the textblock starting from CURRENT, then PREVIOUS else * REFERENCE * * @param tb * @return */ @SuppressWarnings("unchecked") public static <T extends DocumentNode> T getNewestVersion(T tb) { // TODO is it possible that there is already a CURRENT version? if (Version.CURRENT.equals(tb.getVersion())) { return tb; } for (DocumentNode tbv : tb.getOtherVersions()) { if (tbv.getVersion().equals(Version.CURRENT)) { return (T) tbv; } else if (tbv.getVersion().equals(Version.PREVIOUS)) { tb = (T) tbv; } } return tb; } /** * Creates a new copy of the given {@link DocumentNode} having the given newVersion set as * version. THe whole containment structure will also be copied. The new version will also be added * to the <code>otherVersions</code> reference and vice versa. * * @param node * the rootBlock that should be copied, make sure that it has the correct * version, as only this version of the whole tree will be copied. All other * elements will only be referenced by the new version. * @param newVersion * The target version that will be created. * @param manifestValues * If set to true all token values will be manifested using the {@link ShortPrettyPrinter}. * This means that if the given input tb model s a flyweight it will be turned into a complete * editable representation. * @param shortPrettyPrinter * The pretty printer used for the manifestation of token values. If <code>manifestValues</code> * is set to false this may be <code>null</code>; * @return the copied version of <code>rootBlock</code> */ public static DocumentNode createNewCopy(DocumentNode node, final Version newVersion, boolean manifestValues, ShortPrettyPrinter shortPrettyPrinter) { final Map<EObject, EObject> copiedElements = new HashMap<EObject, EObject>(); Copier copier = new EcoreUtil.Copier(true, true) { /** * */ private static final long serialVersionUID = 1L; @Override public EObject copy(EObject eObject) { EObject copied = super.copy(eObject); copiedElements.put(eObject, copied); ((DocumentNode) copied).setVersion(newVersion); return copied; } }; DocumentNode newCopy = (DocumentNode) copier.copy(node); copier.copyReferences(); if (manifestValues) { if (newCopy instanceof LexedToken) { ((LexedToken) newCopy).setValue(shortPrettyPrinter .resynchronizeToEditableState((AbstractToken) newCopy)); } } // as the original already had a reference to its other // versions these would be duplicated, therefore clear this // association before referencing again // TODO this could be improved if deepCopy would allow separate // handling of specific references // Collection<DocumentNode> referencingNodes = // ((TextblocksPackage) // copiedNode // .refImmediatePackage()) // .getDocumentNodeHasDocumentNodeVersions().getDocumentNode( // copiedNode); // TODO: this is only need because obviously deepCopy also // duplicates references from "outside" // to elements that are being copied, this leads to duplicated // version references if // // referenceVersions is called // for (DocumentNode referencingNode : new // ArrayList<DocumentNode>( // referencingNodes)) { // referencingNode.getOtherVersions().remove(copiedNode); // } // copiedNode.getOtherVersions().clear(); for (Entry<EObject, EObject> entry : copiedElements.entrySet()) { DocumentNode copiedNode = (DocumentNode) entry.getValue(); copiedNode.getOtherVersions().clear(); referenceVersions((DocumentNode) entry.getKey(), copiedNode); } // per default assign to the same partition // FIXME: how are we supposed to handle this? if (((EObject) node).eResource() != null) { ((EObject) node).eResource().getContents().add(newCopy); } return newCopy; } /** * References the versions of <code>n1</code> and <code>n2</code> with each other. Also * previously existing other versions of <code>n1</code> and <code>n2</code> will be * referenced by this. * * @param n1 * @param n2 */ public static void referenceVersions(DocumentNode n1, DocumentNode n2) { if (n1.equals(n2)) { return; } if (n1.getOtherVersions().size() > 0) { // Also add reference to other existing versions for (DocumentNode existingVersion : n1.getOtherVersions()) { if (!n2.getOtherVersions().contains(existingVersion)) { if (n2 != existingVersion) { existingVersion.getOtherVersions().add(n2); n2.getOtherVersions().add(existingVersion); } } } } if (n2.getOtherVersions().size() > 0) { // Also add reference to other existing versions for (DocumentNode existingVersion : n2.getOtherVersions()) { if (!n1.getOtherVersions().contains(existingVersion)) { if (n1 != existingVersion) { existingVersion.getOtherVersions().add(n1); n1.getOtherVersions().add(existingVersion); } } } } if (!n1.getOtherVersions().contains(n2)) { n1.getOtherVersions().add(n2); } if (!n2.getOtherVersions().contains(n1)) { n2.getOtherVersions().add(n1); } } /** * Helper function that returns teh sublist without the leading bos and trailing eos tokens. * Throws IllegalArgumentExceptions, if bos or eos is not found in the right place. * * @param nodes * nodes list to trim * @return nodes list without leading eos and trailing bos tokens */ public static List<? extends DocumentNode> withoutBosEos(List<? extends DocumentNode> nodes) { if (nodes.size() < 2) { throw new IllegalArgumentException("nodes do not contain both bos and eos"); } if (!(nodes.get(0) instanceof Bostoken)) { throw new IllegalArgumentException("first token not bos"); } if (!(nodes.get(nodes.size() - 1) instanceof Eostoken)) { throw new IllegalArgumentException("last token not eos"); } return nodes.subList(1, nodes.size() - 1); } /** * Helper function that creates a map with the node and all hierarchical parents mapped to their * hierarchy level * * @param node * DocumentNode to start at * @param level * hierarchy level of the node * @return filled node level map */ public static Map<Integer, DocumentNode> createNodeLevelMap(DocumentNode node, int level) { Map<Integer, DocumentNode> levelMap = new HashMap<Integer, DocumentNode>(); levelMap.put(level, node); TextBlock parent = node.getParent(); Integer curParentLevel = level - 1; while (parent != null) { levelMap.put(curParentLevel, parent); parent = parent.getParent(); curParentLevel--; } return levelMap; } /** * Returns the relative offset (position) of the absoluteOffset in relation to the start offset * of the given node. * * @param absoluteOffset * offset absolute to the beginning of the document text * @param node * node to find relative offset to * @return relative offset to the node start */ public static int getRelativeOffsetFromNode(int absoluteOffset, DocumentNode node) { return absoluteOffset - getAbsoluteOffset(node); } /** * returns true if node has a parent in parent hierarchy which is parentBlock * * @param parentBlock * @param documentNode * @return */ public static boolean isAncestorOf(TextBlock parentBlock, DocumentNode node) { if (node == null) { return false; } TextBlock loopParentBlock = node.getParent(); while (loopParentBlock != null) { // could be infinite loop with cylces, but model prevents cycles if (loopParentBlock.equals(parentBlock)) { return true; } loopParentBlock = loopParentBlock.getParent(); } return false; } public static void dereferenceVersions(DocumentNode v1, DocumentNode v2) { v1.getOtherVersions().remove(v2); v2.getOtherVersions().remove(v1); // also remove from any remaining versions for (DocumentNode otherVersion : v1.getOtherVersions()) { otherVersion.getOtherVersions().remove(v2); } for (DocumentNode otherVersion : v2.getOtherVersions()) { otherVersion.getOtherVersions().remove(v1); } } public static void referenceVersions(DocumentNode nodeToAddVersionsTo, List<DocumentNode> otherVersions) { for (DocumentNode otherVersion : otherVersions) { referenceVersions(nodeToAddVersionsTo, otherVersion); } } /** * Searches in the {@link DocumentNode#getCorrespondingModelElements()} of the given TextBlock * for an element that matches the {@link QualifiedNamedElement#getMetaReference()} of the * template that was used to create the textblock. This is not a hundred percent guarantee, as * multiple elements may reside in the corresponding model elements. * * @param parentBlock * @return The element from {@link DocumentNode#getCorrespondingModelElements()} that was most * probably created using the template of the {@link TextBlock}. <code>null</code> if * no element matches this criterion. */ public static EObject getCreatedElement(TextBlock parentBlock) { for (EObject ro : parentBlock.getCorrespondingModelElements()) { if (parentBlock.getType() != null && parentBlock.getType() != null) { if (ro.eClass().equals(parentBlock.getType().getMetaReference())) { return ro; } } } return null; } /** * Gives the absolute offset of the first token that is not an {@link OmittedToken} and that is still * transitively contained in parentBlock. * * @param parentBlock * @return */ public static int getAbsoluteOffsetWithoutBlanks(TextBlock parentBlock) { AbstractToken leadingTok = TbNavigationUtil.firstTokenWithoutBOS(parentBlock); while(leadingTok instanceof OmittedToken && TbUtil.isAncestorOf(parentBlock, leadingTok)){ leadingTok = TbNavigationUtil.nextToken(leadingTok); } return TbUtil.getAbsoluteOffset(leadingTok); } /** * Gives the length of the block starting from first token that is not an {@link OmittedToken} and that is still * transitively contained in parentBlock. * * @param parentBlock * @return */ public static int getLengthWithoutStartingBlanks(TextBlock parentBlock) { int absoluteOffsetTok = getAbsoluteOffsetWithoutBlanks(parentBlock); return parentBlock.getLength() - (absoluteOffsetTok - TbUtil.getAbsoluteOffset(parentBlock)); } public static boolean isTextBlockOfType(ClassTemplate rootTemplate, TextBlock block) { return block.getType() != null && block.getType() != null && block.getType().equals(rootTemplate); } public static boolean isEmpty(TextBlock oldVersion) { if (TbNavigationUtil.firstToken(oldVersion) == null) { return true; } if (TbNavigationUtil.firstToken(oldVersion) instanceof Bostoken && (TbNavigationUtil.nextToken(TbNavigationUtil.firstToken(oldVersion)) == null || TbNavigationUtil .nextToken(TbNavigationUtil.firstToken(oldVersion)) instanceof Eostoken)) { return true; } if (TbNavigationUtil.getSubNodesSize(oldVersion) > 2) { return false; } else if (TbNavigationUtil.getSubNodesSize(oldVersion) < 2 && TbNavigationUtil.getSubNodesSize(oldVersion) > 0) { return false; } else if (TbNavigationUtil.getSubNodesSize(oldVersion) == 2) { return TbNavigationUtil.getSubNodeAt(oldVersion, 0) instanceof Bostoken && TbNavigationUtil.getSubNodeAt(oldVersion, 1) instanceof Eostoken; } else { return true; } } public static Collection<TextBlock> filterVersionedTextBlockForNewest( Collection<TextBlock> tbs) { for (TextBlock textBlock : new ArrayList<TextBlock>(tbs)) { if (textBlock == null) { System.out.println("WTF tb was null"); tbs.remove(null); continue; } if(!textBlock.equals(TbUtil.getNewestVersion(textBlock))) { tbs.remove(textBlock); } } return tbs; } /** * Find the TextBlock representing the given modelElement within the tree under * the given rootBlock. */ public static Collection<TextBlock> findTextBlockOf(TextBlock rootBlock, EObject modelElement, ResourceSet resourceSet) { ArrayList<TextBlock> result = new ArrayList<TextBlock>(); OppositeEndFinder endFinder = getOppositeEndFinder(); for (EObject o : endFinder.navigateOppositePropertyWithBackwardScope( TextblocksPackage.eINSTANCE.getTextBlock_CorrespondingModelElements(), modelElement)) { TextBlock tb = (TextBlock) o; if (EcoreUtil.getRootContainer(o) == rootBlock && tb.getVersion() == rootBlock.getVersion()) { result.add(tb); } } return result; } /** * Find all DocumentNodes representing the given property on the given modelElement */ public static Collection<DocumentNode> findTokensFor(EObject modelElement, Property property, Version version) { ArrayList<DocumentNode> result = new ArrayList<DocumentNode>(); OppositeEndFinder endFinder = getOppositeEndFinder(); for (EObject o : endFinder.navigateOppositePropertyWithBackwardScope( TextblocksPackage.eINSTANCE.getDocumentNode_SequenceElement(), property)) { DocumentNode node = (DocumentNode) o; if (node.getVersion() == version && correspondsTo(modelElement, node)) { result.add(node); } } return result; } private static boolean correspondsTo(EObject modelElement, DocumentNode node) { TextBlock tb = (node instanceof TextBlock) ? (TextBlock) node : node.getParent(); if (tb == null) { return false; } return tb.getCorrespondingModelElements().contains(modelElement); } private static OppositeEndFinder getOppositeEndFinder() { // HashSet<URI> scope = new HashSet<URI>(2); // scope.add(modelElement.eResource().getURI()); // scope.add(rootBlock.eResource().getURI()); // FIXME: Disabled. Otherwise a reference resolving tests fails because // query2 is unable to resolve an existing reference.... Stephan Erb, 17.05.2011 // new Query2OppositeEndFinder(new TextBlocksPartitonQueryContextProvider(resourceSet, scope)); OppositeEndFinder endFinder = DefaultOppositeEndFinder.getInstance(); return endFinder; } }