2010년 10월 13일 수요일

JSR 303 (Bean Validation)

Domain 모델은 데이터베이스에 저장되는 Entity로 사용될 뿐만 아니라 사용자로 부터 입력받는 폼, 컨트롤러, 서비스, DAO 등 다양한 서비스 계층에서 사용되어 진다.
이러한 Domain 객체를 사용하는 각 서비스 계층에서는 도메인 객체의 프로퍼티 값이 유효한지 체크하게 된다.
예를 들면 컨트롤러에서는 사용자 입력 폼에서 필수 입력 항목이라든지 포맷등을 체크하거나 DAO에서 저장하기 전에 필수값 체크, 자리수 체크 등을 해야 한다.
스프링에서도 org.springframework.validation.Validator 인터페이스를 이용한 validation 기능을 제공한다. Spring3.x 에서는 JSR-303 의 Bean Validation을 이용한 validation을 사용할 수 있다.
이번 포스트에서는 JSR-303(Bean validation) 기능을 이용하여 Validation 기능을 사용하는 방법을 알아보기로 하자.

maven의 pom.xml 파일에 아래와 같이 dependency를 추가해 주어야 한다.
...
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.4</version>
      <type>jar</type>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>4.0.2.GA</version>
      <type>jar</type>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.6.1</version>
      <type>jar</type>
      <scope>compile</scope>
    </dependency>
  </dependencies>
...

사용자 등록을 위한 User 도메인 객체를 생각해보자.
userId, password, email, age를 포함하고 있다고 가정하자

package myproject.spring3.validator;

public class User {
  
  String userId;
  String password;
  String email;
  int age;
  
  public String getUserId() {
    return userId;
  }
  public void setUserId(String userId) {
    this.userId = userId;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }

}


위의 도메인 객체에 validation 기능을 추가해 보도록 하겠다.
우선 사용자 가입 시를 생각해 보자.
userId, password, age(19세 이상체크 왜?)는 필수 입력사항이다.
email은 null을 허용하지만 값이 설정되면 email포맷에 적합해야 한다.

Bean Validation은 annotation을 이용하여 validation체크를 한다.

package myproject.spring3.uservalidation;

import javax.validation.constraints.Min;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;

public class User {
  @NotEmpty
  String userId;
  @NotEmpty
  String password;
  @Email
  String email;
  @Min(value=19)
  int age;
  
  public String getUserId() {
    return userId;
  }
  public void setUserId(String userId) {
    this.userId = userId;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }

}


위에서 설정한 annotation으로 validation을 체크하는 소스는 아래와 같다.

package myproject.spring3.uservalidation;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;

public class UserValidationTest {
  Validator validator;
  
  @Before
  public void init() throws Exception {
    ValidatorFactory factory = 
      Validation.buildDefaultValidatorFactory();
    this.validator = factory.getValidator();
  }
  
  @Test
  public void testUserInsertValidation_OK() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setAge(29);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
  }

  @Test
  public void testUserInsertValidation_Email_Error() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("invalid-email");
    user.setAge(29);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 1);
  }

  @Test
  public void testUserInsertValidation_Email_Age_Error() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("invalid-email");
    user.setAge(18);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 2);
  }

  @Test
  public void testUserInsertValidation_Email_Age_Password_Error() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword(null);
    user.setEmail("invalid-email");
    user.setAge(18);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 3);
  }
  
}


2개 이상의 Validator 사용하기


도메인 객체의 property에 validation 을 annotation으로 설정하는 경우 2개 이상의 다른 validation을 설정해야 하는 경우는 어떻게 처리가 가능한가?
예를 들면 사용자 등록시에는 userId, password, email(null 이 아닌 경우에 포맷체크), age(19세 이상)에 대한 validation 체크를 하고, 삭제시에는 userId와 password에 대한 validation 체크를 한다고 가정해보자. User 도메인 클래스는 하나 밖에 없는데, Validation은 2개 이상처리해야 하므로 별도의 처리가 필요하다. 바로 groups라는 annotation 속성을 이용하면 된다.
groups는 클래스 목록을 설정할 수 있는 속성이다. 이름만 식별할 수 있는 인터페이스(또는 클래스)를 만들어서 각 property의 validator의 groups 속성에 지정한다.
위의 예제에서 사용자 등록 validation을 UserCreateGroup이라고 지정하고 삭제 validaton을 UserDeleteGroup이라고 지정한다.
userId와 password는 등록과 삭제시 공통으로 체크해야 하는 항목이므로 groups={ UserCreateGroup.class, UserDeleteGroup.class}로 지정하고, email과 age는 등록 시에만 체크해야 하므로 groups={UserCreateGroup.class}로 지정하면 된다.
아래에 변경된 User 클래스다.

package myproject.spring3.uservalidation;

import javax.validation.constraints.Min;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;

public class User {
  @NotEmpty(groups={UserCreateGroup.class, UserDeleteGroup.class})
  String userId;
  @NotEmpty(groups={UserCreateGroup.class, UserDeleteGroup.class})
  String password;
  @Email(groups={UserCreateGroup.class})
  String email;
  @Min(value=19, groups={UserCreateGroup.class})
  int age;
  
  public String getUserId() {
    return userId;
  }
  public void setUserId(String userId) {
    this.userId = userId;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }

}


package myproject.spring3.uservalidation;

public interface UserCreateGroup {

}


package myproject.spring3.uservalidation;

public interface UserDeleteGroup {

}


위의 테스트 케이스에 사용자 등록 및 삭제에 대한 테스트 메서드를 아래와 같이 추가하면 테스트가 가능하다.

@Test
  public void testUserCreateValidation() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("valide-email@gmail.com");
    user.setAge(19);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user, UserCreateGroup.class);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

  @Test
  public void testUserDeleteValidation() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail(null);
    user.setAge(0);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user, UserDeleteGroup.class);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

  @Test
  public void testUserCreateAndDeleteValidation() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("valid-email@gmail.com");
    user.setAge(20);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user, UserDeleteGroup.class, UserCreateGroup.class);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

  @Test
  public void testUserWithoutGroup() throws Exception {
    User user = new User();
    
//    user.setUserId("userid");
//    user.setPassword("password");
//    user.setEmail("valid-email@gmail.com");
//    user.setAge(0); 

    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

validator.validate 메서드의 첫 번째 파라미터는 validation을 체크하고 싶은 도메인 객체를 넘긴다.
두번째 파라미터부터는 varargs를 이용하여 Group 클래스를 n개 넘길 수 있다. 위의 세번째 메서드가 Create와 Delete를 동시에 validation 체크하는 예제다. 이렇게 사용할 경우가 있을지는 좀 고민해볼 사항이다.
Validation annotation의 groups 속성을 지정하였을 경우, validator.validate에 Group 클래스를 지정하지 않으면 무조건 유효한 것으로 처리한다.

2010년 9월 28일 화요일

CXF에서 Log4J 사용하기

Resources:
http://www.techper.net/2008/01/30/configuring-cxf-logging-to-go-through-log4j/
http://gtko.springnote.com/pages/4826007

CXF에서 사용하는 로그는 별도로 지정하지 않으면 디폴트로 java.util.logging.Logger를 사용한다.
CXF에서 log4j를 사용하려면 다음과 같이 지정해 주면 된다.

1. -D옵션을 이용하여 시스템 property로 지정한다.
-Dorg.apache.cxf.Logger=org.apache.cxf.common.logging.Log4jLogger

2. META-INF/cxf/org.apache.cxf.Logger 파일로 설정한다.
-D옵션을 설정하기 용이치 않으면 위의 클래스패스에 위치한 파일에 아래와 같이 설정한다.
org.apache.cxf.common.logging.Log4jLogger

일반적으로 -D 옵션을 이용하는 방법과 동일한 결과를 프로그래밍적으로 해결하기 위한 방법으로
System.setProperty("org.apache.cxf.Logger", "org.apache.cxf.common.logging.Log4jLogger");
와 같이 설정하여 처리하고자 한다면 정상적으로 작동하지 않을 것이다.
이는 Spring이 초기화 되는 시점에 CXF 관련 빈들도 초기화 하는데 위의 코드가 반드시 CXF 빈들보다 먼저 초기화 된다고 볼 수 없기 때문이다.

1 또는 2의 방법중에서 하나만 설정해도 정상적으로 동작한다.
특히나 Inbound/Outbound 메시지를 보고 싶다면 log4j 설정파일에서
INFO레벨로 로그를 설정해주면 된다.

<logger name="org.apache.cxf" additivity="false">
<level value="INFO" />
<appender-ref ref="File_Appender" />
</logger>

2010년 9월 27일 월요일

SuppressWarnings value값 지정

Resources:
- http://knol.google.com/k/suppresswarnings-annotation-in-java#
- http://download.oracle.com/javase/tutorial/java/javaOO/annotations.html


SuppressWarnings 의 값들은 Java에서 제공하는 표준 값들과 Eclipse와 같은 IDE에서 자체적으로 정의한 값들이 있다.
SuppressWarnings의 값으로 지정하면 IDE에서 해당 Warning이 발견되면 Warning을 무시한다.

JDK에서 정의한 값들은 아래와 같다.

For example, Sun JDK 1.5 shows:

* all - suppress all warnings from this code
* deprecation - suppress warnings from using deprecated code
* unchecked - suppress warnings from an unchecked call or an unchecked cast
* fallthrough - suppress warnings if a switch falls through without finding a valid case (and no default)
* path -
* serial - suppress warnings if a Serializable class does not define a serialVersionUID
* finally - suppress warnings from return within a finally (which will ignore return with the try)


And Sun JDK 1.6 adds:

* cast
* divzero - suppress warnings if integer divide by zero is detected
* empty
* overrides
* none


Eclipse는 JDK에서 정의한 값을 포함하여 아래의 값들을 사용할 수 있다.

The Eclipse warning values for Eclipse 3.3 are documented in the JDT docs.

* all - suppress all warnings
* boxing - suppress warnings relative to boxing/unboxing operations
* cast - suppress warnings relative to cast operations
* dep-ann - suppress warnings relative to deprecated annotation
* deprecation - suppress warnings relative to deprecation
* fallthrough - suppress warnings relative to missing breaks in switch statements
* finally - suppress warnings relative to finally block that don't return
* hiding - suppress warnings relative to locals that hide variable
* incomplete-switch - suppress warnings relative to missing entries in a switch statement (enum case)
* nls - suppress warnings relative to non-nls string literals
* null - suppress warnings relative to null analysis
* restriction - suppress warnings relative to usage of discouraged or forbidden references
* serial - suppress warnings relative to missing serialVersionUID field for a serializable class
* static-access - suppress warnings relative to incorrect static access
* synthetic-access - suppress warnings relative to unoptimized access from inner classes
* unchecked - suppress warnings relative to unchecked operations
* unqualified-field-access - suppress warnings relative to field access unqualified
* unused - suppress warnings relative to unused code



Examples

An example of specifying a single warning:
@SuppressWarnings("unchecked")
public void methodWithScaryWarnings() {
List rawList = new ArrayList();
List stringList = (List)rawList;
}

An example of using two warnings:
@SuppressWarnings({"unchecked","deprecation"})
public void methodWithScaryWarnings() {
callDeprecatedMethod();
}

2010년 9월 17일 금요일

Spring에서 Controller JUnit으로 테스트하기

작성중...

Resources:
http://www.coderanch.com/t/459840/Spring/Integration-testing-Spring-Annotation-based

Controller에 대한 JUnit테스트는 조금 복잡한다.
Controller를 제외한 Repository, Service, Component stereotype들은 Dependency Injection이 복잡하지 않다. 하지만 Controller는 서블릿 컨텍스트를 초기화하여 Injection해야 하기 때문에 여러가지로 복잡한 부분이 있다.
이러한 문제 때문에 webmvc 관련된 설정은 기타 스프링 설정 파일과는 별로의 폴더에 설정한다.

아래와 같은 컨트롤러가 개발되어 있다고 가정하자.


package mypackage;

@Controller("accountController")
@RequestMapping("/accountController.sh")
public class AccountController extends BaseController {
...  
  
  @RequestMapping(params = "method=list")
  public String listAccounts(HttpServletRequest request, 
   @ModelAttribute AccountCondition condition,
   BindingResult result1, ModelMap map) throws Exception {

    
    
    map.addAttribute("accountCondition", condition);
    
    return "/account/account_lst";
  }
}


이 컨트롤러를 JUnit으로 테스트 하려면 WebContextLoader가 필요하다.
Tomcat과 같은 서블릿 컨테이너에서 테스트가 실행되는 것이 아니기 때문에 이와 비슷한 기능을 하는 Mock Web Context가 필요하다.

package mypackage;

import javax.servlet.ServletContext;

import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.context.support.AbstractContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;

public class MockWebContextLoader extends AbstractContextLoader {

  public static final ServletContext SERVLET_CONTEXT = 
    new MockServletContext("/src/main/webapp", new FileSystemResourceLoader());
  
  private final static GenericWebApplicationContext webContext =
    new GenericWebApplicationContext();
      
  protected BeanDefinitionReader createBeanDefinitionReader(
    final GenericApplicationContext context) {
    return new XmlBeanDefinitionReader(context);  
  }

  @Override
  protected String getResourceSuffix() {
    System.out.println("getResourceSuffic()");
    return ".xml";
  }

  @Override
  public ApplicationContext loadContext(String... locations) throws Exception {
    SERVLET_CONTEXT.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, webContext);
    webContext.setServletContext(SERVLET_CONTEXT);
    createBeanDefinitionReader(webContext).loadBeanDefinitions(locations);
    AnnotationConfigUtils.registerAnnotationConfigProcessors(webContext);
    webContext.refresh();
    webContext.registerShutdownHook();
    return webContext;
  }
  
  public static WebApplicationContext getInstance() {
    return webContext;
  }
}


