/**
* Copyright (C) 2006-2017 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.template;
import spoon.Launcher;
import spoon.SpoonException;
import spoon.reflect.code.CtBlock;
import spoon.reflect.code.CtFieldAccess;
import spoon.reflect.code.CtInvocation;
import spoon.reflect.code.CtStatement;
import spoon.reflect.code.CtStatementList;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtField;
import spoon.reflect.declaration.CtNamedElement;
import spoon.reflect.declaration.CtParameter;
import spoon.reflect.declaration.CtVariable;
import spoon.reflect.declaration.ParentNotInitializedException;
import spoon.reflect.reference.CtExecutableReference;
import spoon.reflect.reference.CtFieldReference;
import spoon.reflect.reference.CtPackageReference;
import spoon.reflect.reference.CtReference;
import spoon.reflect.reference.CtTypeParameterReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.CtScanner;
import spoon.reflect.visitor.Filter;
import spoon.reflect.visitor.Query;
import spoon.reflect.visitor.filter.InvocationFilter;
import spoon.support.template.DefaultParameterMatcher;
import spoon.support.template.ParameterMatcher;
import spoon.support.template.Parameters;
import spoon.support.util.RtHelper;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class defines an engine for matching a template to pieces of code.
*/
public class TemplateMatcher implements Filter<CtElement> {
private List<CtInvocation<?>> getMethods(CtClass<? extends Template<?>> root) {
CtExecutableReference<?> methodRef = root.getFactory().Executable()
.createReference(root.getFactory().Type().createReference(TemplateParameter.class), root.getFactory().Type().createTypeParameterReference("T"), "S");
List<CtInvocation<?>> meths = Query.getElements(root, new InvocationFilter(methodRef));
return meths;
}
private List<String> getTemplateNameParameters(CtClass<? extends Template<?>> templateType) {
return Parameters.getNames(templateType);
}
private List<CtTypeReference<?>> getTemplateTypeParameters(final CtClass<? extends Template<?>> templateType) {
final List<CtTypeReference<?>> ts = new ArrayList<>();
final Collection<String> c = Parameters.getNames(templateType);
new CtScanner() {
@Override
public void visitCtTypeParameterReference(CtTypeParameterReference reference) {
if (c.contains(reference.getSimpleName())) {
ts.add(reference);
}
}
@Override
public <T> void visitCtTypeReference(CtTypeReference<T> reference) {
if (c.contains(reference.getSimpleName())) {
ts.add(reference);
}
}
}.scan(templateType);
return ts;
}
private List<CtFieldReference<?>> getVarargs(CtClass<? extends Template<?>> root, List<CtInvocation<?>> variables) {
List<CtFieldReference<?>> fields = new ArrayList<>();
for (CtFieldReference<?> field : root.getReference().getAllFields()) {
if (field.getType().getActualClass() == CtStatementList.class) {
boolean alreadyAdded = false;
for (CtInvocation<?> invocation : variables) {
alreadyAdded |= ((CtFieldAccess<?>) invocation.getTarget()).getVariable().getDeclaration().equals(field);
}
if (!alreadyAdded) {
fields.add(field);
}
}
}
return fields;
}
/** the template itself */
private CtElement templateRoot;
private List<CtElement> finds = new ArrayList<>();
private Map<Object, Object> matches = new HashMap<>();
private List<String> names;
private CtClass<? extends Template<?>> templateType;
private List<CtTypeReference<?>> typeVariables;
private List<CtFieldReference<?>> varArgs;
private List<CtInvocation<?>> variables;
/**
* Constructs a matcher for a given template.
*
* @param templateRoot the template to match against
*
*/
@SuppressWarnings("unchecked")
public TemplateMatcher(CtElement templateRoot) {
this.templateType = templateRoot.getParent(CtClass.class);
this.templateRoot = templateRoot;
variables = getMethods(templateType);
typeVariables = getTemplateTypeParameters(templateType);
names = getTemplateNameParameters(templateType);
varArgs = getVarargs(templateType, variables);
//check that template matches itself
if (helperMatch(this.templateRoot, this.templateRoot) == false) {
throw new SpoonException("TemplateMatcher was unable to find itself, it certainly indicates a bug. Please revise your template or report an issue.");
}
}
private boolean addMatch(Object template, Object target) {
Object inv = matches.get(template);
Object o = matches.put(template, target);
return (null == inv) || inv.equals(o);
}
private CtElement checkListStatements(List<?> teList) {
for (Object tem : teList) {
if (variables.contains(tem) && (tem instanceof CtInvocation)) {
CtInvocation<?> listCand = (CtInvocation<?>) tem;
boolean ok = listCand.getFactory().Type().createReference(TemplateParameter.class).isSubtypeOf(listCand.getTarget().getType());
return ok ? listCand : null;
}
if (tem instanceof CtVariable) {
CtVariable<?> var = (CtVariable<?>) tem;
String name = var.getSimpleName();
for (CtFieldReference<?> f : varArgs) {
if (f.getSimpleName().equals(name)) {
return f.getDeclaration();
}
}
}
}
return null;
}
/**
* Finds all target program sub-trees that correspond to a template.
*
* @param targetRoot
* the target to be tested for match
* @return the matched elements
*/
public <T extends CtElement> List<T> find(final CtElement targetRoot) {
return targetRoot.filterChildren(this).list();
}
/**
*
* returns an appropriate ParameterMatcher defined in a template parameter, or else a default one
*
* if a template parameter (field annotated with @Parameter) whose name (field name) is a substring of the template name, it also works
*/
private ParameterMatcher findParameterMatcher(CtNamedElement templateDeclaration) throws InstantiationException, IllegalAccessException {
if (templateDeclaration == null) {
return new DefaultParameterMatcher();
}
String name = templateDeclaration.getSimpleName();
CtClass<?> clazz = null;
try {
clazz = templateDeclaration.getParent(CtClass.class);
} catch (ParentNotInitializedException e) {
Launcher.LOGGER.error(e.getMessage(), e);
}
if (clazz == null) {
return new DefaultParameterMatcher();
}
Collection<CtFieldReference<?>> fields = clazz.getReference().getAllFields();
CtFieldReference<?> param = null;
for (CtFieldReference<?> fieldRef : fields) {
Parameter p = fieldRef.getDeclaration().getAnnotation(Parameter.class);
if (p == null) {
continue; // not a parameter.
}
String proxy = p.value();
if (!"".equals(proxy)) {
if (name.contains(proxy)) {
param = fieldRef;
break;
}
}
if (name.contains(fieldRef.getSimpleName())) {
param = fieldRef;
break;
}
// todo: check for field hack.
}
return getParameterInstance(param);
}
@SuppressWarnings("unused")
private String getBindedParameter(String pname) {
final String[] x = new String[1]; // HACK! jeje
x[0] = pname;
new CtScanner() {
@Override
public <T> void visitCtField(CtField<T> f) {
Parameter p = f.getAnnotation(Parameter.class);
if ((p != null) && p.value().equals(x[0])) {
x[0] = f.getSimpleName();
return;
}
super.visitCtField(f);
}
}.scan(templateType);
return x[0];
}
/**
* Returns all the matches in a map where the keys are the corresponding
* template parameters. The {@link #match(CtElement, CtElement)} method must
* have been called before.
*/
private Map<Object, Object> getMatches() {
return matches;
}
/** returns a specific ParameterMatcher corresponding to the field acting as template parameter */
private ParameterMatcher getParameterInstance(CtFieldReference<?> param) throws InstantiationException, IllegalAccessException {
if (param == null) {
// return a default impl
return new DefaultParameterMatcher();
}
Parameter anParam = param.getDeclaration().getAnnotation(Parameter.class);
if (anParam == null) {
// Parameter not annotated. Probably is a TemplateParameter. Just
// return a default impl
return new DefaultParameterMatcher();
}
Class<? extends ParameterMatcher> pm = anParam.match();
ParameterMatcher instance = pm.newInstance();
return instance;
}
/*
* Made private to hide the Objects.
*/
private boolean helperMatch(Object target, Object template) {
if ((target == null) && (template == null)) {
return true;
}
if ((target == null) || (template == null)) {
return false;
}
if (variables.contains(template) || typeVariables.contains(template)) {
// TODO: upcall the parameter matcher if defined
boolean add = invokeCallBack(target, template);
if (add) {
return addMatch(template, target);
}
return false;
}
if (target.getClass() != template.getClass()) {
return false;
}
if ((template instanceof CtTypeReference) && template.equals(templateType.getReference())) {
return true;
}
if ((template instanceof CtPackageReference) && template.equals(templateType.getPackage())) {
return true;
}
if (template instanceof CtReference) {
CtReference tRef = (CtReference) template;
boolean ok = matchNames(tRef.getSimpleName(), ((CtReference) target).getSimpleName());
if (ok && !template.equals(target)) {
boolean remove = !invokeCallBack(target, template);
if (remove) {
matches.remove(tRef.getSimpleName());
return false;
}
return true;
}
}
if (template instanceof CtNamedElement) {
CtNamedElement named = (CtNamedElement) template;
boolean ok = matchNames(named.getSimpleName(), ((CtNamedElement) target).getSimpleName());
if (ok && !template.equals(target)) {
boolean remove = !invokeCallBack(target, template);
if (remove) {
matches.remove(named.getSimpleName());
return false;
}
}
}
if (template instanceof Collection) {
return matchCollections((Collection<?>) target, (Collection<?>) template);
}
if (template instanceof Map) {
if (template.equals(target)) {
return true;
}
Map<?, ?> temMap = (Map<?, ?>) template;
Map<?, ?> tarMap = (Map<?, ?>) target;
if (!temMap.keySet().equals(tarMap.keySet())) {
return false;
}
return matchCollections(tarMap.values(), temMap.values());
}
if (template instanceof CtBlock<?>) {
final List<CtStatement> statements = ((CtBlock) template).getStatements();
if (statements.size() == 1 && statements.get(0) instanceof CtInvocation) {
final CtInvocation ctStatement = (CtInvocation) statements.get(0);
if ("S".equals(ctStatement.getExecutable().getSimpleName()) && CtBlock.class.equals(ctStatement.getType().getActualClass())) {
return true;
}
}
}
if (target instanceof CtElement) {
for (Field f : RtHelper.getAllFields(target.getClass())) {
f.setAccessible(true);
if (Modifier.isStatic(f.getModifiers())) {
continue;
}
if (f.getName().equals("parent")) {
continue;
}
if (f.getName().equals("position")) {
continue;
}
if (f.getName().equals("docComment")) {
continue;
}
if (f.getName().equals("factory")) {
continue;
}
if (f.getName().equals("comments")) {
continue;
}
if (f.getName().equals("metadata")) {
continue;
}
try {
if (!helperMatch(f.get(target), f.get(template))) {
return false;
}
} catch (IllegalAccessException ignore) {
}
}
return true;
} else if (target instanceof String) {
return matchNames((String) template, (String) target);
} else {
return target.equals(template);
}
}
private boolean invokeCallBack(Object target, Object template) {
try {
if (template instanceof CtInvocation) {
CtFieldAccess<?> param = (CtFieldAccess<?>) ((CtInvocation<?>) template).getTarget();
ParameterMatcher instance = getParameterInstance(param.getVariable());
return instance.match(this, (CtInvocation<?>) template, (CtElement) target);
} else if (template instanceof CtReference) {
// Get parameter
CtReference ref = (CtReference) template;
ParameterMatcher instance;
if (ref.getDeclaration() == null || ref.getDeclaration().getAnnotation(Parameter.class) == null) {
instance = new DefaultParameterMatcher();
} else {
Parameter param = ref.getDeclaration().getAnnotation(Parameter.class);
instance = param.match().newInstance();
}
return instance.match(this, (CtReference) template, (CtReference) target);
} else if (template instanceof CtNamedElement) {
CtNamedElement named = (CtNamedElement) template;
ParameterMatcher instance = findParameterMatcher(named);
return instance.match(this, (CtElement) template, (CtElement) target);
} else {
// Should not happen
throw new RuntimeException();
}
} catch (InstantiationException e) {
Launcher.LOGGER.error(e.getMessage(), e);
return true;
} catch (IllegalAccessException e) {
Launcher.LOGGER.error(e.getMessage(), e);
return true;
}
}
private boolean isCurrentTemplate(Object object, CtElement inMulti) {
if (object instanceof CtInvocation<?>) {
return object.equals(inMulti);
}
if (object instanceof CtParameter) {
CtParameter<?> param = (CtParameter<?>) object;
for (CtFieldReference<?> varArg : varArgs) {
if (param.getSimpleName().equals(varArg.getSimpleName())) {
return varArg.equals(inMulti);
}
}
}
return false;
}
/**
* Matches a target program sub-tree against a template.
*
* @param targetRoot
* the target to be tested for match
* @return true if matches
*/
@Override
public boolean matches(CtElement targetRoot) {
if (targetRoot == templateRoot) {
// This case can occur when we are scanning the entire package for example see TemplateTest#testTemplateMatcherWithWholePackage
// Correct template matches itself of course, but client does not want that
return false;
}
return helperMatch(targetRoot, templateRoot);
}
@SuppressWarnings("unchecked")
private boolean matchCollections(Collection<?> target, Collection<?> template) {
List<Object> teList = new ArrayList<>(template);
List<Object> taList = new ArrayList<>(target);
// inMulti keeps the multiElement templateVariable we are at
CtElement inMulti = nextListStatement(teList, null);
// multi keeps the values to assign to inMulti
List<Object> multi = new ArrayList<>();
if (null == inMulti) {
// If we are not looking at template with multiElements
// the sizes should then be the same
if (teList.size() != taList.size()) {
return false;
}
for (int te = 0, ta = 0; (te < teList.size()) && (ta < taList.size()); te++, ta++) {
if (!helperMatch(taList.get(ta), teList.get(te))) {
return false;
}
}
return true;
}
for (int te = 0, ta = 0; (te < teList.size()) && (ta < taList.size()); te++, ta++) {
if (isCurrentTemplate(teList.get(te), inMulti)) {
if (te + 1 >= teList.size()) {
multi.addAll(taList.subList(te, taList.size()));
CtStatementList tpl = templateType.getFactory().Core().createStatementList();
tpl.setStatements((List<CtStatement>) (List<?>) multi);
if (!invokeCallBack(tpl, inMulti)) {
return false;
}
boolean ret = addMatch(inMulti, multi);
return ret;
}
te++;
while ((te < teList.size()) && (ta < taList.size()) && !helperMatch(taList.get(ta), teList.get(te))) {
multi.add(taList.get(ta));
ta++;
}
CtStatementList tpl = templateType.getFactory().Core().createStatementList();
tpl.setStatements((List<CtStatement>) (List<?>) multi);
if (!invokeCallBack(tpl, inMulti)) {
return false;
}
addMatch(inMulti, tpl);
// update inMulti
inMulti = nextListStatement(teList, inMulti);
multi = new ArrayList<>();
} else {
if (!helperMatch(taList.get(ta), teList.get(te))) {
return false;
}
if (!(ta + 1 < taList.size()) && (inMulti != null)) {
CtStatementList tpl = templateType.getFactory().Core().createStatementList();
for (Object o : multi) {
tpl.addStatement((CtStatement) o);
}
if (!invokeCallBack(tpl, inMulti)) {
return false;
}
addMatch(inMulti, tpl);
// update inMulti
inMulti = nextListStatement(teList, inMulti);
multi = new ArrayList<>();
}
}
}
return true;
}
private boolean matchNames(String templateName, String elementName) {
for (String templateParameterName : names) {
// pname = pname.replace("_FIELD_", "");
if (templateName.contains(templateParameterName)) {
String newName = templateName.replace(templateParameterName, "(.*)");
Pattern p = Pattern.compile(newName);
Matcher m = p.matcher(elementName);
if (!m.matches()) {
return false;
}
// TODO: fix with parameter from @Parameter
// boolean ok = addMatch(getBindedParameter(pname),
// m.group(1));
boolean ok = addMatch(templateParameterName, m.group(1));
if (!ok) {
Launcher.LOGGER.debug("incongruent match");
return false;
}
return true;
}
}
return templateName.equals(elementName);
}
private CtElement nextListStatement(List<?> teList, CtElement inMulti) {
if (inMulti == null) {
return checkListStatements(teList);
}
List<?> teList2 = new ArrayList<Object>(teList);
if (inMulti instanceof CtInvocation) {
teList2.remove(inMulti);
} else if (inMulti instanceof CtVariable) {
CtVariable<?> var = (CtVariable<?>) inMulti;
for (Iterator<?> iter = teList2.iterator(); iter.hasNext();) {
CtVariable<?> teVar = (CtVariable<?>) iter.next();
if (teVar.getSimpleName().equals(var.getSimpleName())) {
iter.remove();
}
}
}
return checkListStatements(teList2);
}
}