/*
* Copyright 2003-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.lang.pattern;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Match/extract values of a node against pattern/sample node.
* Not thread-safe.
* <p/>
* IMPLEMENTATION NODE: Pattern node passed to {@link #match(SNode, SNode)} doesn't need to have
* set properties/association/aggregation we are going to capture/iterate with distinct matcher.
* @author Artem Tikhomirov
* @since 3.4
*/
public final class NodeMatcher {
private final ChildMatcher myParent;
private final ValueContainer myValues;
private Map<SProperty, String> myPropertyToVariableName;
private Map<SReferenceLink, String> myReferenceToVariableName;
private Map<SContainmentLink, ChildMatcher> myChildExtractors;
private List<Pair<SNode, NodeMatcher>> myDisjunction;
private String myPatternVarName;
private boolean myMatchAny = false;
public NodeMatcher(@NotNull ValueContainer vc) {
myValues = vc;
myParent = null;
}
/*package*/ NodeMatcher(@NotNull ChildMatcher parent) {
myValues = parent.getValues();
myParent = parent;
}
// XXX I could have introduced PropertyExtractor with single useful #capture(String) method (plus #done()), but it's just too much
public NodeMatcher property(@NotNull SProperty p, @NotNull String patternVarName) {
if (myPropertyToVariableName == null) {
myPropertyToVariableName = new HashMap<SProperty, String>(8);
}
myPropertyToVariableName.put(p, patternVarName);
return this;
}
public NodeMatcher capture(String nodeVarName) {
myPatternVarName = nodeVarName;
return this;
}
/**
* 'wildcard' node match (aka '_')
* @return <code>this</code>
*/
public NodeMatcher any() {
myMatchAny = true;
return this;
}
// see #property(), could have added RefExtractor, if needed
public NodeMatcher association(@NotNull SReferenceLink l, @NotNull String linkVarName) {
if (myReferenceToVariableName == null) {
myReferenceToVariableName = new HashMap<SReferenceLink, String>(8);
}
myReferenceToVariableName.put(l, linkVarName);
return this;
}
public ChildMatcher child(@NotNull SContainmentLink role) {
if (myChildExtractors == null) {
myChildExtractors = new HashMap<SContainmentLink, ChildMatcher>(8);
}
ChildMatcher childExtractor = myChildExtractors.get(role);
if (childExtractor == null) {
myChildExtractors.put(role, childExtractor = new ChildMatcher(this));
}
return childExtractor;
}
/**
* Tells to match disjunction of nodes using supplied alternatives.
* <code>NodeMatcher</code> with alternatives doesn't check children, associations or properties. It's still possible, though, to
* capture value of the node with {@link #capture(String)}.
*
* <p/>When matching, each disjunct is processed in order they were added to the matcher, the first one to match cancels processing of other
* (i.e. similar to Java's || operator).
*
* @param patternNode pattern to match disjunct against.
* @param disjunct ordered matcher sequence, consulted one my one until match is found.
* @return <code>this</code>
*/
public NodeMatcher disjunct(@NotNull SNode patternNode, @NotNull NodeMatcher disjunct) {
assert patternNode.getModel() == null : "expect pattern nodes to hand in the air not to address model access";
if (myDisjunction == null) {
myDisjunction = new ArrayList<Pair<SNode, NodeMatcher>>(4);
}
myDisjunction.add(new Pair<SNode, NodeMatcher>(patternNode, disjunct));
return this;
}
public ChildMatcher done() {
return myParent;
}
/*package*/ ValueContainer getValues() {
return myValues;
}
public boolean match(@NotNull SNode pattern, @NotNull SNode against) {
return internalMatch(pattern, against);
}
/**
* relaxed version of {@link #match(SNode, SNode)} that accounts for missing child in pattern node,
* when e.g. a disjunction or wildcard would match that of actual.
* Shall be used from within {@link ChildMatcher} only!
*/
/*package*/ boolean internalMatch(@Nullable SNode pattern, SNode against) {
if (myMatchAny) {
return true;
}
final boolean rv;
if (myDisjunction != null) {
rv = matchDisjunction(against);
// technically, we can always try matchDisjunction before matchStructure, just don't see
// any reason to - generated code didn't go deeper than OrPattern.
} else {
rv = matchStructure(pattern, against);
}
if (rv && myPatternVarName != null) {
getValues().put(myPatternVarName, against);
}
return rv;
}
private boolean matchStructure(SNode pattern, SNode against) {
if (myPatternVarName != null && myChildExtractors == null && myPropertyToVariableName == null && myReferenceToVariableName == null) {
// FIXME if it's a mere accessor to a node to capture it, do not check concept match. Though IMO it's a defect in original implementation,
// it's left here for the time being to get existing tests pass before I fix them with a distinct change
// The case is "if (#v1) { #v2 }", where v2 attributes ExpressionStatement, and input comes as an empty Statement.
// After some consideration and discussion, it seems we need to cover both scenarios (explicitly picked by user when
// pattern variable is assigned), and this alternative shall remain default for compatibility with existing code.
return true;
}
if (pattern == null) {
return false;
}
if (!against.getConcept().isSubConceptOf(pattern.getConcept())) {
return false;
}
// properties
Map<SProperty, String> prop2var = myPropertyToVariableName == null ? Collections.<SProperty,String>emptyMap() : myPropertyToVariableName;
ArrayList<SProperty> propsToCheck = new ArrayList<SProperty>(prop2var.keySet());
propsToCheck.addAll(IterableUtil.asCollection(pattern.getProperties()));
for (SProperty p : propsToCheck) {
if (prop2var.containsKey(p)) {
getValues().put(prop2var.get(p), against.getProperty(p));
} else {
if (!pattern.getProperty(p).equals(against.getProperty(p))) {
return false;
}
}
}
//
// references
final Map<SReferenceLink, String> ref2var = myReferenceToVariableName == null ? Collections.<SReferenceLink, String>emptyMap() : myReferenceToVariableName;
ArrayList<SReferenceLink> refsToCheck = new ArrayList<SReferenceLink>(ref2var.keySet());
for (SReference r : pattern.getReferences()) {
refsToCheck.add(r.getLink());
}
for (SReferenceLink r : refsToCheck) {
SReference r2 = against.getReference(r);
SNodeReference actualTarget = r2 == null ? null : r2.getTargetNodeReference();
if (ref2var.containsKey(r)) {
getValues().put(ref2var.get(r), actualTarget, r2 == null ? null : r2.getTargetNode());
} else {
final SReference expectedReference = pattern.getReference(r);
assert expectedReference != null : "otherwise how did it get into refsToCheck";
final SNodeReference expectedTarget = expectedReference.getTargetNodeReference();
if (!expectedTarget.equals(actualTarget)) {
return false;
}
}
}
//
// children
ArrayList<SContainmentLink> knownChildRoles = new ArrayList<SContainmentLink>();
// patterns are generally small and don't specify vast child hierarchies in different roles, list is sufficient to hold 1-2 roles
for (SNode child = pattern.getFirstChild(); child != null; child = child.getNextSibling()) {
final SContainmentLink cl = child.getContainmentLink();
if (!knownChildRoles.contains(cl)) {
knownChildRoles.add(cl);
}
}
final Map<SContainmentLink, ChildMatcher> ce = myChildExtractors == null ? Collections.<SContainmentLink,ChildMatcher>emptyMap() : myChildExtractors;
knownChildRoles.addAll(ce.keySet());
final ChildMatcher defaultChildExtractor = new ChildMatcher(this);
for (SContainmentLink l : knownChildRoles) {
ChildMatcher childExtractor = ce.get(l);
if (childExtractor == null) {
childExtractor = defaultChildExtractor;
}
if (!childExtractor.match(IterableUtil.asList(pattern.getChildren(l)), IterableUtil.asList(against.getChildren(l)))) {
return false;
};
}
return true;
}
private boolean matchDisjunction(SNode against) {
for (Pair<SNode, NodeMatcher> pair : myDisjunction) {
if (pair.o2.match(pair.o1, against)) {
return true;
}
}
return false;
}
}