위 WebContextLoader에서 주의 깊게 살펴 볼 메서드는 loadContext다.
maven에서 web resource를 관리하는 폴더인 src/main/webapp에서 리소스를 읽어 오도록 설정되어 있다.

컨트롤러 테스트 클래스들은 위에서 작성한 MockWebContextLoader를 사용하는 부분이 공통적으로 필요하다.
아래의 클래스는 DispatchServlet을 초기화 시키는 부분이 포함되어 있다.
또한 MockHttpServletRequest와 MockHttpServletResponse를 생성하는 메서드도 포함하고 있다.

package mypackage;

import java.util.Map;

import javax.servlet.ServletException;

import org.junit.runner.RunWith;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import junit.framework.TestCase;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=MockWebContextLoader.class, 
  locations={"classpath*:/spring/context-*.xml", 
    "classpath*:/springmvc/common-servlet.xml"})
public class AbstractControllerTestSupport extends TestCase {

  private static DispatcherServlet dispatcherServlet;
  
  @SuppressWarnings("serial")
  public static DispatcherServlet getServletInstance() {
    if(dispatcherServlet == null) {
      dispatcherServlet = new DispatcherServlet() {
        protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
          return MockWebContextLoader.getInstance();
        }
      };
      
      try {
        dispatcherServlet.init(new MockServletConfig());
      } catch(ServletException ex) {
      }
    }
    return dispatcherServlet;
  }
  
  protected MockHttpServletRequest mockRequest(String method, String uri, Map params) {
    MockHttpServletRequest req = new MockHttpServletRequest(method, uri);
    
    if(params != null) {
      for(String key: params.keySet()) {
        req.addParameter(key, params.get(key));
      }
    }
    return req;
  }
  
  protected MockHttpServletResponse mockResponse() {
    return new MockHttpServletResponse();
  }
}


이제 실제로 컨트롤러를 테스트를 작성할 차례다.

package mypackage;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.annotation.Resource;

import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.BindingResultUtils;

import mypackage.AccountCondition;

public class AccountControllerTest extends AbstractControllerTestSupport {

  @Resource(name="accountController")
  AccountController accountController;
  
  @Test()
  public void testListAccounts() throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    
    
    AccountCondition condition = new AccountCondition();
//    Map map = request.getParameterMap();
    Map map = new HashMap();
    
    BeanPropertyBindingResult result1 = new BeanPropertyBindingResult(condition, "accountCondition");

    map.put(BindingResult.MODEL_KEY_PREFIX + "accountCondition", result1);
    
    condition.setQueryEmail("credemol@gmail.com");
    map.put("accountCondition", condition);

    ModelMap modelMap = new ModelMap();
    String mav = accountController.listAccounts(request, condition, result1, modelMap);
    
    Collection result = (Collection) modelMap.get("accountResult");
    
    for(Iterator it = result.iterator(); it.hasNext();  ) {
      Object obj = it.next();
      
      System.out.println("obj: " + obj);
    }
  }
}

2010년 8월 30일 월요일

Spring에서 DWR 사용하기

DWR(Direct Web Remoting)은 일반 서비스를 Ajax 형태로 제공할 수 있도록 자동 변환해주는 기능을 제공한다.
서버사이드 기능은 DwrServlet에서 처리되고, 클라이언트 기능은 자동 생성되는 자바 스크립트에서 대부분의 기능이 구현된다.

TrafficInfo(도메인객체)를 리턴하는 검색 기능을 제공하는 TrafficService 가 있다고 가정하자
TrafficService를 DWR을 이용하여 개발하는 예제를 살펴보도록 하자.

TrafficInfo.java - 도메인 객체
package com.roadrantz.traffic;

public class TrafficInfo {

  private String summary;
  private String details;
  public TrafficInfo() {
    super();
  }
  public String getSummary() {
    return summary;
  }
  public void setSummary(String summary) {
    this.summary = summary;
  }
  public String getDetails() {
    return details;
  }
  public void setDetails(String details) {
    this.details = details;
  }
}


TrafficService.java - 서비스 인터페이스
package com.roadrantz.traffic;

public interface TrafficService {

  TrafficInfo[] getTrafficInfo(String zipCode, int zoom, int severity);
}


TrafficServiceImpl.java - 서비스 구현클래스
package com.roadrantz.traffic;

public class TrafficServiceImpl implements TrafficService {
  private String appId = "springinaction";
    
  public TrafficServiceImpl() {
    super();
  }

  public String getAppId() {
    return appId;
  }

  public void setAppId(String appId) {
    this.appId = appId;
  }

  @Override
  public TrafficInfo[] getTrafficInfo(String zipCode, int zoom, int severity) {
    try {
      TrafficInfo[] trafficInfo = new TrafficInfo[2];
      
      trafficInfo[0] = new TrafficInfo();
      trafficInfo[0].setSummary("summary 1");
      trafficInfo[0].setDetails("detail 1");
      
      trafficInfo[1] = new TrafficInfo();
      trafficInfo[1].setSummary("summary 2");
      trafficInfo[1].setDetails("detail 2");
      
      return trafficInfo;

    } catch(Exception e) {
      e.printStackTrace(System.out);
      return new TrafficInfo[] {};
    }
  }
}


위의 세 클래스는 보통 스프링에서 서비스를 개발하는 것과 다를게 없다.
이렇게 개발된 서비스를 별도의 컨트롤러나 Restful 개발 프레임웍을 사용하지 않고
AJAX 형태로 서비스를 제공하는 방법을 살펴보자.

