package com.sap.furcas.runtime.parser.textblocks.observer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.Token;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import com.sap.furcas.metamodel.FURCAS.TCS.ForeachPredicatePropertyInit;
import com.sap.furcas.metamodel.FURCAS.TCS.InjectorAction;
import com.sap.furcas.metamodel.FURCAS.TCS.InjectorActionsBlock;
import com.sap.furcas.metamodel.FURCAS.TCS.OperatorTemplate;
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.TextBlock;
import com.sap.furcas.runtime.common.implementation.ResolvedModelElementProxy;
import com.sap.furcas.runtime.common.interfaces.IModelElementProxy;
import com.sap.furcas.runtime.parser.ANTLR3LocationToken;
import com.sap.furcas.runtime.parser.IParsingObserver;
import com.sap.furcas.runtime.parser.PartitionAssignmentHandler;
import com.sap.furcas.runtime.parser.impl.DelayedReference;
import com.sap.furcas.runtime.parser.impl.ModelElementProxy;
import com.sap.furcas.runtime.parser.impl.ParserScope;
import com.sap.furcas.runtime.parser.textblocks.ITextBlocksTokenStream;
import com.sap.furcas.runtime.textblocks.TbUtil;
/**
* This class handles the connection between the parser and the textblocks
* the connection between the parser and the textblocks
* model.
*
* IMPORTANT NOTE: The current assumption is that the smallest unit of re-use is
* a whole textblock. This means that whenever {@link #notifyEnterRule(List)} is
* called and the contents of the block are not consistent to the parse rule
* anymore, the whole textblock is deleted and re-constructed according to the
* called rules.
*
* @author C5106462
*
*/
public class ParserTextBlocksHandler implements IParsingObserver {
/**
* used to track current context in virtual tree in parallel to existing
* tree
*/
private TextBlockTraverser traverser;
private final ITextBlocksTokenStream input;
/**
* Flag to indicate whether root rule has been entered yet or not.<p>
*
* ruleDepth == -1 means we are outside root context of traverser <p>
* ruleDepth == 0 means we are in root context of traverser
*/
private int ruleDepth = -1;
private static final int EOF = -1;
private final ParserScope parserScope;
private TextBlock existingRoot;
/**
* Constructs a new Handler which will read from the TextBlocksTokenStream
* on Parser events, and build up a textBlocks model
*
* @param partitionAssignmentHandler to retrieve information about the location of textblocks
* @param injector used for error reporting
*/
public ParserTextBlocksHandler(ITextBlocksTokenStream input, ParserScope parserScope,
PartitionAssignmentHandler partitionAssignmentHandler) {
this.parserScope = parserScope;
this.input = input;
this.traverser = new TextBlockTraverser();
}
/**
* returns the current TextBlockProxy of the TextBlockTraverser.
* @return
*/
public TextBlockProxy getCurrentTbProxy() {
return traverser.getCurrent();
}
/**
* This method is called at the start of a parse rule.
*
* NOTE: It is assumed that currently the relationship between a parse
* rule/a template to the structure of the textblocks is one-to-one.
* Furthermore re-use of textblocks is done only using their ordering. An
* incremental parsing technique will be employed instead of this once it
* becomes available.
*/
@Override
public void notifyEnterRule(String templateURI) {
if (ruleDepth > -1) {
traverser.enterNextChild();
}
Template template = (Template) parserScope.getResourceSet().getEObject(URI.createURI(templateURI), true);
if (template != null) {
traverser.getCurrent().setTemplate(template);
ruleDepth++;
} else {
failWithError("Could not resolve TCS template " + templateURI
+ ". This indicates that parser and mapping are not in sync.");
}
}
/**
* It navigates to the textblock that belongs to the given context within
* the {@link #currentTextBlock} which is then used during parsing to add
* all matched token elements and subblocks to it.
*/
@Override
public void notifyEnterSequenceAlternative(int choice) {
traverser.enterAlternative(choice);
}
@Override
public void notifyExitSequenceAlternative() {
traverser.exitAlternative();
}
/**
* Marks the current textblock as incomplete. This is to be set by the
* parser when it cannot match the a rule to the tokens in its token stream.
*/
@Override
public void notifyErrorInRule(RecognitionException re) {
TextBlockProxy currentBlock = traverser.getCurrent();
if (currentBlock != null) {
currentBlock.setComplete(false);
}
}
/**
* Exits the current textblock context by setting the parent textblock of
* the current textblock as new current textblock.
*/
@Override
public void notifyExitRule() {
if (ruleDepth < 0) { // cannot leave root context depth
throw new IllegalStateException("Attempt to leave root context more than once");
}
try {
/*
* complete filling and setting of textblock that was started on
* enterRule
*/
TextBlockProxy currentTextBlock = traverser.getCurrent();
// ruleDepth == -1 means we are outside root context of traverser
// ruleDepth == 0 means we are in root context of traverser
/*
* do not leave root even if new depth will be == 0, because
* some tokens may still be consumed, those will be added on EOF
* no need to leave child, as there is no parent in traverser,
* no need to save to parent
*/
if (ruleDepth > 0) {
/* Now leave the current context with all the required side effects */
traverser.leaveChild();
TextBlockProxy contextTextBlock = traverser.getCurrent();
if (currentTextBlock.getSubNodes().size() == 0) {
//textblock has no tokens and was only used to
//instanciate additional model elements so add them to the elements of this block
//and remove empty block;
contextTextBlock.addAdditionalTemplate(currentTextBlock.getTemplate());
contextTextBlock.addCorrespondingModelElementProxies(currentTextBlock.getCorrespondingModelElementProxies());
contextTextBlock.addAdditionalTemplates(currentTextBlock.getAdditionalTemplates());
contextTextBlock.removeSubNode(currentTextBlock);
} else {
// we have no textblock to add, so ignore
}
}
} catch (RuntimeException jmie) {
// should never happen, but happens. Exceptions get swallowed in the call trace else,
// due to consequent Exceptions
jmie.printStackTrace();
throw jmie;
} finally {
// (-1 is still allowed, we assign tokens to rootblock then)
ruleDepth--; // needs to be called even on exceptions to prevent trying to leave root.
}
}
@Override
public void notifyTokenConsume(Token token) {
consumeToken(token);
}
@Override
public void notifyTokenConsumeWithError(Token token) {
consumeToken(token);
}
/**
* add all omitted tokens that were skipped so far to the text block as well
* TODO check this behaviour, it might be necessary to make this
* configurable because there might be different expectations on where
* omitted tokens get stored (before, behind, in current textblock etc.)
*
* @param token
*/
private void consumeToken(Token token) {
if (token.getType() != EOF) {
AbstractToken tokenToRelocate = input.consumeTokenModelElementForParserToken(token);
Collection<? extends AbstractToken> offChannelTokens = input.consumeOffChannelTokensUpTo(tokenToRelocate);
// add the tokenToRelocate after adding all skipped tokens
for (AbstractToken offChannelToken : offChannelTokens) {
traverser.addSubNode(offChannelToken);
}
traverser.addSubNode(tokenToRelocate);
} else {
// consume all left tokens
List<? extends AbstractToken> offChannelTokens = input.consumeOffChannelTokensUpTo(null);
traverser.addSubNodes(offChannelTokens);
}
}
@Override
public void notifyEnterSequenceElement() {
}
@Override
public void notifyEnterSequenceElement(String sequenceElementURI) {
SequenceElement sequenceElement = (SequenceElement) parserScope.getResourceSet().getEObject(
URI.createURI(sequenceElementURI), true);
if (sequenceElement != null) {
traverser.setCurrentSequenceElement(sequenceElement);
} else {
failWithError("Could not resolve TCS sequence element " + sequenceElementURI
+ ". This indicates that parser and mapping are not in sync.");
}
}
@Override
public void notifyEnterInjectorAction() {
SequenceElement sequenceElement = traverser.getCurrentSequenceElement();
if (sequenceElement instanceof InjectorActionsBlock) {
InjectorAction oldAction = traverser.getCurrent().getInjectorAction();
InjectorAction currentAction = null;
if (oldAction == null) {
currentAction = ((InjectorActionsBlock) sequenceElement).getInjectorActions().iterator().next();
} else {
boolean next = false;
for (InjectorAction action : ((InjectorActionsBlock) sequenceElement).getInjectorActions()) {
if (action.equals(oldAction)) {
next = true;
continue;
}
if (next) {
currentAction = action;
break;
}
}
}
traverser.getCurrent().setInjectorAction(currentAction);
}
}
@Override
public void notifyExitInjectorAction() {
}
@Override
public void notifyExitSequenceElement() {
if (traverser.isOperatorToken()) {
//matching of operator tokenalways is surrounded with enterEqEl and exitSeqEl directly. So we need
//to switch operator token off here, otherwise elements from this point to to the
//exitOperatorSequence will also get operatorToken set to true.
traverser.setOperatorToken(false);
}
}
/**
* The given <code>newModelElement</code> is added to the
* {@link DocumentNode#getCorrespondingModelElements()} of the
* {@link #currentTextBlock}.
*/
@Override
public void notifyCommittedModelElementCreation(Object newModelElement) {
if (newModelElement == null) {
throw new IllegalArgumentException("Notified with null");
}
if (newModelElement instanceof IModelElementProxy) {
TextBlockProxy currentTextBlock = traverser.getCurrent();
currentTextBlock.getCorrespondingModelElementProxies().add(0, //always add at first position as it is the "leading" element
(IModelElementProxy) newModelElement);
((ModelElementProxy) newModelElement).setTextBlock(currentTextBlock);
if (currentTextBlock.getTemplate() instanceof OperatorTemplate
&& ((OperatorTemplate) currentTextBlock.getTemplate()).getStoreLeftSideTo() != null) {
//need to add left hand side textblock to current one as child to correctly represent
//composition
String storeLeftToProperty = ((OperatorTemplate) currentTextBlock.getTemplate()).getStoreLeftSideTo()
.getStrucfeature().getName();
ModelElementProxy leftHandSide = (ModelElementProxy) ((Collection<Object>) ((ModelElementProxy) newModelElement)
.getAttributeMap().get(storeLeftToProperty)).iterator().next();
TextBlockProxy leftHandSideTBProxy = (TextBlockProxy) leftHandSide.getTextBlock();
//add all consumed tokens and textblock starting from the lefthand side proxy to the current textblock
TextBlockProxy parent = leftHandSideTBProxy.getParent();
if (parent != null) {
int index = parent.getSubNodes().indexOf(leftHandSideTBProxy);
List<Object> elementsMoved = new ArrayList<Object>();
for (; index < parent.getSubNodes().size(); index++) {
Object element = parent.getSubNodes().get(index);
elementsMoved.add(element);
}
int position = 0;
for (Object object : elementsMoved) {
if (!currentTextBlock.equals(object)) {
// remove elements from their original parent
parent.getSubNodes().remove(object);
if (object instanceof TextBlockProxy) {
((TextBlockProxy) object).setParent(currentTextBlock);
}
// add elements to current proxy at start index
currentTextBlock.addSubNodeAt(object, position++);
}
}
}
}
} else {
throw new RuntimeException("Expected IModelElementProxy but got: " + newModelElement.getClass().getCanonicalName());
}
}
/**
* The given <code>modelElement</code> is added to the
* {@link DocumentNode#getReferencedElements()} of the
* corresponding {@link AbstractToken} of the <code>referenceLocation</code>
* {@link Token}. The <code>referenceType</code> indicates what the type of the
* {@link DelayedReference} was that was resolved. That can be e.g., {@link DelayedReference#SEMANTIC_PREDICATE}
* meaning that the delayed reference was created for a things like a {@link ForeachPredicatePropertyInit}.
*
*/
@Override
public void notifyModelElementResolvedOutOfContext(Object modelElement, Object contextModelElement, Token referenceLocation,
DelayedReference reference) {
if (contextModelElement instanceof ResolvedModelElementProxy) {
contextModelElement = ((ResolvedModelElementProxy) contextModelElement).getRealObject();
}
if (modelElement instanceof Collection && ((Collection<?>) modelElement).size() == 1) {
modelElement = ((Collection<?>) modelElement).iterator().next();
}
TextBlock contextBlock = getTextBlockForElementAt((EObject) contextModelElement, (ANTLR3LocationToken) referenceLocation);
if (contextBlock != null && modelElement instanceof EObject) {
if (reference.getType() == DelayedReference.ReferenceType.TYPE_FOREACH_PREDICATE) {
// this means we are in the resolving of a foreachproperty init
// thus we have to add the curently set template to the
// additionalTemplates of
// of the contextBlock. This will make all injectoractions
// associated with the template
// available for the GDR for the given model element
contextBlock.getAdditionalTemplates().add(getCurrentTbProxy().getTemplate());
contextBlock.getCorrespondingModelElements().add((EObject) modelElement);
} else {
AbstractToken referenceToken = navigateToToken(contextBlock, referenceLocation);
if (referenceToken != null && referenceToken instanceof LexedToken) {
((LexedToken) referenceToken).getReferencedElements().add((EObject) modelElement);
}
}
}
}
/**
* Attaches the current {@link SequenceElement} or {@link InjectorAction} to the given
* {@link DelayedReference}.
*/
@Override
public void notifyDelayedReferenceCreated(DelayedReference ref) {
if (traverser.getCurrentSequenceElement() instanceof InjectorActionsBlock) {
ref.setQueryElement(traverser.getCurrent().getInjectorAction());
} else {
ref.setQueryElement(traverser.getCurrent().getSequenceElement());
}
ref.setTextBlock(traverser.getCurrent());
}
@Override
public void notifyElementAddedToContext(Object element) {
traverser.addToContext(element);
}
/**
* Navigates to the corresponding AbstractToken of the given
* referenceLocation within the <code>contextBlock</code>.
*
* @param contextBlock
* Block from where to start navigation, has to be the direct
* owner of the token that is searched for.
* @param referenceLocation
* The ANLTR token that denotes the position of the corresponind
* modelelement token.
* @return the model element token that corresponds to the given
* referenceLocation
*/
private AbstractToken navigateToToken(TextBlock contextBlock, Token referenceLocation) {
if (referenceLocation != null) {
int absoluteLocation = ((ANTLR3LocationToken) referenceLocation).getStartIndex();
int relativeLocation = absoluteLocation - TbUtil.getAbsoluteOffset(contextBlock);
for (DocumentNode node : contextBlock.getSubNodes()) {
if (node.getOffset() == relativeLocation && node instanceof AbstractToken) {
return (AbstractToken) node;
}
}
}
return null;
}
/**
* Uses {@link DocumentNodeReferencesCorrespondingModelElement} to find a corresponding {@link TextBlock} for the
* given <code>element</code>.
*
* @param element the element to get the {@link TextBlock} for
* @return the corresponding {@link TextBlock} for the given element
*/
private TextBlock getTextBlockForElementAt(EObject element, ANTLR3LocationToken referenceToken) {
TextBlock rootBlock = TbUtil.getNewestVersion(existingRoot);
for (TextBlock textBlock : TbUtil.findTextBlockOf(rootBlock, element, parserScope.getResourceSet())) {
int candidateOffset = TbUtil.getAbsoluteOffset(textBlock);
if (referenceToken != null && ((candidateOffset > referenceToken.getStartIndex()) ||
(candidateOffset + textBlock.getLength() < referenceToken.getStopIndex()))) {
continue;
}
return textBlock;
}
return null;
}
/**
* This setter can be used to define the parent context on which the class
* is going to work
*
* @param root
*/
public void setRootBlock(TextBlock root) {
this.existingRoot = root;
this.traverser = new TextBlockTraverser();
}
@Override
public void notifyCommitModelElementFailed() {
}
@Override
public void notifyEnterOperatoredBrackettedSequence() {
}
@Override
public void notifyEnterSeparatorSequence() {
//store sequence before separator as it will be restored after separator consumption
SequenceElement currentSequenceElement = traverser.getCurrentSequenceElement();
traverser.setSequenceElementOfSeparator(currentSequenceElement);
}
@Override
public void notifyExitOperatoredBrackettedSequence() {
}
@Override
public void notifyExitSeparatorSequence() {
//restore sequence from before separator
SequenceElement currentSequenceElement = traverser.getSequenceElementOfSeparator();
traverser.setCurrentSequenceElement(currentSequenceElement);
}
@Override
public void notifyEnterOperatorSequence(String operator, int arity, boolean isUnaryPostfix) {
traverser.setOperatorToken(true);
}
@Override
public void notifyExitOperatorSequence() {
//if there was a right hand side assign the textblock created for it to the textblock responsible for
//the operator templated element.
TextBlockProxy currentTextBlockProxy = traverser.getCurrent();
//first get the second to the last TextBlockProxy within the parent
int index = currentTextBlockProxy.getSubNodes().size() - 1;
boolean countedFirstTbFromReverse = false;
TextBlockProxy last = null;
TextBlockProxy secondToLast = null;
while (index > 0) {
Object o = currentTextBlockProxy.getSubNodes().get(index--);
if (o instanceof TextBlockProxy) {
if (countedFirstTbFromReverse) {
secondToLast = (TextBlockProxy) o;
break;
} else {
last = (TextBlockProxy) o;
countedFirstTbFromReverse = true;
}
}
}
if (secondToLast != null && secondToLast.getTemplate() instanceof OperatorTemplate) {
OperatorTemplate optemplate = (OperatorTemplate) secondToLast.getTemplate();
if (optemplate.getStoreRightSideTo() != null) {
//this means that the right hand side proxy was added to the parent textblock instead of
//the one for the operatortemplate, so move it to there
last.setParent(secondToLast);
currentTextBlockProxy.removeSubNode(last);
secondToLast.addSubNodeAt(last, secondToLast.getSubNodes().size());
}
}
}
@Override
public void reset() {
this.traverser = new TextBlockTraverser();
//ruleDepth = 0;
}
/**
* This class should not fail silently, as this means that we might continue incremental parsing based
* on false information. This might lead to information loss when we try to "re-use" information.
*/
private void failWithError(Throwable cause, String message) {
cause.printStackTrace();
throw new RuntimeException(message, cause);
}
private void failWithError(String message) {
failWithError(new RuntimeException(message), message);
}
}