2010년 3월 31일 수요일

Eclipse 단축키

유용한 단축 키들을 정리해 두었다. 사용하기 편리한 기능들을 계속해서 추가하도록 한다.
Ctrl + Shift + L을 누르면 모두 확인할 수 있다.


이클립스 단축키 목록 보기 : Ctrl + Shift + L

에디터
Switch Editor: Ctrl + Shift + E
Previous Editor : Ctrl + Shift + F6
Next Editor : Ctrl + F6

문서 편집
Insert Line Above Current Line : Ctrl + Shift + Enter
Insert Line Below Current Line : Shift + Enter
Move Lines Down : Alt + Down
Move Lines Up : Alt + Up

To Lower Case : Ctrl + Shift + Y
To Upper Case : Ctrl + Shift + X

Delete to End of Line : Ctrl + Shift + Delete (현재 라인에서 커서 뒷부분을 모두 지운다.)

Duplicate Lines: Ctrl + Alt + Up (선택된 라인을 복사하여 현재 커서 아래에 붙혀넣는다.)

Go to LIne : Ctrl + L
Go to Matching Bracket: Ctrl + Shift + P
Go to Next Member: Ctrl + Shift + Down
Go to Previous Member : Ctrl + Shift + Up

Debug JUnit Test : Alt + Shift + D, T
Run JUnit Test : Alt + Shift + X, T

Change Method Signature : Alt + Shift + C

2010년 3월 29일 월요일

maven에서 JDK 버전 설정

pom.xml 파일에 아래와 같이 입력한다.



JDK 컴파일러를 1.6으로 설정하고
소스 코드 문자 인코딩을 UTF-8로 설정하는 예제이다.

나중에 기회과 되면 maven에서 자주 사용할 수 있는 플러그인을 정리해야 겠다.
<build>
<finalName>eproject</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>

</plugins>

</build>



아래와 같이 설정하여도 가능하다.
<profiles>
<profile>
<activation>
<jdk>1.6</jdk>
</activation>
</profile>
</profiles>

2010년 3월 26일 금요일

CXF 웹 서비스 개발

1. web.xml


