package com.sap.ide.cts.parser.incremental; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EModelElement; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.ocl.ecore.opposites.OppositeEndFinder; import com.sap.furcas.metamodel.FURCAS.TCS.AndExp; import com.sap.furcas.metamodel.FURCAS.TCS.AtomExp; import com.sap.furcas.metamodel.FURCAS.TCS.ConditionalElement; import com.sap.furcas.metamodel.FURCAS.TCS.OperatorTemplate; import com.sap.furcas.metamodel.FURCAS.TCS.Property; import com.sap.furcas.metamodel.FURCAS.TCS.PropertyReference; import com.sap.furcas.metamodel.FURCAS.TCS.SequenceElement; import com.sap.furcas.metamodel.FURCAS.TCS.Template; import com.sap.furcas.metamodel.FURCAS.textblocks.AbstractToken; import com.sap.furcas.metamodel.FURCAS.textblocks.DocumentNode; 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.runtime.common.interfaces.IModelElementProxy; import com.sap.furcas.runtime.common.util.EcoreHelper; import com.sap.furcas.runtime.parser.IModelInjector; import com.sap.furcas.runtime.parser.exceptions.ModelCreationOntheFlyRuntimeException; import com.sap.furcas.runtime.parser.impl.ModelElementProxy; import com.sap.furcas.runtime.parser.textblocks.observer.TextBlockProxy; import com.sap.furcas.runtime.tcs.TcsUtil; import com.sap.furcas.runtime.textblocks.TbNavigationUtil; import com.sap.furcas.runtime.textblocks.TbUtil; import com.sap.furcas.runtime.textblocks.modifcation.TbChangeUtil; public class IncrementalParsingUtil { public static class CompositeRefAssociationBean { public CompositeRefAssociationBean(EReference refAssoc, boolean isParentFirstEnd) { compositeFeatureAssoc = refAssoc; this.isParentFirstEnd = isParentFirstEnd; } public EReference compositeFeatureAssoc; public boolean isParentFirstEnd; } static AbstractToken firstToken(Object node) { while (!isToken(node) && node != null) { node = getSubNodeAt(((TextBlockProxy) node), 0); } return (AbstractToken) node; } static AbstractToken lastToken(Object node) { while (!isToken(node) && node != null) { node = getSubNodeAt(((TextBlockProxy) node), ((TextBlockProxy) node).getSubNodes().size() - 1); } return (AbstractToken) node; } static Object getSubNodeAt(TextBlockProxy textBlockProxy, int i) { return textBlockProxy.getSubNodes().get(i); } static boolean isToken(Object node) { return node instanceof AbstractToken; } /** * Sets the feature that holds the corresponding value of the elements that are stored in the correspondingmodel elements of * the <code>newVersion</code> using the parent Element taken from the given <code>parentTextBlock</code>. * * @param newVersion * @param parentTextBlock * @param modelInjector * @return */ static SetNewFeatureBean setFeatureWithNewValue(TextBlockProxy newVersion, TextBlock parentTextBlock, OppositeEndFinder oppositeEndFinder) { if (newVersion.getParent() != null) { int indexInCorrespondingElements = 0; for (IModelElementProxy parentProxy : newVersion.getParent().getCorrespondingModelElementProxies()) { // first find corresponding attribute within parent proxy for (String key : ((ModelElementProxy) parentProxy).getAttributeMap().keySet()) { int valueIndex = 0; for (Object value : ((ModelElementProxy) parentProxy).getAttributeMap().get(key)) { for (IModelElementProxy proxy : newVersion.getCorrespondingModelElementProxies()) { if (proxy.equals(value)) { // found element to be replaced EObject parentRefObject = getCorrespondingElement(newVersion.getParent(), proxy, key, indexInCorrespondingElements, parentTextBlock, oppositeEndFinder); if (parentRefObject != null) { return new SetNewFeatureBean(parentRefObject, key, proxy.getRealObject(), valueIndex); } } } for (IModelElementProxy proxy : newVersion.getReferencedElementProxies()) { if (proxy.equals(value)) { // found element to be replaced EObject parentRefObject = getCorrespondingElement(newVersion.getParent(), proxy, key, indexInCorrespondingElements, parentTextBlock, oppositeEndFinder); if (parentRefObject != null) { return new SetNewFeatureBean(parentRefObject, key, proxy.getRealObject(), valueIndex); } } } valueIndex++; } } indexInCorrespondingElements++; } } return null; } static EObject getCorrespondingElement(TextBlockProxy textblock, IModelElementProxy proxy, String propertyName, int i, TextBlock parentBlock, OppositeEndFinder oppositeEndFinder) { // TODO this is an initial implementation which might not be perfect // as it just takes the type of the element Collection<EClassifier> metaClasses = new ArrayList<EClassifier>(); metaClasses.add(textblock.getTemplate().getMetaReference()); for (Template additionalTempl : textblock.getAdditionalTemplates()) { metaClasses.add(additionalTempl.getMetaReference()); } for (EObject element : parentBlock.getCorrespondingModelElements()) { for (EClassifier classifier : metaClasses) { if (classifier.isInstance(element)) { EModelElement result = element.eClass().getEStructuralFeature(propertyName); if (result != null) { return element; } else if (oppositeEndFinder.getAllOppositeEnds(classifier).containsKey(propertyName)) { // there is a hidden opposite with the specified name, so retunr the element return element; } else { continue; } } } } return null; } static TextBlock getOriginalVersion(TextBlockProxy newVersion, TextBlock parent) { AbstractToken firstToken = firstToken(newVersion); AbstractToken tok = firstToken; AbstractToken lastToken = lastToken(newVersion); while (tok instanceof OmittedToken && !tok.equals(lastToken)) { tok = TbNavigationUtil.nextToken(tok); } if (tok instanceof OmittedToken) { throw new RuntimeException("Found textblock with nothing but omitted tokens in it. This should never happen!"); } TextBlock candidate = tok.getParent(); TextBlock result = candidate; firstToken = getFirstTokenInFirstNonEmptyBlock(candidate); boolean onOffsetRange = TbUtil.getAbsoluteOffset(firstToken) >= TbUtil.getAbsoluteOffset(firstToken(newVersion)); while (candidate != null && onOffsetRange && (parent != null) && !parent.equals(candidate)) { result = candidate; candidate = candidate.getParent(); firstToken = getFirstTokenInFirstNonEmptyBlock(candidate); onOffsetRange = TbUtil.getAbsoluteOffset(firstToken) >= TbUtil.getAbsoluteOffset(firstToken(newVersion)); } return result; } /** * FIXME: The way this method is called, it may happen that parent blocks have empty leading sub-blocks. * NbNavigationUtil.firstToken cannot deal with this. We cannot fix firstToken, because it is also used to determine if a * block is empty and shall be deleted. * * Workaround: Use firstTokenWithoutBOS as a workaround. It does not have this problem and will skip over empty blocks. */ protected static AbstractToken getFirstTokenInFirstNonEmptyBlock(TextBlock candidate) { AbstractToken firstToken; if (TbNavigationUtil.isUltraRoot(candidate)) { firstToken = TbNavigationUtil.firstToken(candidate); } else { firstToken = TbNavigationUtil.firstTokenWithoutBOS(candidate); } return firstToken; } public static void setNewFeature(SetNewFeatureBean newFeatureBean, IModelInjector injector, boolean assignToPartition) { injector.set(newFeatureBean.parentRefObject, newFeatureBean.property, newFeatureBean.value, newFeatureBean.valueIndex); // as default assign elements to the same partition as parents if (newFeatureBean.value instanceof EObject && assignToPartition) { if (!(newFeatureBean.parentRefObject).eResource().equals(((EObject) newFeatureBean.value).eResource())) { (newFeatureBean.parentRefObject).eResource().getContents().add((EObject) newFeatureBean.value); } } } public static void setNewPrimitiveFeature(TextBlockProxy newVersion, TextBlock oldVersion, AbstractToken subNode, IModelInjector injector) { int i = 0; EStructuralFeature compareToProperty = null; if (subNode instanceof LexedToken) { if (((LexedToken) subNode).isOperator() && newVersion.getTemplate() instanceof OperatorTemplate) { PropertyReference storeOperatorTo = ((OperatorTemplate) newVersion.getTemplate()).getStoreOperatorTo(); if (storeOperatorTo != null) { compareToProperty = storeOperatorTo.getStrucfeature(); } else { return; } } else if (((LexedToken) subNode).getSequenceElement() != null && checkIsDefinedOptional(subNode)) { // if the subNode is responsible for a property that is set through // an isDefined clause // then the value to which the token's valuen should be compared is // not the value of the attribute // but rather the value so the property is defined in the parent // isDefined clause SequenceElement parent = TcsUtil.getContainerSequenceElement(((LexedToken) subNode).getSequenceElement()); if (!(parent instanceof ConditionalElement) || !(((ConditionalElement) parent).getCondition() instanceof AndExp) || !(((AndExp) ((ConditionalElement) parent).getCondition()).getExpressions().size() == 1)) { throw new IllegalStateException("isDefined expression expected but got:" + parent.eClass().getName()); } else { AndExp andExp = (AndExp) ((ConditionalElement) parent).getCondition(); AtomExp conditionExpression = andExp.getExpressions().iterator().next(); compareToProperty = conditionExpression.getPropertyReference().getStrucfeature(); } } else { if (!(((LexedToken) subNode).getSequenceElement() instanceof Property)) { // nothing to do for now // TODO check if there are other scenarios where we have to // do something return; } compareToProperty = ((Property) ((LexedToken) subNode).getSequenceElement()).getPropertyReference() .getStrucfeature(); } for (EObject ro : oldVersion.getCorrespondingModelElements()) { if (newVersion.getCorrespondingModelElementProxies().size() >= i + 1) { ModelElementProxy correspondingProxy = (ModelElementProxy) newVersion.getCorrespondingModelElementProxies() .get(i); List<Object> values = correspondingProxy.getAttributeMap().get(compareToProperty.getName()); if (values == null) { // may be deferred reference, so do nothing here i++; continue; } if (values.size() > 1) { // throw new // RuntimeException("Incremental updating of multi valued primitive attributes is currently not supported"); if (!(values.get(0) instanceof IModelElementProxy)) { // if its a proxy its not a primitive value! injector.set(ro, compareToProperty.getName(), values.get(0)); } // TODO log warning } else { if (!(values.get(0) instanceof IModelElementProxy)) { // if its a proxy its not a primitive value! injector.set(ro, compareToProperty.getName(), values.get(0)); } } } else { break; } i++; } } } /** * Checks the case if a token represents an optional sequence element * * E.g.: ...(isDefined(isValueType) ? "value") ... Will result in true if the <tt>nextToken</tt> represents the "value" token * which is optional. * * @param nextToken * @return */ public static boolean checkIsDefinedOptional(AbstractToken candidate) { LexedToken lexedToken = (LexedToken) candidate; SequenceElement se = lexedToken.getSequenceElement(); if (se != null) { // now check if it is contained within a isDefined clause // TODO maybe this has to be done recursively ascending? SequenceElement parent = TcsUtil.getContainerSequenceElement(se); if (parent != null) { if (parent instanceof ConditionalElement) { ConditionalElement conditional = (ConditionalElement) parent; return conditional.getThenSequence().getElements().contains(se); } } } return false; } public static Collection<? extends EObject> deletePreviousEmptyBlocks(TextBlock original) { if (original != null) { TextBlock previous = TbNavigationUtil.previousBlockInSubTree(original); if (previous != null) { Collection<EObject> affectedModelElements = new ArrayList<EObject>(); Collection<TextBlock> deleteTB = new ArrayList<TextBlock>(); for (DocumentNode node : new ArrayList<DocumentNode>(original.getSubNodes())) { if (node instanceof TextBlock) { affectedModelElements.addAll(deletePreviousEmptyBlocks((TextBlock) node)); deleteTB.add((TextBlock) node); } } for (TextBlock textBlock : deleteTB) { affectedModelElements.addAll(deleteEmptyBlocks(textBlock)); } return affectedModelElements; } } return Collections.emptyList(); } public static Collection<? extends EObject> deleteNextEmptyBlocks(TextBlock original) { if (original != null && EcoreHelper.isAlive(original)) { TextBlock next = TbNavigationUtil.nextBlockInSubTree(original); if (next != null) { Collection<EObject> affectedModelElements = new ArrayList<EObject>(); Collection<TextBlock> deleteTB = new ArrayList<TextBlock>(); for (DocumentNode node : new ArrayList<DocumentNode>(original.getSubNodes())) { if (node instanceof TextBlock) { affectedModelElements.addAll(deleteNextEmptyBlocks((TextBlock) node)); deleteTB.add((TextBlock) node); } } for (TextBlock textBlock : deleteTB) { affectedModelElements.addAll(deleteEmptyBlocks(textBlock)); } return affectedModelElements; } } return Collections.emptyList(); } public static Collection<? extends EObject> deleteEmptyBlocks(TextBlock original) { TextBlock tbDeletionCandidate = original; Collection<EObject> affectedModelElements = new ArrayList<EObject>(); if (EcoreHelper.isAlive(original)) { for (DocumentNode node : new ArrayList<DocumentNode>(original.getSubNodes())) { if (node instanceof TextBlock) { affectedModelElements.addAll(deleteEmptyBlocks((TextBlock) node)); } } } if (EcoreHelper.isAlive(original) && TbNavigationUtil.firstToken(tbDeletionCandidate) == null // this may be the case if there are any empty blocks before // remaining tokens // inside the block && tbDeletionCandidate.getTokens().size() == 0) { TextBlock deleteTB = tbDeletionCandidate; if (deleteTB.getCorrespondingModelElements().size() > 0) { affectedModelElements.addAll(deleteTB.getCorrespondingModelElements()); } TbChangeUtil.delete(deleteTB); } return affectedModelElements; } public static void deleteCorrespondingModelElements(EObject parentRefObject, String propertyToDeleteFrom, TextBlock oldVersion, ReferenceHandler referenceHandler) { Collection<EObject> correspondingModelElements = new ArrayList<EObject>(oldVersion.getCorrespondingModelElements()); for (EObject oldModelElement : correspondingModelElements) { // TODO: What if the property is a non referenced association end? // -->search the association and do the check there Object value = referenceHandler.getFeatureValue(parentRefObject, propertyToDeleteFrom); if (value instanceof Collection<?>) { if (((Collection<?>) value).contains(oldModelElement)) { EcoreUtil.delete(oldModelElement, true); } } else if (value instanceof EObject) { if (value.equals(oldModelElement)) { EcoreUtil.delete(oldModelElement, true); } } } // TODO also delete modelelements of tokens? } /** * Deletes the given {@link TextBlock} <tt>block</tt> including any empty blocks that are adjecent to it. * * @param original */ public static Collection<EObject> deleteEmptyBlocksIncludingAdjecentBlocks(TextBlock original) { Collection<EObject> affectedModelElements = new ArrayList<EObject>(); affectedModelElements.addAll(deletePreviousEmptyBlocks(original)); affectedModelElements.addAll(deleteNextEmptyBlocks(original)); affectedModelElements.addAll(deleteEmptyBlocks(original)); affectedModelElements.addAll(deleteEmptyParentBlocks(original)); return affectedModelElements; } private static Collection<? extends EObject> deleteEmptyParentBlocks(TextBlock original) { Collection<EObject> affectedModelElements = new ArrayList<EObject>(); TextBlock tbDeletionCandidate = original; while (tbDeletionCandidate != null && EcoreHelper.isAlive(tbDeletionCandidate) && TbNavigationUtil.firstToken(tbDeletionCandidate) == null // this may be the case if there are any empty blocks before // remaining tokens // inside the block && tbDeletionCandidate.getTokens().size() == 0) { TextBlock deleteTB = tbDeletionCandidate; if (deleteTB.getCorrespondingModelElements().size() > 0) { affectedModelElements.addAll(deleteTB.getCorrespondingModelElements()); } tbDeletionCandidate = tbDeletionCandidate.getParent(); TbChangeUtil.delete(deleteTB); } return affectedModelElements; } public static void unsetPrimitiveFeature(TextBlock oldVersion, LexedToken lt, IModelInjector injector) { if (oldVersion.getCorrespondingModelElements().size() > 0 && lt.getSequenceElement() != null && lt.getSequenceElement() instanceof Property) { for (EObject ro : oldVersion.getCorrespondingModelElements()) { try { Collection<EObject> elements = new ArrayList<EObject>(lt.getReferencedElements()); injector.unset(ro, ((Property) lt.getSequenceElement()).getPropertyReference().getStrucfeature().getName(), elements); } catch (ModelCreationOntheFlyRuntimeException e) { // do nothing just try next element } } } } public static void unsetFeature(TextBlock oldVersion, TextBlock tb, IModelInjector injector) { if (oldVersion.getCorrespondingModelElements().size() > 0 && tb.getSequenceElement() != null && tb.getSequenceElement() instanceof Property) { for (EObject ro : oldVersion.getCorrespondingModelElements()) { Collection<EObject> elements = new ArrayList<EObject>(tb.getCorrespondingModelElements()); for (EObject refObject : elements) { try { injector.unset(ro, ((Property) tb.getSequenceElement()).getPropertyReference().getStrucfeature() .getName(), Collections.singleton(refObject)); } catch (ModelCreationOntheFlyRuntimeException e) { // do nothing just try next element } } } } } /** * Returns a {@link SetNewFeatureBean} for the corresponding AND referenced elements of the given <tt>newVersion</tt> * {@link TextBlock}. The element for which the value is to be set is derived from the corresponding model elements of the * oldVersion's parent {@link TextBlock}. * * @param newVersion * @param oldVersion * @param before * tells whether to add the value before or after the index of the oldVersion's index within its parent block. * @return */ public static SetNewFeatureBean insertFeatureValue(TextBlock newVersion, TextBlock oldVersion, boolean before) { // newVersion proxy has no parent so use the element from the CURRENT // version of the given parentTextBlock if (oldVersion != null && oldVersion.getSequenceElement() != null && oldVersion.getSequenceElement() instanceof Property) { int index = oldVersion.getParent().getSubBlocks().indexOf(oldVersion); if (!before) { index += 1; } if (oldVersion.getParent().getCorrespondingModelElements().size() > 1) { throw new RuntimeException("Tried to set a value for multiple parent elements:" + new ArrayList<EObject>(oldVersion.getParent().getCorrespondingModelElements()) + "\nDon't know how to handle this."); } EObject parentRefObject = oldVersion.getParent().getCorrespondingModelElements().iterator().next(); if (parentRefObject != null) { List<EObject> values = new ArrayList<EObject>(newVersion.getCorrespondingModelElements()); Object value = null; if (values.size() == 1) { value = values.get(0); } else { value = values; } return new SetNewFeatureBean(parentRefObject, ((Property) oldVersion.getSequenceElement()).getPropertyReference() .getStrucfeature().getName(), value, index); } } return null; } static void copyAttributes(TextBlock oldVersion, TextBlock result) { result.setLength(oldVersion.getLength()); result.setOffsetRelative(oldVersion.isOffsetRelative()); result.setOffset(oldVersion.getOffset()); List<DocumentNode> otherVersions = new ArrayList<DocumentNode>(oldVersion.getOtherVersions()); for (DocumentNode otherVersion : otherVersions) { oldVersion.getOtherVersions().remove(otherVersion); TbUtil.dereferenceVersions(oldVersion, otherVersion); } TbUtil.referenceVersions(result, otherVersions.get(0)); } /** * Finds the {@link EReference} that is the containment relation between <code>parent</code> and <code>child</code>; * * @param parent * @param child * @param connection * the {@link ResourceSet} to use for navigating the model. * @return */ public static IncrementalParsingUtil.CompositeRefAssociationBean findComposingFeature(EObject parent, EObject child, ResourceSet connection) { IncrementalParsingUtil.CompositeRefAssociationBean bean = new IncrementalParsingUtil.CompositeRefAssociationBean( child.eContainmentFeature(), true); // if (parent != null && child != null) { // Collection<EReference> compositeAssociations = EcoreHelper.getCompositeReferences( // parent.eClass(), // child.eClass()); // for (EReference reference : compositeAssociations) { // if (IncrementalParsingUtil.typesMatch(reference, parent.eClass(), child // .eClass(), connection) // && (parent.eGet(reference).equals(child))) { // bean = new IncrementalParsingUtil.CompositeRefAssociationBean(reference, true); // break; // the first valid reference is the correct one as // // there may only be // // one composite relationship between // // two elements // } else if (IncrementalParsingUtil.typesMatch(reference, child.eClass(), // parent.eClass(), connection) // && linkExists(parent, child, reference)) { // bean = new IncrementalParsingUtil.CompositeRefAssociationBean(reference, false); // break; // the first valid association is the correct one as // // there may only be // // one composite relationship between // // two elements // } // } // } return bean; } public static boolean linkExists(EObject parent, EObject child, EReference reference) { return ((parent.eGet(reference) instanceof Collection) && ((Collection<?>) parent.eGet(reference)).contains(child)); } /** * Checks whether the types of the given refObjects match the types of the association ends of the given association. * * @param reference * @param metaObject1 * @param metaObject2 * @param ResourceSet * the connection to get things like the {@link JmiHelper} from. * @return */ static boolean typesMatch(EReference reference, EClass metaObject1, EClass metaObject2, ResourceSet connection) { EClassifier firstEndType = reference.getEContainingClass(); EClassifier secondEndType = reference.getEType(); if (firstEndType.equals(metaObject1) || metaObject1.getEAllSuperTypes().contains(firstEndType)) { // first end matches, so check second end if (secondEndType.equals(metaObject2) || metaObject2.getEAllSuperTypes().contains(secondEndType)) { return true; } } return false; } static boolean isInTransientPartition(EObject correspondingNewElement) { // boolean isInTransientPartition = false; // ResourceSet connection = correspondingNewElement.get___Connection(); // for (Resource part : connection.getTransientPartitions()) { // if (correspondingNewElement.eResource().getURI().equals( // part.getURI())) { // isInTransientPartition = true; // } // } // if (correspondingNewElement.eResource().getURI().equals( // connection.getNullPartition().getPri())) { // isInTransientPartition = true; // } return correspondingNewElement.eResource() == null; } }