/**
* 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.generating.replace;
import spoon.SpoonException;
import spoon.generating.ReplacementVisitorGenerator;
import spoon.reflect.code.CtAssignment;
import spoon.reflect.code.CtBlock;
import spoon.reflect.code.CtComment;
import spoon.reflect.code.CtConstructorCall;
import spoon.reflect.code.CtExpression;
import spoon.reflect.code.CtFieldAccess;
import spoon.reflect.code.CtInvocation;
import spoon.reflect.code.CtThisAccess;
import spoon.reflect.code.CtTypeAccess;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtConstructor;
import spoon.reflect.declaration.CtField;
import spoon.reflect.declaration.CtInterface;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtParameter;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtTypeParameter;
import spoon.reflect.factory.Factory;
import spoon.reflect.reference.CtExecutableReference;
import spoon.reflect.reference.CtTypeParameterReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.CtScanner;
import spoon.reflect.visitor.filter.TypeFilter;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ReplaceScanner extends CtScanner {
public static final String TARGET_REPLACE_PACKAGE = "spoon.support.visitor.replace";
public static final String GENERATING_REPLACE_PACKAGE = "spoon.generating.replace";
public static final String GENERATING_REPLACE_VISITOR = GENERATING_REPLACE_PACKAGE + ".ReplacementVisitor";
private final Map<String, CtClass> listeners = new HashMap<>();
private final CtClass<Object> target;
private final CtExecutableReference<?> element;
private final CtExecutableReference<?> list;
private final CtExecutableReference<?> map;
private final CtExecutableReference<?> set;
public ReplaceScanner(CtClass<Object> target) {
this.target = target;
this.element = target.getMethodsByName("replaceElementIfExist").get(0).getReference();
this.list = target.getMethodsByName("replaceInListIfExist").get(0).getReference();
this.map = target.getMethodsByName("replaceInMapIfExist").get(0).getReference();
this.set = target.getMethodsByName("replaceInSetIfExist").get(0).getReference();
}
@Override
public <T> void visitCtMethod(CtMethod<T> element) {
if (!element.getSimpleName().startsWith("visitCt")) {
return;
}
Factory factory = element.getFactory();
CtMethod<T> clone = element.clone();
factory.Annotation().annotate(clone, Override.class);
clone.getBody().getStatements().clear();
for (int i = 1; i < element.getBody().getStatements().size() - 1; i++) {
CtInvocation inv = element.getBody().getStatement(i);
CtInvocation getter = (CtInvocation) inv.getArguments().get(0);
if (clone.getComments().size() == 0) {
// Add auto-generated comment.
final CtComment comment = factory.Core().createComment();
comment.setCommentType(CtComment.CommentType.INLINE);
comment.setContent("auto-generated, see " + ReplacementVisitorGenerator.class.getName());
clone.addComment(comment);
}
Class actualClass = getter.getType().getActualClass();
CtInvocation<?> invocation = createInvocation(factory, element, inv, getter, actualClass);
clone.getBody().addStatement(invocation);
}
target.addMethod(clone);
}
private <T> CtInvocation<?> createInvocation(Factory factory, CtMethod<T> candidate, CtInvocation current, CtInvocation getter, Class getterTypeClass) {
CtInvocation<?> invocation;
Type type;
if (getterTypeClass.equals(Collection.class) || getterTypeClass.equals(List.class)) {
invocation = factory.Code().createInvocation(null, this.list, current.getArguments());
type = Type.LIST;
} else if (getterTypeClass.equals(Map.class)) {
invocation = factory.Code().createInvocation(null, this.map, current.getArguments());
type = Type.MAP;
} else if (getterTypeClass.equals(Set.class)) {
invocation = factory.Code().createInvocation(null, this.set, current.getArguments());
type = Type.SET;
} else {
invocation = factory.Code().createInvocation(null, this.element, current.getArguments());
type = Type.ELEMENT;
}
// Listener
final String name = getter.getExecutable().getSimpleName().substring(3);
final String listenerName = getter.getExecutable().getDeclaringType().getSimpleName() + name + "ReplaceListener";
CtClass listener;
if (listeners.containsKey(listenerName)) {
listener = listeners.get(listenerName);
} else {
final CtTypeReference getterType = getGetterType(factory, getter);
listener = createListenerClass(factory, listenerName, getterType, type);
final CtMethod setter = getSetter(name, getter.getTarget().getType().getDeclaration());
final CtField field = updateField(listener, setter.getDeclaringType().getReference());
updateConstructor(listener, setter.getDeclaringType().getReference());
updateSetter(factory, (CtMethod<?>) listener.getMethodsByName("set").get(0), getterType, field, setter);
// Add auto-generated comment.
final CtComment comment = factory.Core().createComment();
comment.setCommentType(CtComment.CommentType.INLINE);
comment.setContent("auto-generated, see " + ReplacementVisitorGenerator.class.getName());
listener.addComment(comment);
listeners.put(listenerName, listener);
}
invocation.addArgument(getConstructorCall(listener, factory.Code().createVariableRead(candidate.getParameters().get(0).getReference(), false)));
return invocation;
}
private CtTypeReference getGetterType(Factory factory, CtInvocation getter) {
CtTypeReference getterType;
final CtTypeReference type = getter.getType();
if (type instanceof CtTypeParameterReference) {
getterType = getTypeFromTypeParameterReference((CtTypeParameterReference) getter.getExecutable().getDeclaration().getType());
} else {
getterType = type.clone();
}
getterType.getActualTypeArguments().clear();
return getterType;
}
private CtTypeReference getTypeFromTypeParameterReference(CtTypeParameterReference ctTypeParameterRef) {
final CtMethod parentMethod = ctTypeParameterRef.getParent(CtMethod.class);
for (CtTypeParameter formal : parentMethod.getFormalCtTypeParameters()) {
if (formal.getSimpleName().equals(ctTypeParameterRef.getSimpleName())) {
return ((CtTypeParameterReference) formal).getBoundingType();
}
}
final CtInterface parentInterface = ctTypeParameterRef.getParent(CtInterface.class);
for (CtTypeParameter formal : parentInterface.getFormalCtTypeParameters()) {
if (formal.getSimpleName().equals(ctTypeParameterRef.getSimpleName())) {
return formal.getReference().getBoundingType();
}
}
throw new SpoonException("Can't get the type of the CtTypeParameterReference " + ctTypeParameterRef);
}
private CtClass createListenerClass(Factory factory, String listenerName, CtTypeReference getterType, Type type) {
CtClass listener;
listener = factory.Class().get(GENERATING_REPLACE_PACKAGE + ".CtListener").clone();
listener.setSimpleName(listenerName);
target.addNestedType(listener);
final List<CtTypeReference> references = listener.getElements(new TypeFilter<CtTypeReference>(CtTypeReference.class) {
@Override
public boolean matches(CtTypeReference reference) {
return (GENERATING_REPLACE_PACKAGE + ".CtListener").equals(reference.getQualifiedName());
}
});
for (CtTypeReference reference : references) {
reference.setPackage(listener.getPackage().getReference());
}
final CtTypeReference<Object> theInterface = factory.Class().createReference(GENERATING_REPLACE_PACKAGE + "." + type.name);
theInterface.addActualTypeArgument(getterType);
final Set<CtTypeReference<?>> interfaces = new HashSet<>();
interfaces.add(theInterface);
listener.setSuperInterfaces(interfaces);
return listener;
}
private CtParameter<?> updateConstructor(CtClass listener, CtTypeReference type) {
final CtConstructor ctConstructor = (CtConstructor) listener.getConstructors().toArray(new CtConstructor[listener.getConstructors().size()])[0];
CtAssignment assign = (CtAssignment) ctConstructor.getBody().getStatement(1);
CtThisAccess fieldAccess = (CtThisAccess) ((CtFieldAccess) assign.getAssigned()).getTarget();
((CtTypeAccess) fieldAccess.getTarget()).getAccessedType().setImplicit(true);
final CtParameter<?> aParameter = (CtParameter<?>) ctConstructor.getParameters().get(0);
aParameter.setType(type);
return aParameter;
}
private CtField updateField(CtClass listener, CtTypeReference<?> type) {
final CtField field = listener.getField("element");
field.setType(type);
return field;
}
private void updateSetter(Factory factory, CtMethod<?> setListener, CtTypeReference getterType, CtField<?> field, CtMethod setter) {
setListener.getParameters().get(0).setType(getterType);
CtInvocation ctInvocation = factory.Code().createInvocation(//
factory.Code().createVariableRead(field.getReference(), false), //
setter.getReference(), //
factory.Code().createVariableRead(setListener.getParameters().get(0).getReference(), false) //
);
CtBlock ctBlock = factory.Code().createCtBlock(ctInvocation);
setListener.setBody(ctBlock);
}
private CtMethod getSetter(String name, CtType declaration) {
Set<CtMethod> allMethods = declaration.getAllMethods();
CtMethod setter = null;
for (CtMethod aMethod : allMethods) {
if (("set" + name).equals(aMethod.getSimpleName())) {
setter = aMethod;
break;
}
}
return setter;
}
private CtConstructorCall<?> getConstructorCall(CtClass listener, CtExpression argument) {
return listener.getFactory().Code().createConstructorCall(listener.getReference(), argument);
}
enum Type {
ELEMENT("ReplaceListener"), LIST("ReplaceListListener"), SET("ReplaceSetListener"), MAP("ReplaceMapListener");
String name;
Type(String name) {
this.name = name;
}
}
}