web.xml 파일에서는 cxf를 실행할 수 있는 환경을 설정해 주어야 한다.
아래의 샘플 파일에서는 스프링 설정 파일로 WEB-INF/beans.xml 파일을 설정하고 있다.
또한 웹 경로의 /services/* 로 시작하는 URL 은 CXFServlet으로 제어를 넘긴다.

web.xml

----------------------------------------------------------
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/beans.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>CXFServlet</servlet-name>
<display-name>CXF Servlet</display-name>
<servlet-class>
org.apache.cxf.transport.servlet.CXFServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>
</web-app>

----------------------------------------------------------


2. beans.xml


beans.xml 파일에서는 CXF에서 실행할 웹서비스 정보를 설정해 주면 된다.
아래의 경우에 /services/OrderProcess 라고 요청이 들어오면 demo.order.OrderProcessImpl
클래스의 특정 메서드가 실행된다.

beans.xml
----------------------------------------------------------

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">
<import resource="classpath:META-INF/cxf/cxf.xml" />
<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
<jaxws:endpoint
id="orderProcess"
implementor="demo.order.OrderProcessImpl"
address="/OrderProcess" />
</beans>
----------------------------------------------------------



3. Endpoint Service Interface


Interface : OrderProcess


----------------------------------------------------------
package demo.order;

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService
public interface OrderProcess {

@WebMethod
String processOrder(Order order);
}
----------------------------------------------------------


4. Service 구현 클래스



----------------------------------------------------------
package demo.order;

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService()
public class OrderProcessImpl implements OrderProcess {

@Override
public String processOrder(Order order) {
String orderID = validate(order);
return orderID;
}
@WebMethod(action="1", exclude=false)
private String validate(Order order) {
String custID = order.getCustomID();
String itemID = order.getItemID();
int qty = order.getQty();
double price = order.getPrice();

if(custID != null && itemID != null && !custID.equals("") &&
!itemID.equals("") && qty > 0 && price > 0.0) {
return "ORD1234";
}
return null;
}

}
----------------------------------------------------------


5. Soap Message


Soap Body로 리턴되는 클래스는 XmlRootElement annotation을 반드시 선언해 주어야 한다.

----------------------------------------------------------
package demo.order;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "Order")
public class Order {
private String customID;
private String itemID;
private int qty;
private double price;

public Order() {
super();
}

public String getCustomID() {
return customID;
}

public void setCustomID(String customID) {
this.customID = customID;
}

public String getItemID() {
return itemID;
}

public void setItemID(String itemID) {
this.itemID = itemID;
}

public int getQty() {
return qty;
}

public void setQty(int qty) {
this.qty = qty;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}
}
----------------------------------------------------------


6. 결과


이렇게 구성된 web application을 cxfsample-web.war 파일로 묶어서 정상적으로 톰켓에 디플로이 시켰다면,
http://localhost:8080/cxfsample-web/services/OrderProcess?wsdl 라고 웹브라우저의 주소창에 입력하였다면 화면에서 본 서비스의 WSDL 내용을 볼 수 있다.

7. 테스트


Soap 메시지 기반의 웹서비스 테스트는 SoapUI를 이용해서 테스트 할 수 있다.

REQUEST MESSAGE SAMPLE

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ord="http://order.demo/">
<soapenv:Header/>
<soapenv:Body>
<ord:processOrder>
<arg0>
<customID>u111</customID>
<itemID>a111</itemID>
<price>100</price>
<qty>10</qty>
</arg0>
</ord:processOrder>
</soapenv:Body>
</soapenv:Envelope>


RESPONSE MESSAGE Sample

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:processOrderResponse xmlns:ns2="http://order.demo/">
<return>ORD1234</return>
</ns2:processOrderResponse>
</soap:Body>
</soap:Envelope>

2010년 3월 19일 금요일

Eclipse Editor 열기

요구사항:
Eclipse 에디터 파트에 프로젝트에 저장되어 있는 파일을 연다.

아래와 같이 IDE 클래스를 이용하면 Eclipse의 에디터를 open 할 수 있다.


public static IEditorPart openInEditor(IFile file, boolean activate) throws PartInitException {
if (file == null) {
throwPartInitException("file is null");
}

IWorkbenchPage p= AmfPlugin.getActivePage();
if (p == null) {
throwPartInitException("active page is null");
}
IEditorPart editorPart= IDE.openEditor(p, file, activate);
// initializeHighlightRange(editorPart);
return editorPart;
}

private static void throwPartInitException(String message) throws PartInitException {
throwPartInitException(message, IStatus.OK);
}
private static void throwPartInitException(String message, int code) throws PartInitException {
IStatus status= new Status(IStatus.ERROR, IRuntimeConstants.PI_RUNTIME, code, message, null);
throw new PartInitException(status);
}




WorkbenchPage는 Activator 클래스에서 가져올 수 있다.


public static IWorkbenchWindow getActiveWorkbenchWindow() {
return getDefault().getWorkbench().getActiveWorkbenchWindow();
}

public static IWorkbenchPage getActivePage() {
IWorkbenchWindow window = getActiveWorkbenchWindow();
if(window == null) {
return null;
}
return window.getActivePage();
}

2010년 3월 10일 수요일

Eclipse(galileo)에 CXF 플러그인 설치

Eclipse 3.6 (Helios)에서는 이런 고민 필요없다.
CXF가 기본적으로 제공된다.



-- 이건 옛날 고민이 되어 버렸다.

웹서비스 플랫폼을 선택할 때 Axis2와 CXF 중에서 어떤 걸 선택할 지를 고민을 꽤 많이했다. 사실 그 고민은 아직도 진행중이긴 하다.
둘다 Apache Software Foundation 에서 관리하는 프로젝트 이긴 한데, 어떤 걸 선택할 때 참고가 될 지 모르겠지만 웹 블로그 하나를 소개하도록 하겠다.
http://www.theserverside.com/tt/articles/article.tss?l=AxisAxis2andCXF

위의 글도 2007년도에 작성되었기 때문에 두 프로젝트를 비교한 factor가 현재 시점에서 보면 맞지 않는 부분이 있는데, 그 이유는 버전이 올라가면서 둘간의 장점은 상쇄되고, 단점이 보완되어 가기 때문이다. 2009년 가을까지만 해도 Axis2가 JAX-RS를 지원하지 않아 CXF를 선택하는 데, 주저함이 없었지만 Axis2 1.5에서 Restful을 지원한다.

Axis2가 Spring가 통합되지 않는 건 아니지만, CXF는 Spring 기반으로 작성되어 있어 Spring 기반 프로젝트라면 좀 더 사용하기가 용이할 거라 생각한다.

Axis2의 장점이라면 Eclipse의 웹서비스 툴로써 기본적으로 Axis가 포함되어 있기 때문에 개발 시 별도의 플러그인을 설치할 필요가 없다는 것이다.

CXF 플러그인 WTP의 incubation으로 제공되고 있으며 개발자가 플러그인을 설치해 주어야 한다.

CXF 플러그인 설치
Help --> Install New Software... 메뉴를 클릭한 후 URL에 http://download.eclipse.org/webtools/updates 라고 입력하면 CXF Web Services 라는 플러그인을 찾을 수 있다. 선택하여 설치해 주어야 한다.




CXF 설치
http://cxf.apache.org/download.html 에서 CXF를 다운받아 로컬 디스크의 특정 위치에 압축을 푼다. (ex: d:\apache\cxf\v2.2.6)

Preference 설정





Resources:
http://www.eclipse4sl.org/documentation/userdoc/html/webservice/cxf/index.php
http://www.eclipse.org/webtools/

2010년 3월 8일 월요일

Wizard Page Template

본 문서는 수정중인 문서입니다.

Wizard를 만들다 보면 개발자 각각의 패턴화된 Wizard 및 WizardPage 개발 방식이 있다.

WizardPage에 대한 샘플을 소개한다.



package com.archnal.amf.ui.wizards;

import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;

public class NewRsMethodWizardPage extends WizardPage {

private static final String PAGE_NAME = "newRsMethodWizardPage";

public NewRsMethodWizardPage() {
super(PAGE_NAME);

setTitle("New Restful Method");
setDescription("Restful Method Configuration Page");
}

@Override
public void createControl(Composite parent) {
Composite container = new Composite(parent, SWT.NONE);

initialize();
setControl(container);
}

private void initialize() {
dialogChanged();
}

private void dialogChanged() {
updateStatus(null);
}

private void updateStatus(String message) {
setErrorMessage(message);
setPageComplete(message == null);
}
}


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

JDT Dialog에서 타입 validation check

JDT로 클래스나 인터페이스를 생성하거나 메소드나 필드를 생성할 때도 이름이 유효한지 체크할 필요가 있다.

org.eclipse.jdt.ui.wizards.NewTypeWizardPage 에서 사용하는 방식을 소개하도록 하겠다.
NewTypeWizardPage에서 타입 validation을 하는데 사용하는 클래스는 org.eclipse.jdt.core.JavaConventions 클래스다.

아래의 소스 코드는 NewTypeWizardPage의 일부이다.


private static IStatus validateJavaTypeName(String text, IJavaProject project) {
if (project == null || !project.exists()) {
return JavaConventions.validateJavaTypeName(text, JavaCore.VERSION_1_3, JavaCore.VERSION_1_3);
}
return JavaConventionsUtil.validateJavaTypeName(text, project);
}


JavaConventionsUtil도 내부적으로 JavaConventions를 사용한다.
JavaConventions는 아래와 같은 메서드들을 제공한다.


  • validateCompilationUnitName(String, String, String)

  • validateClassFileName(String, String, String)

  • validateFieldName(String, String, String)

  • validateIdentifier(String, String, String)

  • validateImportDeclaration(String, String, String)

  • validateJavaTypeName(String, String, String)

  • validateMethodName(String, String, String)

  • validatePackageName(String, String, String)

  • validateClasspathEntry(IJavaProject, IClasspathEntry, boolean)

  • validateTypeVariableName(String, String, String)



상기 메소드들은 모두 org.eclipse.core.runtime.IStatus 으로 리턴한다. IStatus의 getSeverity() 메서드와 getMessage() 메서드를 이용하여 에러 여부와 에러 메시지를 참조할 수 있다.

2010년 3월 5일 금요일

JDT에서 Annotation 수정하거나 추가시키기

요구사항: 클래스 Annotation 값 UI로 변경 가능하게 구현

Restful 서비스 클래스에는 아래와 같은 Annotation이 설정된다.
@javax.ws.rs.Produces("")
@javax.ws.rs.Path("")

Produces와 Path Annotation은 name-value pair로 값이 지정되는 일반 annotation이 아니라 안에 문자열로 값만을 저장하고 있는 annotation이다.
JDT에서는 이를 SingleMemberAnnotation이라 부르고, name-value pair로 값이 저장되는 것을 NormalAnnotation이라 부른다.

요구사항은 다름 아니라 Path와 Produces를 UI로 설정하면 해당 빈(클래스)에 annotation값이 변경되도록 하는 것이다.
이미 Produces나 Path annotation이 클래스에 존재하는 경우에는 StringLiteral의 문자열만 변경시켜주면 된다. 하지만 annotation이 없는 경우도 있을 테니 존재하는지 체크하는 로직과 추가시키는 로직도 포함해야 한다.

여기서도 여전히 SimpleName과 QualifiedName 문제가 있다. @javax.ws.rs.Path라고 설정하면 QualifiedName으로 @Path라고 설정하였으면 SimpleName으로 파싱된다.

createSingleMemberAnnotation 은 이미 annotation이 존재하는 경우 ASTRewrite에 의해서 replace하기 위해서 새로운 Annotation을 만드는 메서드고 findSingleMemberAnnotation 는 클래스내에 해당 Annotation이 존재하는지를 판단하는 메서드다.

Annotation을 추가(insert)하는 기능을 찾지 못하였다. 아시는 분은 댓글로 남겨 주세요. 그래서 클래스 시작 위치를 저장해 두었다가 모든 처리가 끝난 후 annotation 문자열 (ex "@javax.ws.rs.Path(\"xxx\")" 등)을 소스 코드에 추가시키도록 처리했다. 이는 모든 replace가 끝나고 document에 반영한 이후에 처리해 주어야 한다.

아래 소스에서 javaElement.getBuffer().replace(startPos, 0, sb.toString()); 부분이 소스파일에 문자열을 추가시키는 부분이다.

자세한 내용은 아래 소스를 살펴보기 바란다.



private void saveConfiguration() {
if (this.selectedBean == null) {
return;
}
String path = selectedBean.getBeanClass().replace('.', '/') + ".java";
try {


ICompilationUnit javaElement = (ICompilationUnit) javaProject
.findElement(new Path(path));
if (javaElement == null || javaElement.exists() == false) {
return;
}

Document document = new Document(javaElement.getSource());

CompilationUnit astRoot = ProjectHelper.parse(javaElement);


astRoot.recordModifications();

ASTRewrite rewrite = ASTRewrite.create(astRoot.getAST());
TypeDeclaration typeDecl = (TypeDeclaration) astRoot.types().get(0);

int startPos = typeDecl.getStartPosition();

SingleMemberAnnotation pathAnno = findSingleMemberAnnotation(
typeDecl, "Path", "javax.ws.rs.Path");


String ln = System.getProperty("line.separator");
StringBuffer sb = new StringBuffer();

if (pathAnno != null) {
SingleMemberAnnotation newPathAnno = createSingleMemberAnnotation(
astRoot, "javax.ws.rs.Path", pathText.getText());
rewrite.replace(pathAnno, newPathAnno, null);
} else {
sb.append("@javax.ws.rs.Path(\"").append(pathText.getText()).append("\")").append(ln);
}

SingleMemberAnnotation producesAnno = findSingleMemberAnnotation(
typeDecl, "Produces", "javax.ws.rs.Produces");

if (producesAnno != null) {
SingleMemberAnnotation newProducesAnno = createSingleMemberAnnotation(
astRoot, "javax.ws.rs.Produces", producesText.getText());
rewrite.replace(producesAnno, newProducesAnno, null);
} else {
sb.append("@javax.ws.rs.Produces(\"").append(producesText.getText()).append("\")").append(ln);
}


TextEdit edits = rewrite.rewriteAST(document, javaElement
.getJavaProject().getOptions(true));
edits.apply(document);


javaElement.getBuffer().setContents(document.get());

if(sb.length() > 0) {
javaElement.getBuffer().replace(startPos, 0, sb.toString());
}

javaElement.save(null, true);


} catch (Exception e) {
DialogHelper.showError(getShell(), e.getMessage());
AmfPlugin.log(e);
}
}

private static SingleMemberAnnotation createSingleMemberAnnotation(
CompilationUnit astRoot, String annotationFullyQualifiedName,
String literalValue) {
SingleMemberAnnotation newAnno = null;
newAnno = astRoot.getAST().newSingleMemberAnnotation();

String[] array = annotationFullyQualifiedName.split("\\.");
if (array.length < 1) {
return null;
}
Name typeName = astRoot.getAST().newName(array);
newAnno.setTypeName(typeName);
StringLiteral annoValue = astRoot.getAST().newStringLiteral();
annoValue.setLiteralValue(literalValue);
newAnno.setValue(annoValue);

return newAnno;
}

public static SingleMemberAnnotation findSingleMemberAnnotation(
TypeDeclaration typeDecl, String simpleName,
String fullyQualifiedName) {
List list = (List) typeDecl
.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY);

for (Object obj : list) {
if (obj instanceof SingleMemberAnnotation) {
SingleMemberAnnotation cast = (SingleMemberAnnotation) obj;
Name typeName = cast.getTypeName();
if (simpleName.equals(typeName.getFullyQualifiedName())
|| fullyQualifiedName.equals(typeName
.getFullyQualifiedName())) {
return cast;
}
}
}
return null;
}


2010년 3월 3일 수요일

Eclipse의 SVN으로 Commit Trac의 Ticket 정보가 포함되도록 함.


Trac을 Mylyn으로 연동하여 Task기반으로 프로그램을 개발한 후 SVN에 Commit할 때
Trac의 Ticket 정보를 Commit의 Comment로 포함시키는 설정이다.

Galileo에서 mylyn으로 Trac 과 연동되어 있다고 가정하고 진행하겠다.


Help --> Install New Software 메뉴를 선택한 후 Work with 콤보 박스에서 Galileo - http://download.eclipse.org/releases/galileo 를 선택한다. 이 사이트는 Galileo가 설치되면 콤보 박스에 포함되어 있다.
Collaboration --> Subversive SVN Integration for the Mylyn Project (Optional)(Incubation) 을 선택하여 설치하면 된다.





플러그인을 설치하고 eclipse를 restart 시키면 아래와 같이 SVN commit 시
Ticket과 관련된 주석이 자동으로 추가된다.












2010년 3월 2일 화요일

PrimitiveType, SimpleType, QualifiedType, ParameterizedType, ArrayType

JDT로 작업을 하다보면 org.eclipse.jdt.core.dom.Type 때문에 혼란스러웠던 적이 많았다.

타입의 하위 클래스로는 아래와 같은 것들이 있다.

  • ArrayType

  • ParameterizedType

  • PrimitiveType

  • QualifiedType

  • SimpleType

  • WildcardType



클래스 파일을 처리하는 경우라면 타입을 이렇게 복잡하게 정의해서 사용할 필요가 없겠지만,
JDT라서 이렇게 해야겠다는 생각이 든다.
JDT는 소스 파일을 파싱하고, 또 생성해내야 하기 때문이다.

컴파일된 클래스 파일에서 메소드에 대한 정의를 보면 언제나 아래와 같이 하나의 경우만 생긴다.


java.util.List<sample.User> findUsers(java.lang.String username)

하지만 자바 소스는 import 여부에 따라서 다음과 같은 소스 코드가 모두 가능하다.

List<User> findUsers(String username)
java.util.List<User> findUsers(java.lang.String username)
List<sample.User> findUsers(String username)
...


위와 같이 각각의 경우를 모두 다르게 처리해 주어야 하기 때문에 Type이 이처럼 세분화되어 있다.

java.util.List, sample.User와 같이 fullyQualified 명으로 타입이 작성된 경우는 QualifiedType이고,
List, User와 같이 패키지 정보는 생략한 채 클래스명으로만 작성된 경우는 SimpleType이다.
List<User> 와 같이 Generic을 사용한 경우는 ParameterizedType 이다.
String[], String[][]와 같은 타입은 ArrayType 이다.
int, long, void등과 같은 primitive 타입인 경우는 PrimitiveType 이다.
List<? extends User> 와 같이 Generic을 확장한 경우에는 WildcardType 이다.

PrimitiveType과 QualifiedType인 경우에는 toString()을 하면 언제나 클래스 정보와 일치하기 때문에 문제가 되지 않는다. 하지만 SimpleName이 오히려 이름과는 달리 상황을 매우 복잡하게 만든다.

JDT의 경우 메소드의 리턴타입이나 파라미터에 대한 타입을 Type 으로 처리한다.
메소드를 호출하는 소스 코드를 생성하는 경우에 메서드의 리턴타입이나 파라미터 타입은 메소드를 구현한 클래스에서 import 한 것이기 때문에 SimpleType으로 작성되어 있는 메소드를 호출하는 클래스에서는 오류가 발생할 수도 있다. 우연찮게 호출하는 클래스에서도 import가 되어 있으면 오류가 발생하지 않을 수도 있다.

CallerClass가 CalledClass의 List<User> findUsers(String username) 메서드를 호출하는 소스 코드를 생성하고 싶다면 List, User와 같은 클래스를 import 하는 소스를 추가하거나 java.util.List, sample.User 등과같이 fullyQualified 이름으로 변경해서 소스를 생성해야 한다.

다시 말해서 SimpleType을 QualifiedType으로 변경할 필요가 생긴다는 이야기다.

이와 같은 경우 List를 java.util.List로 User를 sample.User로 변경해야 하는 데 이런 내막을 알고 있는 클래스는 CallerClass가 아니라 CalledClass다. CalledClass에서 작성된 메서드기 때문이다.

그래서 이런 작업을 해주는 JDT 클래스가 바로 org.eclipse.jdt.core.IType이고, String[][] resolveType(String typeName) 메서드를 통해서 원하는 결과를 얻을 수 있다.
resolveType() 메서드의 결과로 2차원 배열이 리턴되는데, 왜 2차원으로 했는지는 좀 의하하지만 typeName으로 "List"라고 값을 넘겨주었다면 result[0][0] = "java.util", result[0][1] = "List"가 리턴된다.
resolve 할 수 없는 경우에는 Null이 리턴되고 resolve 할 수 있는 경우에는 result[0][0] + "." + result[0][1] 로 하면 fullyQualified 이름이 된다.
typeName이 fullyQualified 명(ex, java.util.List)로 넘겨진다 하더라도 결과는 위와 같다.

ArrayType인 경우에는 getComponentType() 메소드를 통해서 배열 컴포넌트의 타입을 읽어올 수 있다.
String[] 인 경우에 getComponentType은 String이 되고, String[][]인 경우에는 getComponentType은 String[]이 된다. recursive하게 호출하면 fullyQualified명을 얻을 수 있다.

아래의 코드는 타입을 fullyQualifiedName으로 변경하는 메서드다.
이런 기능을 하는 JDT 유틸리티 클래스를 알고 있다면 댓글에 알려주시면 감사..


public String toTypeString(IType javaType, Type type) {
if(type.isPrimitiveType()) {
return type.toString();
} else if(type.isSimpleType()) {
if(javaType == null) {
return type.toString();
}
String[][] result = null;
try {
result = javaType.resolveType(type.toString());
} catch (JavaModelException e) {
AmfPlugin.log(e);
return type.toString();
}

if(result != null && (result.length > 0 && result[0].length == 2)) {
return result[0][0] + "." + result[0][1];
}
return type.toString();
} else if(type.isQualifiedType()) {
return ((QualifiedType) type).getName().getFullyQualifiedName();
} else if(type.isParameterizedType()) {
StringBuffer sb = new StringBuffer();
ParameterizedType parameterizedType = (ParameterizedType) type;
sb.append(toTypeString(javaType, parameterizedType.getType()));
sb.append("<");
List typeArgs = parameterizedType.typeArguments();
for(int i = 0, len = typeArgs.size(); i <> 0) {
sb.append(", ");
}
sb.append(toTypeString(javaType, (Type) typeArgs.get(i)));
}
sb.append(">");
return sb.toString();
} else if(type.isArrayType()) {
ArrayType arrayType = (ArrayType) type;
Type componentType = arrayType.getComponentType();
// 다차원 배열인 경우에는 component 타입이 또다시 ArrayType으로 리턴되어 recursive하게 호출된다.
StringBuffer sb = new StringBuffer();
sb.append(toTypeString(javaType, componentType));
sb.append("[]");
return sb.toString();

} else {
return type.toString();
}

}



클래스명으로 IType을 리턴하는 아래 코드를 참조


public static IType findType(IJavaProject javaProject, String classname) throws JavaModelException {
ICompilationUnit cu = findJavaElement(javaProject, classname);
return cu.getType(classname.substring(classname.lastIndexOf('.') + 1));
}

public static ICompilationUnit findJavaElement(IJavaProject javaProject, String classname) throws JavaModelException {
String javaPath = classname.replace('.', '/') + ".java";
return (ICompilationUnit) javaProject.findElement(new Path(javaPath));
}


MethodDeclaration 데이터 처리

org.eclipse.jdt.core.dom.MethodDeclaration은 Eclipse의 JDT에 정의된 클래스로, 이클립스에서 자바 소스 코드를 작성할 때 메서드에 대한 여러가지 정보를 읽어 내는데 많이 사용되는 클래스다.

1. 파라미터 정보 읽기


List parameters = method.parameters();
for(int i = 0, len = parameters.size(); i <> 0) {
sb.append(", ");
}
SingleVariableDeclaration var = (SingleVariableDeclaration) parameters.get(i);
sb.append(var.getType()).append(" ");
sb.append(var.getName());
}

2010년 3월 1일 월요일

JDT를 이용하여 java 파일에 필드 및 getter/setter 추가하기

요구사항:
Wizard에서 필드 타입, 필드명, getter/setter 생성 여부, 주석등을 입력받아서 해당 java 파일에 관련 소스 코드를 생성한다.

필드 추가는
org.eclipse.jdt.core.IType
IField createField(String contents, IJavaElement sibling, boolean force, IProgressMonitor monitor) throws JavaModelException; 메서드를 이용하여 생성하였다.
getter / setter 메소드는 이클립스에서 제공하는 org.eclipse.jdt.internal.corext.codemanipulation.AddGetterSetterOperation 를 이용하였다.


public static void addProperty(IProgressMonitor monitor, ICompilationUnit unit, IType type, String propertyType, String propertyName, boolean generateGetter, boolean generateSetter, String comment) throws JavaModelException, CoreException{
IField field = type.getField(propertyName);
if(field.exists() == false) {
StringBuffer sb = new StringBuffer();

if(comment == null || comment.trim().length() == 0) {
comment = propertyName;
}
sb.append(buildFieldComment(comment));
String contents = "private " + propertyType + " " + propertyName + ";";

sb.append(contents);

field = type.createField(sb.toString(), null, true, monitor);
}

IField[] getterFields = new IField[0];
IField[] setterFields = new IField[0];
IField[] accessors = new IField[0];

if(generateGetter) {
getterFields = new IField[1];
getterFields[0] = field;
}
if(generateSetter) {
setterFields = new IField[1];
setterFields[0] = field;
}


CodeGenerationSettings settings= JavaPreferencesSettings.getCodeGenerationSettings(unit.getJavaProject());
// getter/setter의 주석을 추가시킨다.
settings.createComments = true;
AddGetterSetterOperation op = new AddGetterSetterOperation(type, getterFields, setterFields, accessors, new RefactoringASTParser(AST.JLS3).parse(type.getCompilationUnit(), true), null, null, settings, true, false);

op.run(monitor);
}

public static String buildFieldComment(String comment) {
StringBuffer sb = new StringBuffer();
sb.append("/**").append(lineDelimiter);
if(comment == null || comment.trim().length() == 0) {
sb.append(" *");
} else {
for(String aLine : comment.split(lineDelimiter)) {
sb.append(" * ").append(aLine).append(lineDelimiter);
}
}
sb.append(" */");
sb.append(lineDelimiter);
return sb.toString();
}


