2010년 3월 6일 토요일

JDT AST (Abstract Syntax Tree)

AST는 JDT에서만 사용하는 용어가 아니라 일반적인 용어다.
일반적인 컴파일러나 자바 바이트 코드 관련 도구들을 찾아 보더라도 AST라는 용어를 자주 접하게 된다.
프로그램 언어가 트리구조의 문법을 가지고 있기 때문에 소스 코드를 변경해야 하거나 클래스를 동적으로 변경하고자 한다면 AST에 대한 이해를 하고 있어야 한다.
자바 바이트 코드를 동적으로 생성하는 기능을 제공하는 라이브러리로는 asm, cglib, javassist 등이 있다.
각각의 장단점은 있겠지만, javassist가 개발하는데는 쉽고 편하다.
JDT는 Runtime에 클래스를 변경하는 이것들과는 좀 다르다. JDT는 우리가 이해하는 것처럼 자바 개발 환경인 Eclipse의 자바 개발 툴이다. 물론 컴파일러로 클래스도 생성은 가능 하지만 어디까지나 개발 도중에 이용할 목적이지 Runtime에 이용할 목적은 아니다.

JDT의 AST로는 자바소스코드 편집에 관한 모든 일을 할 수 있다고 생각하면 된다.
자바에 익숙한 개발자라면 Velocity 같은 템플릿 엔진이나 심지어는 StringBuffer로도 자바소스를 만들어 낼 것이다.

하지만 이미 만들어진 자바 소스 내에 javaElement를 추가, 삭제하거나 변환 할 때, Javadoc을 추가할 때 자바 소스코드를 가지고는 처리할 수가 없다.

이클립스 플러그인으로 자바 소스코드를 처리해야하는 경우가 있다면 반드시 AST도 함께 보면 좋을 것이다.


보통은 참조 리소스를 가장 마지막에 소개하는데, AST관련하여 비교적 잘 정리가 되어 있어 http://www.ibm.com/developerworks/opensource/library/os-ast/?ca=dgr-lnxw961ASTParser 를 방문하기를 권고한다. 좀 유통기한이 지난 감이 없지 않지만, 나에겐 많은 도움이 된 문서다.

요구사항은 다음과 같다.
다이얼로그에서 스프링 빈으로 등록할 빈 클래스와 그 빈클래스에서 사용하는 프러퍼티를 입력받아 빈 클래스를 생성한다. 스프링에서의 빈은 의존성이 낮은 방식으로 주입되어야 하기 때문에 빈 클래스에 대한 인터페이스도 생성한다. 빈과 프로퍼티에 관련된 커멘트도 함께 생성한다.

1. 자바 엘리먼트 저장 매카니즘


자바 프로젝트의 src 폴더에 패키지명에 맞는 (IFolder)폴더를 생성한 후 자바(인터페이스, 클래스)파일을 저장한다.
패키지명에 따라 계층적으로 폴더를 계속 생성해야 하므로 ProjectHelper.createFolder() 유틸리티 클래스를 만들었다. 다른 항목은 별도로 언급할 만큼 어려운 코드가 아니다. 파일을 생성할 때 InputStream 으로 전달해주면 되기 때문에 굳이 AST를 사용하지 않고 문자열처리로도 가능하다. 하지만 AST 관련 글이라는 점을 계속 기억하고 있길 바란다.
아래코드는 인터페이스 파일을 생성하는 소스 코드지만 buildInterface를 제외하면 클래스를 만드는 소스코드에서도 동일한다.


private void createBeanInterface() throws CoreException{
try {

String interfaceName = beanInterfaceName;
int index = interfaceName.lastIndexOf('.');
String packageName = (index < 0) ? null : interfaceName.substring(0, index);
String simpleName = interfaceName.substring(index + 1);

IFolder packageFolder = null;
if(packageName != null) {
Path packagePath = new Path(packageName.replace('.', '/'));

IFolder srcFolder = ProjectHelper.getSourceFolder(javaProject.getProject());
packageFolder = ProjectHelper.createFolder(srcFolder, packagePath);

} else {
packageFolder = ProjectHelper.getSourceFolder(javaProject.getProject());
}
IFile javaFile = packageFolder.getFile(simpleName + ".java");
if(javaFile.exists() == false) {
javaFile.create(new ByteArrayInputStream(buildInterface(interfaceName).getBytes()), true, null);
}

} catch(Exception ex) {
ex.printStackTrace();
throw new CoreException(DialogHelper.createErrorStatus("Interface Creation ERROR", ex));
}
}




2. 인터페이스 생성


인터페이스 생성은 아주 간단한다. 패키지명과 인터페이스 명만 있으면 생성이 가능해서 그냥 문자열로 처리해도 무리가 없을 정도이다.