먼저 WEB-INF에 web.xml 파일과 dwr.xml파일을 작성해 주어야 한다.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>simple-dwr-project</display-name>
  
  <servlet>
    <servlet-name>dwr</servlet-name>
    <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
    <init-param>
      <param-name>debug</param-name>
      <param-value>true</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>dwr</servlet-name>
    <url-pattern>/dwr/*</url-pattern>
  </servlet-mapping>

</web-app>

/dwr/* 으로 요청된 모든 URL을 DwrServlet이 처리되도록 서블릿을 설정한 예제다.
URL 패턴은 개발환경에 맞게 설정하면된다.

WEB-INF/dwr.xml
<!DOCTYPE dwr PUBLIC
  "-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN"
  "http://www.getahead.ltd.uk/dwr/dwr10.dtd">
<dwr>
  <allow>
    <convert match="com.roadrantz.traffic.TrafficInfo" converter="bean"></convert>
    <create javascript="Traffic" creator="new">
      <param name="class" value="com.roadrantz.traffic.TrafficServiceImpl"></param>
      <exclude method="setAppId"/>
      <exclude method="getAppId"/>
    </create>
  </allow>
</dwr>  

위의 설정파일은 TrafficInfo 서비스를 DWR을 이용하여 Ajax로 서비스를 제공할 수 있도록 한다.
서비스와 무관한 setAppId와 getAppId는 ajax서비스에서 배제시킨다.

아래의 소스는 JSP파일에서 JavaScript를 이용해서 DWR 서비스를 호출하는 예제다.
실제로 DWR서비스를 호출하여 패킷을 살펴보면 Json 포맷의 응답이 전달되는 것을 확인할 수 있다.

아래의 예제는 zipCode가 5자리가 될 때 DWR서비스를 호출하는 예제다.
아래의 소스에서 주의깊게 살펴볼 부분은
스크립트 파일을 include하는 부분과 cellFuncs 부분이다.

dwr/engine.js 파일과 dwr/util.js 파일은 DWR에서 공통적으로 제공하는 파일들이고,
dwr/interface/Traffic.js는 TrafficService를 호출하기 위해서 DWR이 생성한 스크립트 파일이다.

cellFuncs는 AJAX로 호출한 결과를 Table의 TD로 설정해 주는 역할을 한다.


샘플 Jsp 파일
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>DWR Sample</title>

<script type="text/javascript" src="dwr/engine.js"></script>
<script type="text/javascript" src="dwr/interface/Traffic.js"></script>
<script type="text/javascript" src="dwr/util.js"></script>

<script type="text/javascript">

function criteriaChanged() {
  var zipCode = document.trafficForm.zip.value;
  var zoom = document.trafficForm.zoom.value;
  var severity = document.trafficForm.severity.value;

  if(zipCode.length == 5) {
    Traffic.getTrafficInfo(zipCode, zoom, severity, updateTable);
  } else {
    DWRUtil.removeAllRows("trafficTable");
  }
}

var cellFuncs = [
                 function(data) {
                     var cb = document.createElement("input");
                     cb.type = "checkbox";
                     return cb; 
                 },
                 function(data) { return data.summary; },
                 function(data) { return data.details; }
                 ];

function updateTable(results) {
  //alert(results);
  DWRUtil.removeAllRows("trafficTable");
  DWRUtil.addRows("trafficTable", results, cellFuncs);
}


</script>

</head>
<body>

<form name="trafficForm">
<table>
  <tr>
    <td>zip</td>
    <td><input type="text" name="zip" onkeyup="criteriaChanged();"/> </td>
  </tr>
  <tr>
    <td>zoom</td>
    <td><input type="text" name="zoom" onkeyup="criteriaChanged();"/> </td>
  </tr>
  <tr>
    <td>severity</td>
    <td><input type="text" name="severity" onkeyup="criteriaChanged();"/> </td>
  </tr>
  
</table>
</form>

<table width="100%" border="1">
  <thead>
    <tr>
      <td>S</td>
      <td width="100">Summary</td>
      <td>Details</td>
    </tr>
  </thead>
  <tbody id="trafficTable">
  </tbody>
</table>
</body>
</html> 
 
 
 

 

DWR요청













DWR 응답

Spring에서 Jaxb (Un)Marshaller 사용하기

스프링에서 XOM(Xml-Object Mapping)을 사용하는 방법이다.
이미 WebService를 사용하기 위해서 JAXB 애너테이션을 사용하고 있는 경우라면
더욱 사용하기가 용이하다.

다음은 User 객체를 XML로 마샬링하는 방법과 XML을 User객체로 언마샬링하는 방법을 보여주는 간단한 예제다.

/spring/context-oxm.xml 예제
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:oxm="http://www.springframework.org/schema/oxm"
    xsi:schemaLocation="
     http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
     http://www.springframework.org/schema/oxm 
     http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">

 <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
  <property name="classesToBeBound">
   <list>
    <value>sample.maven.jaxws.User</value>
   </list>
  </property>
 </bean>
 
</beans>    

아래의 소스는 사용자 정보를 담고 있는 Jaxb 클래스다.
CXF에서 XmlType 애너테이션만 설정하면 WSDL이 생성되는 것과는 달리
Jaxb2Marshaller에서는 XmlRootElement 애너테이션을 반드시 설정해 주어야 한다.

User 클래스
package sample.maven.jaxws;

import java.sql.Timestamp;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlType
@XmlRootElement(name="user", namespace="org.sds.sample")
public class User {
 private long userId;
 private String loginId;
 private String userName;
 private Timestamp createDate;
 public long getUserId() {
  return userId;
 }
 public void setUserId(long userId) {
  this.userId = userId;
 }
 public String getLoginId() {
  return loginId;
 } 
 public void setLoginId(String loginId) {
  this.loginId = loginId;
 }
 public String getUserName() {
  return userName;
 }
 public void setUserName(String userName) {
  this.userName = userName;
 }
 @XmlJavaTypeAdapter(value=DateTimestampAdapter.class)
 public Timestamp getCreateDate() {
  return createDate;
 }
 public void setCreateDate(Timestamp createDate) {
  this.createDate = createDate;
 }
 @Override
 public String toString() {
  return "User [userId=" + userId + ", loginId=" + loginId
    + ", userName=" + userName + ", createDate=" + createDate + "]";
 }
 
 
}


아래는 JUnit테이스 케이스다.
User객체를 XML로 마샬링하는 예제와 XML을 User객체로 언마샬링하는 예제가 포함되어 있다.

Jaxb 테스트 케이스
package sample.maven.jaxws;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;

import javax.annotation.Resource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@ContextConfiguration(locations={"classpath*:/spring/context-*.xml"})
@RunWith(SpringJUnit4ClassRunner.class)

public class UserXomTest {

 @Resource(name="jaxb2Marshaller")
 Marshaller marshaller;
 
 @Resource(name="jaxb2Marshaller")
 Unmarshaller unmarshaller;
 
 @Test
 public void testMarshaller() throws Exception {
  User user = new User();
  user.setLoginId("loginId");
  
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  
  StreamResult result = new StreamResult(out);
  marshaller.marshal(user, result);
  
  System.out.println(out.toString());
  
  ByteArrayInputStream in = 
   new ByteArrayInputStream(out.toByteArray());
  
  StreamSource source = new StreamSource(in);
  User newUser = (User) unmarshaller.unmarshal(source);
  
  System.out.println("new user: " + newUser);
 }
}

2010년 8월 10일 화요일

Eclipse Memory PermSize 오류 (Java1.6.20_b20)

이번 포스트는 배껴 쓴 것입니다.
원문은 http://hanavy.egloos.com/10545584 입니다.





새로운 패치버전이 아니라 공지라서 좀 이상해서 클릭해 보니 결국 Eclipse 라기보다는 어찌 보면
Sun(이젠 Oracle이지)의 문제로 인한 버그였다.

일단 영문 전문은 다음과 같다.


Oracle/Sun VM 1.6.0_21 on Windows

The Eclipse 3.3 - 3.6 launchers for Windows have a problem with the Oracle/Sun Java VM version '1.6.0_21'.

UPDATE: The latest JDK/JRE downloads have fixed this problem, so the simplest way to resolve is to download and re-install from http://www.java.com (alternative link is http://java.sun.com/javase/downloads/index.jsp)


Before the fix was released, there were three choices to work around this:

switch back to '1.6.0_20' (as of July 19, 2010 it can still be downloaded here)
Change the commandline for launching or add the following line after "-vmargs" to your Eclipse.ini file:
-XX:MaxPermSize=256m
(Detailed instructions/examples)
For Helios, download the fixed eclipse_1308.dll and place it into
(eclipse_home)/plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.1.0.v20100503
The bug itself is open for voting and comments as 6969236 on the Java BugParade, and as bug 319514 on the Eclipse side.



요는 Sun 이 Oracle로 넘어가면서 새로 나오게 된 최신 VM인 1.6.0의 21번 패치에서 java/javaw 실행파일의
내부 문자열에서 VM 종류를 확인하기 위한 회사명(vendor flag)을 Sun에서 Oracle로 변경하면서 Eclipse가
새로운 vm을 제대로 인식하지 못한 것이었다.
그래서 확장 옵션인 메모리 Permsize를 추가하는 옵션을 제대로 주지 못하여 고질적인 메모리 부족 현상이
일어난 것이었다.

해결책은 위에 나온 것 처럼 3가지(4가지?)가 있다.
가장 좋은 방법은 일단 번호 나온 위에 업데이트 된 것처럼 자바의 새 패치 버전이 나온 듯 하다.
(하긴 Eclipse가 개발자들 사이에서 가장 많이 쓰이니 빨리 대응해야 했겠지...)

java -version 명령으로 확인할 경우 이전 자바는 다음과 같은 내용이 나타난다.


Java(TM) SE Runtime Environment (build 1.6.0_21-b06)
Java HotSpot(TM) Client VM (build 17.0-b16, mixed mode, sharing)


위의 굵은 글씨로 표시한 부분을 보면 알겠지만 b06 버전이다.
이 버전이 문제가 있으며 똑같이 패치번호는 21번으로 동일하지만 지금 새로 받으면 b07 버전이 된다.
위 공지의 첫번째 내용은 아직 그 전에 내용으로 20번 패치버전으로 받으라는 얘기였다.

두번째는 eclipse가 내부적으로 메모리 세팅을 하지 못하는 상황이므로 강제로 MaxPermSize를 vmargs로 추가해 주는 방법이다.

세번째는 이제부터 이클립스 버전 따라 다를텐데 일단 현재 최신인 Helios 기준으로 변경된 vm의 벤더 인식 알고리즘을 바꾼 dll을 바꿔끼우는 방법이다.
그 아래 줄줄히 옛날 버전의 대응방법도 나와있다.

이 상황을 겪으면서, 다시 한번 프로그램의 유지보수에 대한 내용을 또 고민해 볼 수 있는 기회가 되었다.
정확히 어떠한 문자열인지, 그게 일종의 내부 정보인지 아니면 공식으로 인정되는 부분인지는 확인할 수 없었지만
(앞이면 Eclipse Foundation에서 잘못한거고 뒤면 Oracle에서 잘못 한거겠지..)
의외로 별 거 아닌거 바꾼 걸로 파급효과가 크게 되었다.

실제 나는 별거 아니라고 바꿔서 배포했다가 그걸 참조했던 프로그램들 때문에 고생했던 기억이 몇 번 있는 나로서는
남 얘기(?)같지 않고 연민감도 드는 일이었다..(덕택에 며칠 삽질은 했다만...)

2010년 7월 29일 목요일

로드 타임 위빙

작성중

Load-time weaving, Spring and Maven.

Resource.

http://blog.jayway.com/2009/12/15/load-time-weaving-spring-and-maven/



<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4</version>
<configuration>
<forkMode>once</forkMode>
<argLine>
-javaagent:${settings.localRepository}/org/springframework/spring-agent/${spring.version}/spring-agent-${spring.version}.jar -javaagent:${settings.localRepository}/org/aspectj/aspectjweaver/1.6.1/aspectjweaver-1.6.1.jar
</argLine>
<useSystemClassloader>true</useSystemClassloader>
</configuration>
</plugin>

Note: If you add a line-break between the two javaagents, it doesn't work.

2010년 7월 27일 화요일

Spring에서 Transaction 처리.

트랜잭션 처리는 보통 클래스 및 메서드 레벨의 애너테이션으로 가능하다.
하지만 Exception 발생 여부와 상관 없이 로그를 남겨야 하는 경우라든지 메서드 내부에서 트랜잭션 안에서 실행되어야 하는 부분과 트랜잭션과 무관하게 실행되어야 하는 부분이 함께 존재한다면 애너테이션을 사용하여 처리할 수 없다. (사실은 Propagation 설정으로 가능할 것 같은데 잘 안 된다. 천천히 알아보기로 하구 일단..)


[PROPAGATION]
REQUIRED : 이미 tx가 존재할 경우, 해당 tx에 참여 / tx가 없을 경우, 신규 tx를 생성하고 실행
SUPPORTS : 이미 tx가 존재할 경우, 해당 tx에 참여 / tx가 없을 경우, 그냥 실행
MANDATORY : 이미 tx가 존재할 경우, 해당 tx에 참여 / tx가 없을 경우, Exception 발생
REQUIRES_NEW : 이미 tx가 존재할 경우, 해당 tx를 suspend 시키고 신규 tx를 생성 / tx가 없을 경우, 신규 tx를 생성
NOT_SUPPORTED : 이미 tx가 존재할 경우, 해당 tx를 suspend 시키고 tx 없이 실행 / tx가 없을 경우, 그냥 실행
NEVER : 이미 tx가 존재할 경우, Exception 발생 / tx가 없을 경우, tx 없이 실행
NESTED : 이미 tx가 존재할 경우, 해당 tx에 참여 / tx가 없을 경우, nested tx 실행

[ISOLATION]
DEFAULT : datastore에 의존
READ_UNCOMMITED : Dirty Reads, Non-Repeatable Reads, Phantom Reads 발생
READ_COMMITED : Dirty Reads 방지, Non-Repeatable Reads, Phantom Reads 발생
REPEATABLE_READ : Non-Repeatable Read 방지, Phantom Reads 발생
SERIALIZABLE : Phantom Read 방지



Transaction 처리 방식
  1. TransactionTemplate 사용
  2. TransactionManager 사용

dataSource 및 transactionManager 설정


spring 설정파일에 아래와 같이 설정한다.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://host:3306/databaseName?useUnicode=true&characterEncoding=UTF-8"/>
    <property name="username" value="dbuser"/>
    <property name="password" value="dbpw"/>
    <property name="maxWait" value="28000"/>
    <property name="maxIdle" value="100" />
    <property name="minIdle" value="1" />
    <property name="initialSize" value="10" />
    <property name="timeBetweenEvictionRunsMillis" value="28000" />
    <property name="minEvictableIdleTimeMillis" value="28000000" />
    <property name="validationQuery" value="select 1 from dual"/>
    <property name="testWhileIdle" value="true" />
</bean>


<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>


TransactionTemplate 사용하기


서비스 클래스에 아래와 같이 프로퍼티를 설정한다.

@Resource(name="txManager")
  protected DataSourceTransactionManager txManager;

  protected TransactionTemplate transactionTemplate;
  
  @PostConstruct
  public void init() {
    transactionTemplate = new TransactionTemplate(txManager);
  }

  @Override
  public void myTrxMethod(final PutAccountInfoRequest request)
      throws Exception {
    // write log
    MyTrxResult result = (MyTrxResult)
      transactionTemplate.execute(new TransactionCallback() {
      
      @Override
      public Object doInTransaction(TransactionStatus status) {
        boolean success = false;
        Exception exception = null;
        try {
          // trx1
          // trx2
          
        } catch(Exception ex) {
          exception = ex;
          status.setRollbackOnly();
        } 
        return new MyTrxResult();
      }
    });
    
    // write transaction result

  }


리턴되는 결과가 없는 경우에는 TransactionCallback 대신 TransactionCallbackWithoutResult 을 사용할 수도 있으나, TransactionCallback에서 null을 리턴해도 무방하다.
Transaction은 doInTransaction 메서드 단위로 처리가 되며, exception이 발생하였을 때 setRollbackOnly을 호출하면 메서드 내의 Transaction은 롤백처리된다.

TransactionManager 사용하기


protected TransactionStatus getTransaction() {
    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
    definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    return txManager.getTransaction(definition);
  }


  @Override
  public void myTrxMethod(PutAccountInfoRequest request)
      throws Exception {
    // write log

    boolean success = false;
    Exception exception = null;
    
    TransactionStatus txStatus = getTransaction();
    
    try {
      // trx1
      // trx2
      
      txManager.commit(txStatus);
    } catch(Exception ex) {
      exception = ex;
      txManager.rollback(txStatus);
    }

getTransaction하는 시점부터 commit 또는 rollback 할 때까지의 일련의 처리가 단일 트랜잭션으로 처리된다.

JAXB의 XmlJavaTypeAdapter 애너테이션

JAXB를 이용하여 XML-Binding을 처리할 때 유용한 애너테이션들이 많이 있다.
그 중의 하나인 XmlJavaTypeAdapter를 살펴보기로 한다.

아래의 소스코드를 살펴 보자

package sample.maven.jaxws;

import java.sql.Timestamp;

import javax.xml.bind.annotation.XmlType;

@XmlType
public class User {
  private long userId;
  private String loginId;
  private String userName;
  private Timestamp createDate;
  public long getUserId() {
    return userId;
  }
  public void setUserId(long userId) {
    this.userId = userId;
  }
  public String getLoginId() {
    return loginId;
  } 
  public void setLoginId(String loginId) {
    this.loginId = loginId;
  }
  public String getUserName() {
    return userName;
  }
  public void setUserName(String userName) {
    this.userName = userName;
  }
  public Timestamp getCreateDate() {
    return createDate;
  }
  public void setCreateDate(Timestamp createDate) {
    this.createDate = createDate;
  }
}

대부분의 dateTime 처리와 관련하여 java.util.Date 타입을 처리하면 문제가 될 게 없다.
Jaxb에서도 java.util.Date 처리를 기본적으로 지원하고 있다.
하지만 Anyframe의 QueryService는 java.util.Date로 매핑을 하게 되면 시분초를 싸그리 없애버린다.
java.sql.Date도 아닌데 말이다.
어쨋거나 어쩔 수 없이 또는 이런 저런 이유로 Timestamp를 데이터 타입으로 사용해야 하는 경우가 있다.
디폴트 생성자도 없는 Timestamp를 Jaxb는 기본적으로 처리할 수가 없다.
따라서 Jaxb가 Timestamp를 java.util.Date로 처리할 수 있도록 어뎁터(Adapter)를 구현해 주어야 한다.

Adapter를 지정하지 않은 경우에 CXF가 웹서비스를 초기화하는 과정에서 아래와 같은 에러메시지가 출력된다.

Timestamp를 사용하는 bean
package sample.maven.jaxws;

import java.sql.Timestamp;

import javax.xml.bind.annotation.XmlType;

@XmlType
public class User {
  private long userId;
  private String loginId;
  private String userName;
  private Timestamp createDate;
  public long getUserId() {
    return userId;
  }
  public void setUserId(long userId) {
    this.userId = userId;
  }
  public String getLoginId() {
    return loginId;
  } 
  public void setLoginId(String loginId) {
    this.loginId = loginId;
  }
  public String getUserName() {
    return userName;
  }
  public void setUserName(String userName) {
    this.userName = userName;
  }
  public Timestamp getCreateDate() {
    return createDate;
  }
  public void setCreateDate(Timestamp createDate) {
    this.createDate = createDate;
  }
}


에러 메시지
... 39 more
Caused by: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
java.sql.Timestamp does not have a no-arg default constructor.
this problem is related to the following location:
at java.sql.Timestamp
at public java.sql.Timestamp sample.maven.jaxws.User.getCreateDate()
at sample.maven.jaxws.User
at private sample.maven.jaxws.User sample.maven.jaxws.jaxws_asm.GetUserResponse._return
at sample.maven.jaxws.jaxws_asm.GetUserResponse



XmlAdapter
package sample.maven.jaxws;

import java.sql.Timestamp;
import java.util.Date;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class DateTimestampAdapter extends XmlAdapter {

  @Override
  public Timestamp unmarshal(Date v) throws Exception {
    return new Timestamp(v.getTime());
  }

  @Override
  public Date marshal(Timestamp v) throws Exception {
    return new Date(v.getTime());
  }
}


@XmlJavaTypeAdapter를 적용한 User 빈
package sample.maven.jaxws;

import java.sql.Timestamp;

import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlType
public class User {
  private long userId;
  private String loginId;
  private String userName;
  private Timestamp createDate;
  public long getUserId() {
   return userId;
  }
  public void setUserId(long userId) {
    this.userId = userId;
  }
  public String getLoginId() {
    return loginId;
  } 
  public void setLoginId(String loginId) {
    this.loginId = loginId;
  }
  public String getUserName() {
    return userName;
  }
  public void setUserName(String userName) {
    this.userName = userName;
  }
  @XmlJavaTypeAdapter(value=DateTimestampAdapter.class)
  public Timestamp getCreateDate() {
    return createDate;
  }
  public void setCreateDate(Timestamp createDate) {
    this.createDate = createDate;
  }
}

2010년 7월 21일 수요일

EclipseLink를 이용하여 JPA 어플리케이션 개발

Maven 설정

pom.xml 파일에 아래와 같이 설정한다. 2010년 7월 현재 EclipseLink는 Maven Repository(http://repo2.maven.org/maven2/) 에 등록되지 않았다.
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.0.0</version>
<scope>compile</scope>
...
</dependency>
<dependencies>
...
<repositories>
<repository>
<id>EclipseLink Repo</id>
<url>http://www.eclipse.org/downloads/download.php?r=1&nf=1&file=/rt/eclipselink/maven.repo</url>
</repository>
...
</repositories>

Apache Derby

Apache Derby는 100% Pure Java로 구현된 DBMS다

일반 DBMS 처럼 네트워크로 접속도 가능하고 JDBC를 사용하는 것도 지원한다.
뿐만 아니라 어플리케이션을 배포할 때 Embeded 형태로 데이터베이스를 사용할 수 도 있다.
자바로 개발된 경량 어플리케이션에서 별도의 DBMS가 없이도 데이터 처리를 가능하게 된 것이다.

네트워크 서버
서버 실행
$derby_home/bin/startNetworkServer

서버 종료
$derby_home/bin/stopNetworkServer

ij
데이터베이스에 접속

ij>connect 'jdbc:derby://localhost:1527/mydb;
create=true;traceFile=trace.out;user=user1;password=secret4me';

사용자나 비밀번호가 설정되지 않은 경우에는 아래와 같이 실행할 수 있다.
ij>connect 'jdbc:derby://localhost:1527/sampledb'

데이터 베이스에 접속한 이후에는 'show tables'를 비롯한 ij 명령어를 실행할 수 있다.

Embeded Derby


Caution!!!
Embeded Derby로 개발할 때 주의해야 할 사항이 있다.
Embeded Derby는 데이터 조회를 하기 쉽지 않기 때문에 Eclipse의 Data Source Explorer에서 연결해서 데이터를 조회하는 경우가 있다.
하지만 Data Source Explorer가 연결된 상태에서 Embeded Derby를 테스트 하게 되면 이미 다른 프로세스에서 Derby를 boot 하고 있다는 에러메시지가 출력되고 정상적으로 동작하지 않는다.
Embeded Derby 모드로 개발하고 테스트 할 경우에는 반드시 Data Source Explorer를 disconnect 해야 한다.

startup:
처음 커넥션이 생성될 때 부팅된다.
jdbc:derby:D:/apache/db-derby-10.6.1.0/bin/sampledb;create=true

shutdown:
DriverManager.getConnection("jdbc:derby:;shutdown=true");

2010년 7월 15일 목요일

eclipse 3.6(helios) 실행 시 오류

eclipse 가 ganymede(3.4), galileo(3.5) 등으로 목성의 위성들을 프로덕트 명으로 사용하더니 마침내 태양을 나타내는 Helios로 3.6 버전을 내놓았다.
SUN이 오라클에 인수 된 후라서 더 이상 태양계 행성의 위성 따위로 이름을 지을 필요가 없어 졌는지도 모르겠다.

그나저나 helios를 내려 받아서 압축을 풀고나서 eclipse.exe 파일을 더블클릭 하자 아래와 같은 에러 메시지가 출력되어 상당히 기분이 언짢았다.








개발환경이 별로 특이한 것도 아닌 것 같은데... 암튼 위와 같은 에러 메시자가 출력되면 eclipse.ini 파일을 열어 아래와 같이 수정한다.
-vm 옵션으로 javaw.exe 파일을 지정해 주면 된다.


...

-vm
C:\Java\jdk1.6.0_18\bin\javaw.exe

-vmargs
-Dosgi.requiredJavaVersion=1.5
-Xms40m
-Xmx512m

2010년 7월 12일 월요일

Wrapper 클래스의 getBoolean, getInteger, getLong 메서드

Wrapper 클래스 중에서 Boolean, Integer, Long 클래스에는 다른 래퍼 클래스에 없는 get 메서드가 구현되어 있다.

보통 문자열로 래퍼 클래스를 생성하는 경우에는 각 래퍼 클래스의 스태틱 메서드로 parse 메서드가 구현되어 있다.

  • Boolean.parseBoolean(String value)

  • Byte.parseByte(String value)

  • Short.parseShort(String value)

  • Integer.parserInt(String value)

  • Long.parseLong(String value)

  • ... etc.


parse메서드는 입력되는 문자열 값으로 래퍼 클래스를 생성하는 용도로 사용된다.

그러나 Boolean, Integer, Long 클래스는 parse 메서드와는 별도로 get 메서드가 구현되어 있다.

  • Boolean.getBoolean(String name)

  • Integer.getInteger(String name)

  • Long.getLong(String name)



get메서드는 System.getProperties()에 설정된 값에서 각각 boolean, int, long값을 가져오는 기능을 한다. getBoolean의 경우에 대소문자 구분없이 "TRUE"로 값이 설정되면 true를 리턴하고, 값이 설정되지 않았다거나, "TRUE"이외의 값으로 설정된 경우에는 false를 리턴한다.
getInteger와 getLong의 경우에는 값이 integer 값이나 long 값으로 설정되어 있는 경우에 값을 리턴하고, 값이 설정되지 않았다거나 int 또는 long 타입이 아닌 문자열로 설정된 경우에는 null을 리턴한다.
long값의 경우 10L과 같이 지정되는 경우에도 null을 리턴한다.
실제로 많이 사용될거라 예상되지는 않지만 parse메서드와 혼동하지 않았으면 하는 마음으로 포스트를 등록한다.

아래 예제 코드는 get 메서드 사용 예제다.

@Test
public void testBoolean() throws Exception {
System.out.println(Boolean.getBoolean("true"));
System.out.println(Boolean.getBoolean("True"));
System.out.println(Boolean.getBoolean("TRUE"));
System.out.println(Boolean.getBoolean("tRuE"));

// 이건 새롭네..
// Boolean.getBoolean()은 시스템 프로퍼티에 설정된 값이 대소문자에 상관없이 'true'인 경우에
// true를 리턴한다.
System.setProperty("tempKey", "True");
System.out.println(Boolean.getBoolean("tempKey"));

System.setProperty("sampleIntValue", "19");
System.out.println(Integer.getInteger("sampleIntValue"));

System.setProperty("sampleLongValue", "10");
System.out.println(Long.getLong("sampleLongValue"));

}



false
false
false
false
true
19
10

2010년 7월 11일 일요일

Regular Expression - Java에서 정규표현식 사용

이번 주말에는 가벼운 마음으로 정규 표현식에 대한 책을 읽었다.
늘 사용하는 것이긴 하지만 한번쯤 정리해 둘 필요도 있을 것 같기도 하고
oro(apache project) 를 사용하지 않고, java.util.regex 패키지 내에 포함되어 있는
Matcher, Pattern 만을 사용해서 정규표현식을 어느 정도까지 처리할 수 있는 지도 알아 두고 싶었다.

참고 서적 정보는 아래와 같다.
도서명: 손에 잡히는 정규 표현식
출판사: 인사이트
벤 포터 지음, 김경수 옮김.


개행문자 처리


(.*) 표현식을 사용할 경우 개행문자가 포함되어 있으면 매칭되지 않는다.
그런데 개행문자를 포함하여 매칭하고자 하면 Pattern 옵션에 Pattern.DOTALL 플래그를 지정하면 개행문자를 포함하여 매칭한다.
표현식 앞 부분에 (?s)를 포함시키면 DOTALL 플래그를 설정하는 것과 동일하다.


[javadoc 원문]
In dotall mode, the expression . matches any character, including a line terminator. By default this expression does not match line terminators.
Dotall mode can also be enabled via the embedded flag expression (?s). (The s is a mnemonic for "single-line" mode, which is what this is called in Perl.)

  String ln = System.getProperty("line.separator", "\r\n");
 

  @Test
  public void testCdataSectionRemover() {
    String query = "<![CDATA[ select ACCOUNT_ID, NAME, ALIAS, EMAIL " + ln "from U_SAMPLE_ACCOUNT ]]>";
    System.out.println("query: " + query);

//    Pattern p = Pattern.compile("<!\\[CDATA\\[(.*)\\]\\]>", Pattern.DOTALL);
    Pattern p = Pattern.compile("(?s)<!\\[CDATA\\[(.*)\\]\\]>");

    Matcher matcher = p.matcher(query);

    if (matcher.find()) {
      System.out.println("match");
      System.out.println(matcher.group(1));
    } else {
      System.out.println("not match");
    }
  }


수량자 (Quantifiers)


수량자는 패턴이 입력 텍스트를 병합하는 방법을 나타내는 것으로 '?', '*', '+', {min,max}를 표준 수량자라고 한다. 이러한 수량자를 정규표현식에 넣어서 사용함으로써 더 복잡하고 다양한 match가 가능하다.
Greed: 수량자는 greedy하다. 우리말로는 탐욕스럽다는 의미로서, 최대한 많은 수의 문자를 match시키기 때문이다. 표현식에 '?', '*', '+', {min,max}가 포함되어 있을 경우이다.
Reluctant: 표현식에 물음표(?)를 추가하면 그 패턴을 만족시키는 데 필요한 최소한의 문자 수가 match된다. lazy, 최소매칭, non-greedy, ungreedy라고도 한다.
Possesive: 현재 자바에서만 가능한 것으로 더 진보된 것이다. 정규표현식이 문자열에 적용될 때는 match가 실패할 경우 되돌아 갈 수 있도록 (backtrack) 많은 상태(state)를 생성한다.

package test.regexp;

import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.Test;

public class LazyTest {

  @Test
  public void testGreedy() throws Exception {
    System.out.println();
    System.out.println("testGreedy");
    InputStream in = 
      this.getClass().getResourceAsStream("lazy-input1.txt");
    String input = 
      InputLoader.load(in);
    
    System.out.println("input: ");
    System.out.println(input);
    
    String regex = "<[Bb]>.*</[Bb]>";
    
    Pattern pattern = Pattern.compile(regex);
    
    Matcher matcher = pattern.matcher(input);
    
    
    while(matcher.find()) {
      System.out.println(matcher.group());
    }
    
  }

  @Test
  public void testLazy() throws Exception {
    System.out.println();
    System.out.println("testLazy");
    InputStream in = 
      this.getClass().getResourceAsStream("lazy-input1.txt");
    String input = 
      InputLoader.load(in);
    
    System.out.println("input: ");
    System.out.println(input);
    
    String regex = "<[Bb]>.*?</[Bb]>";
    
    Pattern pattern = Pattern.compile(regex);
    
    Matcher matcher = pattern.matcher(input);
    
    while(matcher.find()) {
      System.out.println(matcher.group());
    }
  }
}



아래는 greedy수량자와 lazy수량자를 사용한 결과를 비교한 것이다.

testGreedy
input: 
This offer is not avaliable to customers
living in <B>AK</B> and <B>HI</B>.

<B>AK</B> and <B>HI</B>

testLazy
input: 
This offer is not avaliable to customers
living in <B>AK</B> and <B>HI</B>.

<B>AK</B>
<B>HI</B>


이와 같은 결과는 별표(*)와 더하기(+) 같은 메타 문자가 탐욕(greedy)스럽기 때문인데, 이는 가능한 한 가장 큰 덩어리를 찾으려 한다는 뜻이다. 이런 메타 문자는 찾으려는 텍스트를 앞에서부터 찾는 게 아니라, 텍스트 마지막에서 시작해 거꾸로 찾는다. 이는 의도적으로 수량자(quantifier)를 탐욕적으로 설계했기 때문이다.

경계지정하기




단어 경계 지정하기

단어 경계를 지정하고자 하는 경우에는 메타문자 \b를 사용한다. \b는 패턴으로 검색된 단어 주변에 공백으로 되어 있는 경우에 검색된다.
아래의 예제는 'cat'이 포함되어 있는 패턴을 검색할 때 단어 경계를 지정하는 예제다.

  @Test
  public void testBoundary() throws Exception {
    String input = "The cat scattered his food all over the room.";
    
    System.out.println("input: " + input);
    System.out.println();
    
    String regex_1 = "cat";
    System.out.println("regex: " + regex_1);
    
    Pattern pattern1 = Pattern.compile(regex_1);
    java.util.regex.Matcher matcher1 = pattern1.matcher(input);
    
    while(matcher1.find()) {
      String group = matcher1.group();
      System.out.println("group: " + group + ", start index: " + matcher1.start());
    }
    
    System.out.println();
    String regex_2 = "\\bcat\\b";
    System.out.println("exgex: " + regex_2);
    Pattern pattern2 = Pattern.compile(regex_2);
    Matcher matcher2 = pattern2.matcher(input);
    while(matcher2.find()) {
      String group = matcher2.group();
      System.out.println("group: " + group + ", start index: " + matcher2.start());
    }
    

    System.out.println();
    String regex_3 = "\\Bcat\\B";
    System.out.println("exgex: " + regex_3);
    Pattern pattern3 = Pattern.compile(regex_3);
    Matcher matcher3 = pattern3.matcher(input);
    while(matcher3.find()) {
      String group = matcher3.group();
      System.out.println("group: " + group + ", start index: " + matcher3.start());
    }
  }


결과는 아래와 같다.
단순희 'cat'으로 패턴을 지정하는 경우는
index가 4인 cat과 index가 9인 scattered의 cat이 모두 검색된다.
그러나 \bcat\b으로 패턴을 지정하는 경우는 단어로 경계를 지정하였기 때문에
index가 4인 cat만 검색이 된다.
\B는 \b와 반대되는 개념으로 \Bcat\B로 지정하였다면 cat 앞뒤에 단어의 경계가 없는 경우다.
따라서 scattered 안에 포함되어 있는 cat만 검색하게 된다.
아래의 결과에 start index를 포함하여 로그를 출력하였으므로 함께 살펴보기 바란다.


input: The cat scattered his food all over the room.

regex: cat
group: cat, start index: 4
group: cat, start index: 9

exgex: \bcat\b
group: cat, start index: 4

exgex: \Bcat\B
group: cat, start index: 9


문자열 경계 정의하기


단어 경계는 단어의 위치(단어의 시작, 단어의 마지막, 단어 전체 등)를 기반으로 위치를 찾는다. 문자열 경계는 단어 경계와 기능은 비슷하지만, 전체 문자열의 시작이나 마지막 부분과 패턴을 일치시키고자 할 때 사용한다. 문자열 경계는 메타 문자 가운데 캐럿(^)으로 문자열의 시작을, 달러 기호($)로 문자열의 마지막을 나타낸다.

아래의 예제 소스는 XML의 시작부에 선언되는 XML 선언자를 검색하는 패턴이다. 두 번째 예제에서는 XML 선언자 앞에 유효하지 않은 문자열이 포함되어 있어 XML이 잘못된 경우이지만 정상적으로 검색된 것처럼 작동한다. 따라서 캐럿을 포함시켜 검색을 해서 XML이 유효하지 않음을 찾을 수 있도록 해야 한다.

  @Test
  public void testStringBoundary() throws Exception {
    String ln = System.getProperty("line.separator");
    String input = "<?xml version='1.0' encoding='utf-8'?>" + ln + "<message>" + ln + "</message>";
    
    System.out.println("INPUT");
    System.out.println(input);
    System.out.println();
    
    String regex_1 = "<\\?xml.*\\?>";
    System.out.println("regex: " + regex_1);
    Pattern pattern1 = Pattern.compile(regex_1);
    Matcher matcher1 = pattern1.matcher(input);
    
    if(matcher1.find()) {
      System.out.println("group: " + matcher1.group());
    }
    System.out.println();
    
    input = "This is bad, real bad!" + ln + input;
    System.out.println("INPUT");
    System.out.println(input);
    System.out.println();
    System.out.println("regex: " + regex_1);
    Matcher matcher2 = pattern1.matcher(input);
    if(matcher2.find()) {
      System.out.println("group: " + matcher2.group());
    }
    
    
    System.out.println();
    System.out.println();
    
    input = "This is bad, real bad!" + ln + input;
    System.out.println("INPUT");
    System.out.println(input);
    System.out.println();
    String regex_3 = "^\\s*<\\?xml.*\\?>";
    System.out.println("regex: " + regex_3);
    Matcher matcher3 = Pattern.compile(regex_3).matcher(input);
    System.out.println("FIND: " + matcher3.find());

  }


INPUT
<?xml version='1.0' encoding='utf-8'?>
<message>
</message>

regex: <\?xml.*\?>
group: <?xml version='1.0' encoding='utf-8'?>

INPUT
This is bad, real bad!
<?xml version='1.0' encoding='utf-8'?>
<message>
</message>

regex: <\?xml.*\?>
group: <?xml version='1.0' encoding='utf-8'?>


INPUT
This is bad, real bad!
This is bad, real bad!
<?xml version='1.0' encoding='utf-8'?>
<message>
</message>

regex: ^\s*<\?xml.*\?>
FIND: false


다중행 모드 사용하기
대개 캐럿(^)은 문자열의 시작과 일치하고, 달러 기호($)는 문자열의 마지막과 일치한다. 예외적으로 두 메타 문자의 동작을 바꾸는 방법이 있다. 많은 정규표현식 구현은 다른 메타 문자의 동작을 변경하는 특수한 메타 문자를 지원하는데, 그 중 하나가 (?m)으로 다중행(multiline)을 지원한다. 다중행 모드로 변경하면 강제로 정규 표현식 엔진이 줄바꿈 문자를 문자열 구분자로 인식한다. 캐럿(^)은 문자열 시작이나 줄바꿈 다음(새로운 행)에 나오는 문자열의 시작과 일치하고, 달러 기호($)는 문자열의 마지막이나 줄바꿈 다음에 나오는 문자열의 마지막과 일치한다. (?m)은 항상 패턴 제일 앞에 두어야 한다.

java.util.regex.Pattern에서 다중행 모드를 사용하는 방법은 두 가지가 있다. 첫 번째 방법은 Pattern.compile(regex, flags)의 두번재 인자인 flags에 Pattern.MULTILINE을 지정하는 방식이고, 두번 째 방법은 패턴에 (?m)을 사용하는 것이다.

방식1.
String regex1 = "^\\s*//.*$";
Pattern pattern1 = Pattern.compile(regex1, Pattern.MULTILINE);


