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>

WS-Security 웹서비스 보안

웹서비스 개발을 할 때마다 언제나 보안 문제에 직면하게 된다.
그나마 JAX-RS(RESTful)를 이용하는 경우에는 서비스 소비자(CLIENT)가 서비스 특성을 인정하여 메시지 보안에 대해서는 어느정도는 감안하고 사용하기도 하지만, B2B 업무용 웹서비스를 개발할 경우에는 높은 수준의 보안을 요구한다.
웹서비스에서 사실상 표준으로 자리잡은 WS-Security에 대해서 살펴보고 CXF에서 WS-Security를 이용하는 방법을 살펴보기로 하자.

WS-Security


WS-Security를 이해하기 위해서는 PKI 기반 암호화와 X509(인증서) 기반의 전자서명 개념을 알고 있어야 한다.

메시지 암복호화

비밀키가 노출되지 않는다는 가정하에서 PKI 기반 암복호화는 가장 안전한 암호화 수단이다. 복수의 다수에게 배포된 공개키로 암호화된 메시지는 비밀키로만 복호화가 가능하고, 비밀키로 암호화된 메시지는 공개키로만 복호화가 가능하다. 메시지 발신자는 수신자의 공개키(인증서의 공개키)로 암호화하여 메시지를 전송하면 통신 도중에 제 3자에 의해서 암호화된 메시지가 노출된다고 비밀키가 없기 때문에 메시지 내용을 볼 수가 없다.

전자서명
인증서란 전자서명을 하는 엔티티의 일반정보, 공개키 정보, 이슈어(Issuer) 정보들이 포함되어 있다. 전자서명이란 발신자가 메시지 내용에 대한 메시지 다이제스트를
생성하여 비밀키로 서명을 하면 수신자는 발신자의 인증서(인증서에 포함되어 있는 공개키)를 이용하여 메시지를 검증(verify)한다. 검증이 정상적으로 처리되었다면 아래와 같은 사실을 확신할 수 있다.

  • 메시지 인증 - 메시지 발신자에 대한 인증을 할 수 있다.

  • 무결성(integrity) - 발송된 메시지는 위.변조 되지 않았다.

  • 부인 방지 - 메시지 작성에 대한 부인을 방지할 수 있다.



웹서비스가 one-way 방식이 아닌 request-response 유형일 경우에는 클라이언트와 서비스 프로바이더가 각각 자신의 키페어(key-pair)와 상대방의 인증서를 키스토어에 저장하고 있어야 한다.
서비스 요청시 클라이언트는 발신자가 되고, 서비스 프로바이더는 수신자가 된다. 서비스 응답시에는 반대로 서비스 프로바이더가 발신자가 되고 클라이언트는 수신자가 된다.
서비스 요청시 클라이언트가 WS-Security를 이용하여 메시지를 발신하고자 한다면 서비스 프로바이더의 인증서를 이용하여 메시지를 암호화하고, 자신의 비밀키로 메시지에 대한 서명을 한다. 서비스 요청을 받은 서비스 프로바이더는 클라이언트의 인증서를 이용하여 메시지에 대한 검증을 하고 자신의 비밀키로 메시지를 복호화한다. 메시지 검증과 복호화가 정상적으로 수행되었다면 원래 서비스에서 구현한 로직을 실행한다.
로직 실행 후 결과를 클라이언트에게 리턴하기 위해서 서비스 프로바이더는 클라이언트의 인증서를 이용하여 응답 메시지를 암호화하고, 자신의 비밀키로 메시지를 서명한다. 응답 메시지를 수신한 클라이언트는 서비스 프로바이더의 공개키로 메시지를 검증하고 자신의 비밀키로 메시지를 복호화한다.
각 종단에서 각각 자신의 비밀키와 상대방의 인증서를 이용하여 서명+암호화 및 검증+복호화가 실행되어야 한다.