인터페이스명으로 aa.bb.cc.dd.DDD 라고 입력 받았다면 아래와 같이 생성하면 된다.


package aa.bb.cc.dd;

public interface DDD {
}


실제 위와 같은 문자열을 만들어 내는 소스 코드는 다음과 같다.


public String buildInterface(String interfaceName) throws CoreException {
try {
ASTParser parser = ASTParser.newParser(AST.JLS3);
parser.setSource("".toCharArray());
CompilationUnit unit = (CompilationUnit) parser.createAST(null);
unit.recordModifications();
AST ast = unit.getAST();

PackageDeclaration packageDeclaration = ast.newPackageDeclaration();
packageDeclaration.setName(JdtHelper.createPackageName(ast, interfaceName));
unit.setPackage(packageDeclaration);

TypeDeclaration type = ast.newTypeDeclaration();
type.setInterface(true);
type.setName(ast.newSimpleName(interfaceName.substring(interfaceName.lastIndexOf('.') + 1)));
type.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD));

unit.types().add(type);

Document document = new Document();
TextEdit edits = unit.rewrite(document, javaProject.getOptions(true));
edits.apply(document);
return document.get();

} catch(Exception ex) {
ex.printStackTrace();
throw new CoreException(DialogHelper.createErrorStatus("Interface Build ERROR", ex));
}
}



3. 클래스 생성


클래스 생성은 조금 복잡하다. 인터페이스를 implements도 해주어야 하고, 필드도 추가해야하고, getter/setter 메서드도 만들어야 한다.
시작부분은 인터페이스와 비슷하다.
코드 중간에 있는 BeanPropertyModel 는 클래스는 필드 설정에 관련된 도메인 클래스로 필드명과 fullyQualifiedName으로 저장되어 있는 필드 타입, 그리고 필드 커멘트 등이 포함되어 있다.



public String buildClass(String className, String classComment, String interfaceName, List properties) throws CoreException {
try {
ASTParser parser = ASTParser.newParser(AST.JLS3);
parser.setSource("".toCharArray());
CompilationUnit unit = (CompilationUnit) parser.createAST(null);
unit.recordModifications();
AST ast = unit.getAST();

// 패키지명 선언
PackageDeclaration packageDeclaration = ast.newPackageDeclaration();
packageDeclaration.setName(JdtHelper.createPackageName(ast, className));
unit.setPackage(packageDeclaration);

// 타입선언
TypeDeclaration type = ast.newTypeDeclaration();
type.setInterface(false);
type.setName(ast.newSimpleName(className.substring(className.lastIndexOf('.') + 1)));
type.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD));

// javadoc추가
type.setJavadoc(JdtHelper.createTextJavadoc(ast, classComment, ""));

// 인터페이스 implements
if(interfaceName != null && interfaceName.trim().length() > 0) {
type.superInterfaceTypes().add(ast.newSimpleType(JdtHelper.createTypeName(ast, interfaceName)));
}

// 필드 추가
for(BeanPropertyModel property: properties) {
VariableDeclarationFragment vdf = ast.newVariableDeclarationFragment();
vdf.setName(ast.newSimpleName(property.getName()));
FieldDeclaration fd = ast.newFieldDeclaration(vdf);
fd.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PRIVATE_KEYWORD));
fd.setType(ast.newSimpleType(JdtHelper.createTypeName(ast, property.getType())));

type.bodyDeclarations().add(fd);

fd.setJavadoc(JdtHelper.createTextJavadoc(ast, property.getComment(), property.getName()));
}

// 필드의 getter/setter 메서드 추가.
for(BeanPropertyModel property: properties) {
type.bodyDeclarations().add(JdtHelper.createGetMethodDeclaration(ast, property.getType(), property.getName()));
type.bodyDeclarations().add(JdtHelper.createSetMethodDeclaration(ast, property.getType(), property.getName()));
}
unit.types().add(type);

Document document = new Document();
TextEdit edits = unit.rewrite(document, javaProject.getOptions(true));
edits.apply(document);
return document.get();
} catch(Exception ex) {
ex.printStackTrace();
throw new CoreException(DialogHelper.createErrorStatus("Class Build ERROR", ex));
}
}


그외 유틸리티 함수들


소스 코드 중간에 JdtHelper라는 유틸리티 클래스에서 Javadoc을 생성하거나 getter/setter 등을 생성한다.
Assignment라든지 메서드에 파라미터 전달하는 방법, this에 접근하는 방법등이 아래 소스 코드에 포함되어 있다.


public static Name createPackageName(AST ast, String className) {
String[] identifiers = getPackageIdentifiers(className);
if(identifiers == null || identifiers.length == 0) {
return null;
}
return ast.newName(identifiers);
}

public static Name createTypeName(AST ast, String className) {
String[] identifiers = getClassIdentifiers(className);

if(identifiers == null || identifiers.length == 0) {
return null;
}
return ast.newName(identifiers);
}