방식2.
String regex2 = "(?m)^\\s*//.*$";

아래의 예제 소스는 자바스크립트 소스에서 인라인 주석(//)을 검색하는 패턴이다.
위의 두가지 방식으로 검색하는 예제가 모두 포함되어 있다.

  @Test 
  public void testMultiLine() throws Exception {
    String input = "function doSpellCheck(form, field) {" + ln +
        "  // Make sure not empty" + ln +
        "  if(field.value == '') {" + ln +
        "    return false;" + ln +
        "  }" + ln +
        "  // Init" + ln +
        "}" + ln;
    
    System.out.println("INPUT");
    System.out.println(input);
    
    String regex1 = "^\\s*//.*$";
    Pattern pattern1 = Pattern.compile(regex1, Pattern.MULTILINE);
    Matcher matcher1 = pattern1.matcher(input);
    System.out.println("regex: " + regex1);
    
    System.out.println("== RESULT ==");
    while(matcher1.find()) {
      System.out.println("find: '" + matcher1.group() + "'");
    }

    System.out.println();
    System.out.println();
    
    String regex2 = "(?m)^\\s*//.*$";
    System.out.println("regex: " + regex2);
    Pattern pattern2 = Pattern.compile(regex2);
    Matcher matcher2 = pattern2.matcher(input);
    while(matcher2.find()) {
      System.out.println("find: '" + matcher2.group() + "'");
    }
  }


두 가지 방식으로 검색을 하더라도 결과가 동일함을 확인할 수 있다.
INPUT
function doSpellCheck(form, field) {
  // Make sure not empty
  if(field.value == '') {
    return false;
  }
  // Init
}

regex: ^\s*//.*$
== RESULT ==
find: '  // Make sure not empty'
find: '  // Init'


regex: (?m)^\s*//.*$
find: '  // Make sure not empty'
find: '  // Init'


하위 표현식 Sub expression


하위 표현식은 ()를 사용하여 표현한다.
각각의 하위 표현식은 group으로 표현된다. group(0)은 검색된 전체 문자열이 리턴되고, 1부터 groupCount까지는 각각 하위표현식에 의해서 검색된 문자열이 리턴된다.
아래의 예제는 아이피의 각 바이트 값을 읽어 내는 예제 소스다. 정규표현식이 그다지 정교하다고 할 수는 없지만, 하위표현식으로 group의 값을 읽는 예제로 작성되었음을 이해하고 소스코드를 살펴보기 바란다.
하위 표현식으로 검색할 때 group()과 group(0)이 동일한 값을 리턴한다.

  @Test
  public void testSubExpression() throws Exception {
    String input = "Pinging hog.forta.com [12.159.46.200]" + ln + "with 32 bytes of data:";
    
    String regex = "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})";
    
    System.out.println("INPUT");
    System.out.println(input);
    System.out.println();

    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(input);
    
    if(matcher.find()) {
      int groupCount = matcher.groupCount();
      System.out.println("group count: " + groupCount);
      System.out.println("group: " + matcher.group());
      System.out.println("group[0]: " + matcher.group(0));
      for(int i = 1; i <= groupCount; i++) {
        System.out.println(String.format("group[%1$d]: %2$s", i, matcher.group(i)));
      }
    }
  }


출력 결과
INPUT
Pinging hog.forta.com [12.159.46.200]
with 32 bytes of data:

group count: 4
group: 12.159.46.200
group[0]: 12.159.46.200
group[1]: 12
group[2]: 159
group[3]: 46
group[4]: 200

2010년 7월 8일 목요일

mysql 빵부스러기

drop all tables;
아래의 쿼리 실행 후 drop table 뒤에 쫙 붙혀 넣기.

select GROUP_CONCAT(A.T_NAME) from (
 select concat(T.TABLE_SCHEMA, '.', T.TABLE_NAME) T_NAME from information_schema.TABLES T where table_name like 'T_%' and TABLE_SCHEMA = 'test'
) A

group_concat
select 된 결과를 ', ' 붙혀서 단일 스트링으로 만들어 준다.

select group_concat(hobbies separator ', ')
from peoples_hobbies where person_id = 5 group by 'all';



mysql reference:
http://dev.mysql.com/doc/refman/5.0/en/alter-table.html


dump, import
-- MySQL 덤프
$ mysqldump DB 테이블 -u계정 -p -q -d --default-character-set=euckr > 저장파일 위치/파일명


-- MySQL 임포트
$ mysql -u계정 -p DB < 저장파일 위치/파일명


날짜별 통계



SELECT
ERROR_CODE,
DATE_FORMAT( CREATE_DATE, '%m-%d' ),
COUNT(1)
FROM
TBL_NAME
WHERE
ERROR_CODE='E000'
GROUP BY
ERROR_CODE, DATE_FORMAT( CREATE_DATE, '%m-%d' )

Table 복제

테이블을 기존의 테이블과 동일한 구조로 생성 한 후
기존 테이블의 레코드를 신규 테이블에 저장한다.

CREATE TABLE TSH_OPTION_TMP LIKE TSH_OPTION

INSERT INTO TSH_OPTION_TMP SELECT * FROM TSH_OPTION


날짜 포맷
%M  Month name (January..December)  
%W  Weekday name (Sunday..Saturday)  
%D  Day of the month with English suffix (1st, 2nd, 3rd, etc.)  
%Y  Year, numeric, 4 digits  
%y  Year, numeric, 2 digits  
%X  Year for the week where Sunday is the first day of the week, numeric, 4 digits, used with \'%V\'  
%x  Year for the week, where Monday is the first day of the week, numeric, 4 digits, used with \'%v\'  
%a  Abbreviated weekday name (Sun..Sat)  
%d  Day of the month, numeric (00..31)  
%e  Day of the month, numeric (0..31)  
%m  Month, numeric (01..12)  
%c  Month, numeric (1..12)  
%b  Abbreviated month name (Jan..Dec)  
%j  Day of year (001..366)  
%H  Hour (00..23)  
%k  Hour (0..23)  
%h  Hour (01..12)  
%I  Hour (01..12)  
%l  Hour (1..12)  
%i  Minutes, numeric (00..59)  
%r  Time, 12-hour (hh:mm:ss [AP]M)  
%T  Time, 24-hour (hh:mm:ss)  
%S  Seconds (00..59)  
%s  Seconds (00..59)  
%p  AM or PM  
%w  Day of the week (0=Sunday..6=Saturday)  
%U  Week (0..53), where Sunday is the first day of the week  
%u  Week (0..53), where Monday is the first day of the week  
%V  Week (1..53), where Sunday is the first day of the week. Used with \'%X\'  
%v  Week (1..53), where Monday is the first day of the week. Used with \'%x\'  
%%  A literal `%\'.

인덱스 추가


ALTER TABLE t2 ADD INDEX (d), ADD UNIQUE (a);


date time functions



str_to_date



mysql> SELECT STR_TO_DATE('01,5,2013','%d,%m,%Y');
-> '2013-05-01'
mysql> SELECT STR_TO_DATE('May 1, 2013','%M %d,%Y');
-> '2013-05-01'


resource : http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_str-to-date

DATE_ADD, DATE_SUB

select * from TBL_NAME
WHERE CREATE_DATE >= DATE_SUB(SYSDATE(), INTERVAL 360 MINUTE)

resources: http://www.w3schools.com/sql/func_date_sub.asp


LIMIT


SELECT 실행 시 페이징 처리를 위한 LIMIT
쿼리문 뒤에 limit만 살짝 붙혀주기.

LIMIT offset, size
offset은 0부터 시작함

대소문자 구분

mysql에서는 컬럼 값의 대소문자 구분을 하지 않는다. 웃겼다.. 한참 웃었어.
SELECT 할 때도 대소문자 구분없이 처리가 되지만, PK나 Unique Constraints에 걸려 있는 컬럼들도 대소문자가 다른 값으로 Insert 되지 않는다.
컬럼 속성을
`EMAIL` VARCHAR(50) NOT NULL 이라고 선언했을 경우에
`EMAIL` VARCHAR(50) BINARY NOT NULL 또는
`EMAIL` VARCHAR(50) NOT NULL COLLATE 'utf8_bin' 로 변경해 주면 된다.
위와 같이 컬럼 속성을 변경하면 대소문자 구분된다.

2010년 7월 6일 화요일

Enum으로 코드 정의하기.

Enum으로 코드를 만들어서 관리하는 예제다.



public interface ICode {
public String getValue();
}





public class Codes {
public static boolean isValid(Class clazz, String value) {
if(ICode.class.isAssignableFrom(clazz) == false) {
return false;
}
if(value == null) {
return false;
}

T[] constants = clazz.getEnumConstants();

for(T t : constants) {
if(value.equals(((ICode) t).getValue())) {
return true;
}
}
return false;
}
public static enum ProcessStatus implements ICode {
RESERVED("R"),
ERROR("E"),
COMPLETED("C");

private final String value;
private ProcessStatus(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

public static enum HistoryMode implements ICode {
UPDATE("U"),
REMOVE("R");

private final String value;

private HistoryMode(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
}

2010년 7월 1일 목요일

CXF에서 WS-Security 사용하기.

Resources:

http://www.jroller.com/gmazza/entry/cxf_x509_profile

http://www.jroller.com/gmazza/entry/cxf_usernametoken_profile#UTCXF2

원문에서는 암호화 라이브러리인 bounce-castle을 설치하라고 설명하지만 예제에서 사용하는 암호화 앨거리듬(algorithm)을 사용하는 경우에는 굳이 설치가 필요하지 않다.

keystore에 key-pair 생성



keytool -genkey -alias myservicekey -keyalg RSA -sigalg SHA1withRSA -keypass skpass -storepass sspass -keystore serviceKeystore.jks -dname "cn=localhost"

keytool -genkey -alias myclientkey -keyalg RSA -sigalg SHA1withRSA -keypass ckpass -storepass cspass -keystore clientKeystore.jks -dname "cn=clientuser"


인증서 생성 후 keystore에 등록




keytool -export -rfc -keystore clientKeystore.jks -storepass cspass -alias myclientkey -file MyClient.cer

keytool -import -trustcacerts -keystore serviceKeystore.jks -storepass sspass -alias myclientkey -file MyClient.cer -noprompt


keytool -export -rfc -keystore serviceKeystore.jks -storepass sspass -alias myservicekey -file MyService.cer

keytool -import -trustcacerts -keystore clientKeystore.jks -storepass cspass -alias myservicekey -file MyService.cer -noprompt


인증서가 각각 서로의 종단 키스토어 파일에 등록되었기 때문에 인증서 파일은 삭제해도 상관없다. 키스토어 파일을 cxf에서 인식할 수 있도록 클래스 패스에 복사하도록 한다.
예를 들어 서비스 프로바이더 측에서는 serviceKeystore.jks 파일을 java/main/resources 에 복사한다.

서버측 웹서비스 개발 및 속성


스프링 설정

src/main/resources/spring/spring-config.xml


<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">

<!-- component scan -->
<context:component-scan base-package="sample.cxf" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>




src/main/resources/spring/cxf-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:cxf="http://cxf.apache.org/core"
xmlns:jaxrs="http://cxf.apache.org/jaxrs" 
xmlns:jaxws="http://cxf.apache.org/jaxws"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
">


<!-- Load CXF modules from cxf.jar -->
<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-extension-jaxrs-binding.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

<!-- Enable message logging using the CXF logging feature -->
<cxf:bus>
<cxf:features>
<cxf:logging />
</cxf:features>
</cxf:bus>

<bean id="myPasswordCallback"
class="sample.cxf.webservice.server.ServerPasswordCallback"/>

<jaxws:endpoint id="userWss4jWebService"
implementor="#UserWebService"
address="/wss4jUserWebService">
<jaxws:outInterceptors>
<ref bean="TimestampSignEncrypt_Response"/>
</jaxws:outInterceptors>
<jaxws:inInterceptors>
<ref bean="TimestampSignEncrypt_Request"/>
</jaxws:inInterceptors>
</jaxws:endpoint>


<jaxws:endpoint id="userImageWebService"
implementor="#UserImageWebService"
address="/userImageWebService">
<jaxws:properties>
<entry key="mtom-enabled" value="true"/>
</jaxws:properties>
<jaxws:outInterceptors>
<ref bean="TimestampSignEncrypt_Response"/>
</jaxws:outInterceptors>
<jaxws:inInterceptors>
<ref bean="TimestampSignEncrypt_Request"/>
</jaxws:inInterceptors>
</jaxws:endpoint>

<bean id="TimestampSignEncrypt_Request"
class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"/>
<entry key="signaturePropFile" value="serviceKeystore.properties"/>
<entry key="decryptionPropFile" value="serviceKeystore.properties"/>
<entry key="passwordCallbackClass" value="sample.cxf.webservice.server.ServiceKeystorePasswordCallback"/>

</map>
</constructor-arg>
</bean>

<bean id="TimestampSignEncrypt_Response"
class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"/>
<entry key="user" value="myservicekey"/>
<entry key="signaturePropFile" value="serviceKeystore.properties"/>
<entry key="encryptionPropFile" value="serviceKeystore.properties"/>
<entry key="encryptionUser" value="useReqSigCert"/>
<entry key="passwordCallbackClass" value="sample.cxf.webservice.server.ServiceKeystorePasswordCallback"/>
<entry key="signatureParts" value="{Element}{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Timestamp;{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionParts" value="{Element}{http://www.w3.org/2000/09/xmldsig#}Signature;{Content}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionSymAlgorithm" value="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
</map>
</constructor-arg>
</bean>

<jaxws:endpoint id="userWebService" 
implementor="#UserWebService"
wsdlLocation="WEB-INF/wsdl/simpleUserWebService-usernametoken.wsdl"
address="/userWebService">

<jaxws:inInterceptors>
<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<entry key="action" value="UsernameToken"/>
<entry key="passwordType" value="PasswordText"/>
<entry key="passwordCallbackRef">
<ref bean="myPasswordCallback"/>
</entry>
</map>
</constructor-arg>
</bean>
</jaxws:inInterceptors>
</jaxws:endpoint>

</beans>




src/main/resources/serviceKeystore.properties


org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=sspass
org.apache.ws.security.crypto.merlin.keystore.alias=myservicekey
org.apache.ws.security.crypto.merlin.file=serviceKeystore.jks



serviceKeystore.jsk 파일이 src/main/resources 폴더에 저장되어 있어야 한다.

sample.cxf.webservice.server.ServiceKeystorePasswordCallback

package sample.cxf.webservice.server;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ServerPasswordCallback implements CallbackHandler {

@Override
public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

if("joe".equals(pc.getIdentifier())) {
if(! pc.getPassword().equals("joespassword")) {
throw new IOException("Username/Password failure. - invalid password");
}
} else {
throw new IOException("Username/Password failure. - invalid username");
}
}
}



위 파일을 제외한 나머지 부분들은 일반 웹서비스 개발과 동일하다.
웹 컨텍스트 패스는 cxf-sample-project다. 클라이언트에서 서비스 정보를 설정할 때 컨텍스트 패스를 설정하므로 기억해 두어야 한다.

클라이언트 개발 및 설정


클라이언트 프로그램은 일반적으로 WSDL을 이용하여 CXF툴로 자바 소스를 생성한다. 일반적으로 생성되는 자바 소스의 패키지 명은 WSDL의 targetNamespace로 생성된다. 위의 경우에는 com.mycompany.sample.cxf.{subpackage}.. 로 생성된다.

src/main/resources/spring/spring-config.xml

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">

<!-- component scan -->
<context:component-scan base-package="com.mycompany.sample.cxf" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>





src/main/resources/spring/cxf-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:cxf="http://cxf.apache.org/core"
xmlns:jaxrs="http://cxf.apache.org/jaxrs" 
xmlns:jaxws="http://cxf.apache.org/jaxws"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
">


<!-- Load CXF modules from cxf.jar -->
<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-extension-jaxrs-binding.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

<!-- Enable message logging using the CXF logging feature -->
<cxf:bus>
<cxf:features>
<cxf:logging />
</cxf:features>
</cxf:bus>

<bean id="userClient" class="com.mycompany.sample.cxf.webservice.user.UserWebService"
factory-bean="clientFactory" factory-method="create">
</bean>

<bean id="clientFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
<property name="serviceClass" value="com.mycompany.sample.cxf.webservice.user.UserWebService"></property>
<property name="address" value="http://localhost:8080/cxf-sample-project/ws/wss4jUserWebService"/>
<property name="inInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Response"/>
</list>
</property>
<property name="outInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Request"/>
</list>
</property>
</bean>

<bean id="userImageClient" class="com.mycompany.sample.cxf.webservice.userimage.UserImageWebService"
factory-bean="userImageClientFactory" factory-method="create">
</bean>

<bean id="userImageClientFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
<property name="serviceClass" value="com.mycompany.sample.cxf.webservice.userimage.UserImageWebService"></property>
<property name="address" value="http://localhost:8080/cxf-sample-project/ws/userImageWebService"/>
<property name="inInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Response"/>
</list>
</property>
<property name="outInterceptors">
<list>
<ref bean="TimestampSignEncrypt_Request"/>
</list>
</property>
</bean>

<bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor" 
id="TimestampSignEncrypt_Request">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"></entry>
<entry key="user" value="myclientkey"></entry>
<entry key="signaturePropFile" value="clientKeystore.properties"/>
<entry key="encryptionPropFile" value="clientKeystore.properties"></entry>
<entry key="encryptionUser" value="myservicekey"/>
<entry key="passwordCallbackClass" value="com.mycompany.sample.cxf.webservice.user.ClientKeystorePasswordCallback"/>
<entry key="signatureParts" value="{Element}{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Timestamp;{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionParts" value="{Element}{http://www.w3.org/2000/09/xmldsig#}Signature;{Content}{http://schemas.xmlsoap.org/soap/envelope/}Body"/>
<entry key="encryptionSymAlgorithm" value="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
</map>
</constructor-arg>
</bean>

<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"
id="TimestampSignEncrypt_Response">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt"/>
<entry key="signaturePropFile" value="clientKeystore.properties"/>
<entry key="decryptionPropFile" value="clientKeystore.properties"/>
<entry key="passwordCallbackClass" value="com.mycompany.sample.cxf.webservice.user.ClientKeystorePasswordCallback"/>
</map>
</constructor-arg>
</bean>
</beans>



위의 설정 파일에서 서비스 경로를 주의 깊게 봐야 한다. 서버에서 설정한 컨텍스트 경로와 일치하는지 살펴봐야 한다.
클라이언트 서비스는 두 개다. 일반 SOAP 기반 메시지를 사용하는 웹서비스와 MTOM을 이용한 바이너리 파일 전송 웹서비스다.
실제로 WS-Security에 관련된 사항을 위해 클라이언트 코드가 변경되지는 않는다. CXF에서는 WSS4JInInterceptor와 WSS4JOutInterceptor 에 대한 설정만으로 WS-Security가 가능하다.

java/main/resources/clientKeystore.properties

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=cspass
org.apache.ws.security.crypto.merlin.keystore.alias=myclientkey
org.apache.ws.security.crypto.merlin.file=clientKeystore.jks




com.mycompany.sample.cxf.webservice.user.ClientKeystorePasswordCallback

package com.mycompany.sample.cxf.webservice.user;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;
import org.apache.ws.security.handler.WSHandlerConstants;

public class ClientKeystorePasswordCallback implements CallbackHandler {

private Map passwords = new HashMap();

public ClientKeystorePasswordCallback() {
super();

passwords.put("myclientkey", "ckpass");
}

@Override
public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {

for(int i = 0; i < callbacks.length; i++) {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[i];

String pass = passwords.get(pc.getIdentifier());

if(pass != null) {
pc.setPassword(pass);
return;
}
}
}
}
WS-Security 클라이언트 샘플 - 일반 웹서비스
package com.mycompany.sample.cxf.webservice.user;

import javax.annotation.Resource;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.mycompany.sample.cxf.webservice.message.GetUserRequest;
import com.mycompany.sample.cxf.webservice.message.GetUserResponse;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath*:spring/*-config.xml"})
public class UserServiceClientWssClientTest {

@Resource(name="userClient")
protected UserWebService userClient;
@Test
public void testWss4J() throws Exception {
System.out.println("userClient: " + userClient);

Client client = ClientProxy.getClient(userClient);

client.getInInterceptors().add(new LoggingInInterceptor());
client.getOutInterceptors().add(new LoggingOutInterceptor());

GetUserRequest request = new GetUserRequest();
GetUserResponse response = userClient.getUser(request);

String code = response.getMessageStatus().getCode();
String message = response.getMessageStatus().getMessage();

System.out.println("=======> code: " + code + ", message: " + message);
}
}
WS-Security 클라이언트 샘플 - MTOM 웹서비스
package com.mycompany.sample.cxf.webservice.user;

import java.io.File;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.annotation.Resource;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.mycompany.sample.cxf.webservice.message.GetUserRequest;
import com.mycompany.sample.cxf.webservice.message.GetUserResponse;
import com.mycompany.sample.cxf.webservice.message.MessageStatus;
import com.mycompany.sample.cxf.webservice.userimage.UploadUserImageRequest;
import com.mycompany.sample.cxf.webservice.userimage.UploadUserImageResponse;
import com.mycompany.sample.cxf.webservice.userimage.UserImageWebService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath*:spring/*-config.xml"})
public class UserImageServiceClientWssClientTest {

@Resource(name="userImageClient")
protected UserImageWebService userImageClient;
@Test
public void testWss4J() throws Exception {
System.out.println("userImageClient: " + userImageClient);

Client client = ClientProxy.getClient(userImageClient);

client.getEndpoint().put("mtom-enabled", "true");

client.getInInterceptors().add(new LoggingInInterceptor());
client.getOutInterceptors().add(new LoggingOutInterceptor());


UploadUserImageRequest request = new UploadUserImageRequest();
request.setUploadUserId("kim");

DataSource dataSource = new FileDataSource(new File("d:/test3.zip"));
DataHandler image = new DataHandler(dataSource);
request.setUserImage(image);

UploadUserImageResponse response = userImageClient.uploadImage(request);

MessageStatus status = response.getStatus();

System.out.println("status: " + status);
if(status != null) {
String code = status.getCode();
String message = status.getMessage();

System.out.println("=======> code: " + code + ", message: " + message);

}
}
}
위의 소스 코드를 살펴보면 알겠지만 서버 소스와 클라이언트 소스에서는 WS-Security에 관련된 소스가 전혀 없다. CXF spring 설정 파일에서만 웹서비스 또는 웹서비스 클라이언트에 대한 설정만 하면 된다. 설사 WS-Security에 대한 정책이 변경된다 하더라도 소스 코드를 전혀 수정할 필요가 없는 것이다. 이클립스 개발환경에서 서비스 프로바이더 프로젝트(cxf-sample-project)를 톰켓 서버로 뛰운 상태에서 클라이언트 테스트 클래스를 JUnit으로 실행하면 console에 기록되는 메시지를 살펴보면 WS-Security 포맷의 SOAP 메시지로 변경되어 요청된다. WS-Security 요청 메시지 샘플
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"><soap:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1"><xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncKeyId-EBDC91EE354299835912779728815005"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><ds:X509Data>
<ds:X509IssuerSerial>
<ds:X509IssuerName>CN=localhost</ds:X509IssuerName>
<ds:X509SerialNumber>1277784112</ds:X509SerialNumber>
</ds:X509IssuerSerial>
</ds:X509Data></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>CvejtN49IQuTlqAwcYjR7vZ+vZgt21ysxsdwez35Iqb3NKGej7sjaoIvaCjsq4xnIooIhaXYcloksiEKijUUxN/EYQZgV2FxLwpO5uYi/9QgUqWrcwZqpjQuyLfDlVSM/18PeJmE0wAGRl1nGaCAAZlhEDB2tRhMJB49/xoh6p4=</xenc:CipherValue></xenc:CipherData><xenc:ReferenceList><xenc:DataReference URI="#EncDataId-4" /><xenc:DataReference URI="#EncDataId-5" /></xenc:ReferenceList></xenc:EncryptedKey><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-4" Type="http://www.w3.org/2001/04/xmlenc#Element"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-EBDC91EE354299835912779728815005" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>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
kyB6VCR2k4E=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-1"><wsu:Created>2010-07-01T08:27:59.078Z</wsu:Created><wsu:Expires>2010-07-01T08:32:59.078Z</wsu:Expires></wsu:Timestamp></wsse:Security></soap:Header><soap:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="id-3"><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-5" Type="http://www.w3.org/2001/04/xmlenc#Content"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-EBDC91EE354299835912779728815005" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>2NJaIKrZg4TEzBySTG3gM1NwXFTHKCVkj/pTTuzKGMoOA3R8baccX+/sp2HxhsdW4zfePCNDADsd
1+gQ3aTaifVmSz8eSH4JvasEgbHYN/u79Xtg0pJI6/ekCQMPQHcL7aZ2un3RQeEAcZU9NzfLIg==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData></soap:Body></soap:Envelope>
WS-Security 응답 메시지 샘플
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"><soap:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1"><xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncKeyId-CD9ECFED812A2A37E512779728885785"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><ds:X509Data>
<ds:X509IssuerSerial>
<ds:X509IssuerName>CN=clientuser</ds:X509IssuerName>
<ds:X509SerialNumber>1277784127</ds:X509SerialNumber>
</ds:X509IssuerSerial>
</ds:X509Data></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>egeNxBQ54X7WmGE9LdLD9FwjQyoMF9fdjIXXlg9b8KKntxBRFtjO3fb51rws0IAybz7E0F//Rd5oqg1tB9KNkSrnKL4No5Vz9X1LTXZPszcT4YL2kh5mK7WZaui8dw/k2aODOXQw0o4CNGN61O3IOHq8HiMXUJ0ItohuBJCdTzA=</xenc:CipherValue></xenc:CipherData><xenc:ReferenceList><xenc:DataReference URI="#EncDataId-4" /><xenc:DataReference URI="#EncDataId-5" /></xenc:ReferenceList></xenc:EncryptedKey><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-4" Type="http://www.w3.org/2001/04/xmlenc#Element"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-CD9ECFED812A2A37E512779728885785" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>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
DEnoFDCIi4Y=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-1"><wsu:Created>2010-07-01T08:28:08.515Z</wsu:Created><wsu:Expires>2010-07-01T08:33:08.515Z</wsu:Expires></wsu:Timestamp></wsse:Security></soap:Header><soap:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="id-3"><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-5" Type="http://www.w3.org/2001/04/xmlenc#Content"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" /><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-CD9ECFED812A2A37E512779728885785" /></wsse:SecurityTokenReference>
</ds:KeyInfo><xenc:CipherData><xenc:CipherValue>SdmwkqGNhsspCdzDsKsUsHAfxr0xRTlYHt6r4n2HKTRvIIj7f8qhht7bNjSmqZTBDPV65irjtSVT
uGRyZERDEbSz5/3weCj6Wuag5y96HqruIRGxts4bexmZLAFZU+OaTjCAk/2t4jAMPHARk2S5/g==</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData></soap:Body></soap:Envelope>