IType.getField() 메소드는 필드가 존재하지 않는 경우에도 값이 리턴이 되므로
if(field == null) 이렇게 비교하면 안된다.

getterFields, setterFields, accessors는 null로 전달하면 NullPointerException이 발생하므로 주의해야 한다.
getter, setter 메소드를 생성하기 전에 반드시 field먼저 추가해야 에러가 발생하지 않는다.

IType의 getMethod 사용

getMethod 시 parameterTypeSignatures
org.eclipse.jdt.code.IType 인터페이스에 정의된 IMethod getMethod(String name, String[] parameterTypeSignatures) 사용하여 메소드를 가져오기 위해서는 파라미터 타입 정보를 넘겨줘야 한다.
하지만 이 타입정보라는 것이 int, String 과 같은 클래스 정보를 넘겨줘야 하는 것이 아니다.
JDT에서 사용하는 signature 문자열로 넘겨 줘야 하는데 org.eclipse.jdt.core.Signature api doc 을 참조하면 자세한 내용을 볼 수 있다.
타입정보를 아래와 같이 설정해 주어야 한다.
엄밀히 말하면 TypeSignature는 클래스의 bytecode에 저장되는 메소드의 파리미터 타입 지정 방식이다.



TypeSignature ::=
"B" // byte
| "C" // char
| "D" // double
| "F" // float
| "I" // int
| "J" // long
| "S" // short
| "V" // void
| "Z" // boolean
| "T" + Identifier + ";" // type variable
| "[" + TypeSignature // array X[]
| "!" + TypeSignature // capture-of ?
| ResolvedClassTypeSignature
| UnresolvedClassTypeSignature



파라미터의 클래스 정보를 위와 같이 변경하기에 다소 불편함이 있어서 JDT에서는 이와 관련된 유틸리티 클래스를 제공한다.

org.eclipse.jdt.internal.core.util.Util 클래스다.

다음은 사용 예다.
파라미터 클래스를 Signature로 변경하는 부분을 참조하기 바란다.


...

MethodDeclaration method = (MethodDeclaration) element;
String name = method.getName().getFullyQualifiedName();

List parameters = method.parameters();
String[] parameterTypeSignatures = new String[parameters.size()];

for(int i = 0; i < parameterTypeSignatures.length; i++) {
SingleVariableDeclaration var = (SingleVariableDeclaration) parameters.get(i);
parameterTypeSignatures[i] = Util.getSignature(var.getType());
}

IMethod javaMethod = type.getMethod(name, parameterTypeSignatures);

...