public static String[] getPackageIdentifiers(String className) {
if(className == null) {
return null;
}
String[] array = className.split("\\.");
if(array.length < 1) {
return null;
}

String[] result = new String[array.length - 1];
System.arraycopy(array, 0, result, 0, result.length);
return result;
}

public static String[] getClassIdentifiers(String className) {
if(className == null) {
return null;
}

return className.split("\\.");
}
public static String buildGetMethodName(String propName) {
if(propName == null || propName.length() == 0) {
return null;
}
return "get" + Character.toUpperCase(propName.charAt(0)) + propName.substring(1);
}

public static String buildSetMethodName(String propName) {
if(propName == null || propName.length() == 0) {
return null;
}
return "set" + Character.toUpperCase(propName.charAt(0)) + propName.substring(1);
}

@SuppressWarnings("unchecked")
public static MethodDeclaration createGetMethodDeclaration(AST ast, String returnType, String propertyName) {
MethodDeclaration mdGet = ast.newMethodDeclaration();
mdGet.setConstructor(false);
mdGet.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD));
mdGet.setReturnType2(ast.newSimpleType(JdtHelper.createTypeName(ast, returnType)));
mdGet.setName(ast.newSimpleName(JdtHelper.buildGetMethodName(propertyName)));

Block getBlock = ast.newBlock();
ReturnStatement returnStatement = ast.newReturnStatement();

FieldAccess fieldAccess = ast.newFieldAccess();
ThisExpression thisExpression = ast.newThisExpression();
fieldAccess.setExpression(thisExpression);
fieldAccess.setName(ast.newSimpleName(propertyName));

returnStatement.setExpression(fieldAccess);
getBlock.statements().add(returnStatement);

mdGet.setBody(getBlock);

return mdGet;
}

@SuppressWarnings("unchecked")
public static MethodDeclaration createSetMethodDeclaration(AST ast, String propertyType, String propertyName) {
MethodDeclaration mdSet = ast.newMethodDeclaration();
mdSet.setConstructor(false);
mdSet.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD));
mdSet.setReturnType2(ast.newPrimitiveType(PrimitiveType.VOID));
mdSet.setName(ast.newSimpleName(JdtHelper.buildSetMethodName(propertyName)));

SingleVariableDeclaration varDecl = ast.newSingleVariableDeclaration();
varDecl.setType(ast.newSimpleType(JdtHelper.createTypeName(ast, propertyType)));
varDecl.setName(ast.newSimpleName(propertyName));
mdSet.parameters().add(varDecl);

Block setBlock = ast.newBlock();
Assignment assignment = ast.newAssignment();
FieldAccess leftHandExpression = ast.newFieldAccess();
ThisExpression thisExpression = ast.newThisExpression();
leftHandExpression.setExpression(thisExpression);
leftHandExpression.setName(ast.newSimpleName(propertyName));
assignment.setLeftHandSide(leftHandExpression);

assignment.setOperator(Assignment.Operator.ASSIGN);

assignment.setRightHandSide(ast.newSimpleName(propertyName));
ExpressionStatement expressStatement = ast.newExpressionStatement(assignment);
setBlock.statements().add(expressStatement);

mdSet.setBody(setBlock);

return mdSet;
}


@SuppressWarnings("unchecked")
public static Javadoc createTextJavadoc(AST ast, String comment, String defaultComment) {
Javadoc javadoc = ast.newJavadoc();

if(comment == null || comment.trim().length() == 0) {
TagElement tag = ast.newTagElement();
TextElement te = ast.newTextElement();
te.setText(defaultComment);
tag.fragments().add(te);
javadoc.tags().add(tag);
} else {
for(String aLine : comment.split(System.getProperty("line.separator"))) {
TagElement tag = ast.newTagElement();
TextElement te = ast.newTextElement();
te.setText(aLine);
tag.fragments().add(te);
javadoc.tags().add(tag);
}
}
return javadoc;
}
@SuppressWarnings("unchecked")
public static void addSingleMemberAnnotation(AST ast, TypeDeclaration type, String annotationType, String literalValue) {
SingleMemberAnnotation anno = ast.newSingleMemberAnnotation();
anno.setTypeName(createTypeName(ast, annotationType));
StringLiteral literal = ast.newStringLiteral();
literal.setLiteralValue(literalValue);
anno.setValue(literal);

List list = (List) type.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY);
list.add(anno);

}


Resources
http://devdaily.com/java/jwarehouse/eclipse/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverterTest.java.shtml
http://www.ibm.com/developerworks/opensource/library/os-ast/?ca=dgr-lnxw961ASTParser
http://java.chinaitlab.com/Eclipse/38001.html

댓글 없음:

댓글 쓰기