Reputation: 471
I am trying to refactor old SimpleFormController. I would like to replace getSuccessView() and gerFormView() calls with actual success view and form view Strings.
I went through https://spoon.gforge.inria.fr/first_transformation.html, it shows how to generate and add statements however I could not understand how to modify.
I have tried couple of things.
Replace statements with the getSuccessView() and getFormView() calls
public class SimpleFormControllerReplaceViewCall extends AbstractProcessor<CtMethod> {
MetaData meta;
String successView= "successView";
String formView = "formView";
public SimpleFormControllerReplaceViewCall(MetaData meta) {
this.meta = meta;
}
@Override
public boolean isToBeProcessed(CtMethod candidate) {
if(candidate.getBody() == null) { //Ignore abstract methods
return false;
}
String sourceCode;
try {
sourceCode = candidate.getBody()
.getOriginalSourceFragment()
.getSourceCode();
} catch (Exception e) {
return false;
}
return sourceCode.contains(getViewFunctionName(successView))
|| sourceCode.contains(getViewFunctionName(formView));
}
@Override
public void process(CtMethod method) {
Node beanNode = getBeanNode(method);
CtBlock<Object> body = getFactory().createBlock();
method.getBody().getStatements()
.stream()
.map(s -> {
Optional<String> sourceCode = getStatementSourceCode(s);
if(!sourceCode.isPresent()) {
return s.clone(); // Clone required to handle runtime error for trying attach a node to two parents
} else {
System.out.println("Modifying: " + method.getSignature());
String code = sourceCode.get();
code = replaceViewCalls(beanNode, code, successView);
code = replaceViewCalls(beanNode, code, formView);
return getFactory().createCodeSnippetStatement(code);
}
}).forEach(body::addStatement);
method.setBody(body);
}
private Optional<String> getStatementSourceCode(CtStatement s) {
String sourceCode = null;
try {
sourceCode = s.getOriginalSourceFragment()
.getSourceCode();
} catch (Exception e) {}
System.out.println(sourceCode);
if (sourceCode != null &&
(sourceCode.contains(getViewFunctionName(successView))
|| sourceCode.contains(getViewFunctionName(formView)))) {
sourceCode = sourceCode.trim();
if(sourceCode.endsWith(";"))
sourceCode = sourceCode.substring(0, sourceCode.length()-1);
return Optional.of(sourceCode);
} else {
return Optional.empty();
}
}
public String replaceViewCalls(Node beanNode, String code, String viewType) {
String getViewFunctionName = getViewFunctionName(viewType);
if (!code.contains(getViewFunctionName)) {
return code;
}
String view = AppUtil.getSpringBeanPropertyValue(beanNode, viewType);
return code.replaceAll(getViewFunctionName + "\\(\\)", String.format("\"%s\"", view));
}
public Node getBeanNode(CtMethod method) {
String qualifiedName = method.getParent(CtClass.class).getQualifiedName();
return meta.getFullyQualifiedNameToNodeMap().get(qualifiedName);
}
private String getViewFunctionName(String viewType) {
return "get" + viewType.substring(0, 1).toUpperCase() + viewType.substring(1);
}
}
This however adds unwanted at end of blocks if() {... }; This creates syntax errors when if {} else {} blocks contain return statement(s). Auto import is turned on and imports are not added when there is more one class with same name (e.g., Map is present in classpath from few libraries) - this is consistent with the document. Can this be avoided when refactoring code? Original java file has correct imports.
Another approach I tried is to directly manipulate the body as a whole.
@Override
public void process(CtMethod method) {
String code = method.getBody()
.getOriginalSourceFragment()
.getSourceCode();
Node beanNode = getBeanNode(method);
code = replaceViewCalls(beanNode, code, successView);
code = replaceViewCalls(beanNode, code, formView);
CtCodeSnippetStatement codeStatement = getFactory().createCodeSnippetStatement(code);
method.setBody(codeStatement);
}
this still has same auto import issue as first one. Apart from that it adds redundant curly braces, for examples
void method() { x=y;}
will become
void method() { {x=y;} }
That that will be pretty printed ofcourse.
Also javadocs for getOriginalSourceFragment() also has below warning
Warning: this is a advanced method which cannot be considered as part of the stable API
One more thing I thought of doing is creating pattern for each type of usage of getSuccessView() like viewName = getSuccessView(); return getSuccessView(); return ModelAndView(getSuccessView(), map); etc, however for that I will have to write a whole bunch of processors / templates.
Since it is simple replacement, easiest is do something like below
//Walk over all files and execute
Files.lines(Paths.get("/path/to/java/file"))
.map(l -> l.replaceAll("getSuccessView\\(\\)", "actualViewNameWithEscapedQuotes"))
.map(l -> l.replaceAll("getFormView\\(\\)", "actualViewNameWithEscapedQuotes"))
.forEach(l -> {
//write to file
});
Since I can avoid text manipulation with the help of spoon for things like changing modifiers, annotations, method name, annotations etc, I am hoping there should be a better way to modify the method body.
Upvotes: 1
Views: 983
Reputation: 96
You should treat the processor input as an abstract syntax tree instead of a string:
public class SimpleFormControllerReplaceViewCall extends AbstractProcessor<CtMethod<?>> {
@Override
public boolean isToBeProcessed(CtMethod candidate) {
if(candidate.isAbstract()) { //Ignore abstract methods
return false;
}
return !candidate.filterChildren((CtInvocation i)->
i.getExecutable().getSimpleName().equals("getSuccessView")
|| i.getExecutable().getSimpleName().equals("getFormView")).list().isEmpty();
}
@Override
public void process(CtMethod<?> ctMethod) {
Launcher launcher = new Launcher();
CodeFactory factory = launcher.createFactory().Code();
List<CtInvocation> invocations = ctMethod.filterChildren((CtInvocation i)->
i.getExecutable().getSimpleName().equals("getSuccessView")
|| i.getExecutable().getSimpleName().equals("getFormView")).list();
for(CtInvocation i : invocations) {
if(i.getExecutable().getSimpleName().equals("getSuccessView")) {
i.replace(factory.createLiteral("successView"));
} else {
i.replace(factory.createLiteral("formView"));
}
}
}
}
Here the CtMethod AST is traversed in search for CtInvocation elements with the specified properties. The found elements are then replaced with new string literal elements.
Upvotes: 2