2008년 2월 1일 금요일
RIFE 시작하기(11) - Further Reading (마지막)
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Further Reading
지금까지 많은 RIFE의 기본적인 기능들을 다루었다. 하지만, RIFE는 이 튜토리얼의 범위를 벗어나 더 많은 기능들을 제공한다. 위키페이지의 Cook Book 은 프레임웍의 추가기능들에 관해 자세한 정보와 많은 예제들을 제공한다.
여기 그중에서도 꼭 알아두어야 할 몇가지에 관해서 소개하겠다.
* Constraints : 빈에 관한 정보를 제공하여 RIFE가 자동으로 HTML폼과 사용자 입력을
확인할 수 있게하며 심지어 데이터베이스 테이블까지 자동으로 만들어 준다.
- Meta data merging : RIFE전용코드가 비지니스객체에 삽입되는것을 피하기
위해 제약사항들을 별도의 이웃클래스에 기술할 수 있다.
- Generic query manager : 빈의 메타데이터에 의해 SQL쿼리문을 생성해낸다.
많은경우 코드에 SQL문장을 전혀 사용하지 않을 수 있다.
* Scheduler : RIFE는 자체적으로 특정작업을 나중에 처리할 수 있는 스케쥴링 작업을 지원한다.
Cook Book 이외에도 배울 수 있는 더 많은 자원이 있다:
* RIFE 메일링 리스트
* RIFE javadoc
* RIFE IRC channel
RIFE 시작하기(10) - Using a database for user data
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Using a database for user data
앞장에서 우리는 인증시에 XML에 저장된 사용자 목록을 사용하였다. 더 많은 사용자 목록을 필요로 한다면 RIFE의 데이터베이스 사용자 지원을 사용할 수 있다. 이것이 메모리 사용자와 어떻게 다른지 간략히 다루겠다.
먼저, 인증요소가 memory.xml 대신 rife/autheticated/database.xml 을 확장하도록 해야한다. 그리고, 어떤 데이터소스를 사용할지 설정파일을 기술하여야 한다. 예제를 살펴보자.
------------------------------------------------------------------
데이터베이스 사용자를 위한 인증요소: elements/auth.xml
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">
<element extends="rife/authenticated/database.xml">
<property name="template_name">auth</property>
<property name="role">admin</property>
<property name="datasource"><datasource>postgresql</datasource></property>
<submission name="credentials">
<param name="login"/>
<param name="password"/>
</submission>
<childtrigger name="authid"/>
</element>
------------------------------------------------------------------
------------------------------------------------------------------
사이트 파일에 authid 와 elements/auth.xml 추가
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<!-- Add authid variable -->
<globalvar name="authid"/>
<!-- Refer to the AUTH element definition -->
<element id="AUTH" file="elements/auth.xml" />
<!-- Indicating that a subsite requires authentication -->
<subsite id="ADMIN" file="admin.xml" urlprefix="/admin" inherits="AUTH"/>
...
</site>
------------------------------------------------------------------
이제 DatabaseUserFactory와 DatabaseSessionsFactory로 DatabaseUsers와 DatabaseSessions 객체를 생성해야 한다. RIFE는 많은 데이터베이스들을 지원한다. (Derby, Firebird, H2, HSQLDB, MMcKoiSQL, MySQL, One$DB/DaffodilDB, Oracle, PostgreSQL등) 이것들을 사용하기 위해 어떠한 코딩작업도 필요없다. 사용자와 세션객체의 install메소드만 호출하면 된다. 그후로 인증은 메모리사용자와 동일하게 작동한다.
------------------------------------------------------------------
데이터베이스 사용자를 위한 테이블설치
------------------------------------------------------------------
Datasource source = Datasources.getRepInstance().
getDatasource(Config.getRepInstance().getString("DATASOURCE"));
DatabaseUsersFactory.getInstance(source).install();
DatabaseSessionsFactory.getInstance(source).install();
------------------------------------------------------------------
아래 예제 참여자는 초기에 테이블을 추가하고 기본 사용자인 admin 을 생성한다.
------------------------------------------------------------------
package my.participants;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.uwyn.rife.authentication.credentials.RoleUser;
import com.uwyn.rife.authentication.credentialsmanagers.DatabaseUsers;
import com.uwyn.rife.authentication.credentialsmanagers.DatabaseUsersFactory;
import com.uwyn.rife.authentication.credentialsmanagers.RoleUserAttributes;
import com.uwyn.rife.authentication.sessionmanagers.DatabaseSessionsFactory;
import com.uwyn.rife.database.Datasource;
import com.uwyn.rife.rep.BlockingParticipant;
public class ParticipantDatabaseAuthentication extends
BlockingParticipant {
protected void initialize() {
String dsName = getParameter();
if( null == dsName || 0 == dsName.length() ) {
dsName="datasource";
}
Datasource mDatasource = (Datasource)getRepository().getProperties().getValue(dsName);
boolean hasUsers = true;
DatabaseUsers users = null;
try {
users = DatabaseUsersFactory.getInstance(mDatasource);
users.install();
hasUsers = false;
} catch (Exception e) {
Logger.getLogger(this.getClass().getName()).log(Level.INFO,
"The Database Authentication tables could not be installed -- they probably already exist.", e);
}
// If we were able to create a users table, then
// there aren't going to be any users in it.
if( !hasUsers ) {
try {
// Create an 'admin' role
String defaultRoleName = "admin";
users.addRole(defaultRoleName);
// Create a user named 'admin', with password 'password', and role 'admin'
RoleUser user = new RoleUser();
user.setLogin("admin");
user.setPassword("password");
RoleUserAttributes userAttrs = new RoleUserAttributes();
userAttrs.setPassword(user.getPassword());
userAttrs.setRoles(new String[] {defaultRoleName} );
users.addUser(user.getLogin(), userAttrs);
} catch (Exception e) {
Logger.getLogger(this.getClass().getName()).log(Level.WARNING,
"Unable to create a default user. This will need to be done manually.", e);
}
}
try {
DatabaseSessionsFactory.getInstance(mDatasource).install();
} catch (Exception e) {
Logger.getLogger(this.getClass().getName()).log(Level.INFO,
"The Database Sessions tables could not be installed -- they probably already exist.", e);
}
}
}
------------------------------------------------------------------
------------------------------------------------------------------
참여자 정의 : rep/participants.xml
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">
<rep>
...
<participant param="rep/datasources.xml">ParticipantDatasources</participant>
<property name="datasource"><datasource>mysql</datasource></property>
<participant blocking="true" param="datasource">my.participants.ParticipantDatabaseAuthentication</participant>
...
</rep>
------------------------------------------------------------------
메모리와 데이터베이스 백엔드 복합
어떤경우, 사용자는 데이터베이스에 저장하더로도 세션정보는 저장하지 않길 원할 수도 있다, 이를위해 혼합(mixed) 인증요소가 있다: rife/autheticated/mixed.xml. 이것은 세션데이터가 메모리에 저장되므로 데이터베이스 세션이 필요하지 않다는것만 제외하면 데이터베이스 인증요소와 같은 방법으로 이용한다.
자동 세션 폐기
기본적으로 memory, database, mixed 인증은 이것이 만료(outdated)되었을때 자동으로 세션정보를 폐기한다.
------------------------------------------------------------------
<element extends="rife/authenticated/database.xml">
------------------------------------------------------------------
사용자가 최대 대기가능한 세션타임은 SESSION_DURATION 옵셕으로 설정된다. 기본값은 20분이다. 이값은 milliseconds 단위로 적용된다.
그외, 폐기주기도 SESSION_PURGE_FREQUENCEY와 SESSION_PURGE_SCALE에 값을 기술해 수정할 수 있다. 기본적으로 각 20과 1000을 권장한다. 이것은 폐기가 인승세션에 50번 접근될 때마다 폐기를 한다는걸 의미한다.
사용자 생성
데이터베이스에 사용자를 추가하려면 DatabaseUsers클래스를 사용해야 한다. 사용자는 로그인명과, 비밀번호 그리고, 최소한 하나이상의 역할(role)을 필요로 한다. RoleUser와 RoleUserAttribute 클래스가 이값들을 유지한다. 만약, 가입폼이 있고 사용자의 로그인명과 비밀번호를 입력받는다면, submission 처리자(handler)는 아래와 같을 것이다.
------------------------------------------------------------------
/* Fetch the login and password from the form. */
RoleUser user = getSubmissionBean(RoleUser.class);
/* If the login and password are okay, create the user. */
if (user.validate()) {
/* Fetch the data source for your database in whatever way you like. */
Datasource ds = (Datasource) getProperty("datasource");
/* And use it to create a user-management helper object. */
DatabaseUsers users = DatabaseUsersFactory.getInstance(ds);
try {
/* The user should have a password and a single role of "user".
We could also use the RoleUserAttributes() constructor to avoid
these "set" calls. */
RoleUserAttributes attrs = new RoleUserAttributes();
attrs.setPassword(user.getPassword());
attrs.setRoles(new String[] { "user" });
/* Add the user to the database. */
users.addUser(user.getLogin(), attrs);
/* Send the user to a "thanks for signing up" page (this assumes
an exit called "success" is defined for the registration page.) */
exit("success");
} catch (CredentialsManagerException e) {
/* Handle the error; in this example we just log the exception
and fall through to redisplaying the form. */
log.severe(ExceptionUtils.getExceptionStackTrace(e));
}
}
/* Redisplay the signup form with errors. */
generateForm(template, user);
print(template);
------------------------------------------------------------------
그밖의 사용자정보 관리
물론, 대부분의 사이트는 로그인시 사용자의 로그인명과 비밀번호만 필요로 한다. RIFE의 인증관리는 사용자 테이블의 내용을 관리하며 부가적인 사용자 정보는 분리되어 관리된다. (사용자 모델을 변경하여 그밖의 정보도 포함하려면 RIFE의 인증관리자 클래스를 대체할 클래스를 생성해서 사용 하여도 된다.)
아래 예제에서는 "Generic query manager" 를 사용해 사용자 데이터에 접근할것이다.
Account 라는 클래스에 부가정보를 넣기로 했다. (하지만, 이것은 정해진것이 아니므로 어떤 이름을 사용해도 관계 없다.) 예를들어, 각 사용자를 위해 e-mail주소를 추가할 것이다.
------------------------------------------------------------------
Account.java
------------------------------------------------------------------
public class Account {
/* Account ID. */
private int id;
/* RIFE-managed user ID. */
private long rifeUserId;
/* The user's E-mail address. */
private String email;
... getters and setters for the above ...
}
------------------------------------------------------------------
RIFE에서 제공된 사용자의 ID외에 별개의 유일키(ID)가 저장되는 부분을 주목하자. 기본적으로 이속성은 one-to-one매핑이다. 하지만, RIFE의 퍼시스턴스 레이어는 현재 long타입에 대해 identifier를 지원하지 않는다.
이 클래스는 메타데이터를 갖는다. 메터데이터 결합 메커니즘은 Account클래스를 깨끗하게 유지할 수 있게 한다.
------------------------------------------------------------------
AccountMetaData.java
------------------------------------------------------------------
public class AccountMetaData extends MetaData {
public void activateMetaData() {
String userTable = RifeConfig.Authentication.getTableUser();
addConstraint(new ConstrainedProperty("id")
.identifier(true)
.editable(false));
addConstraint(new ConstrainedProperty("rifeUserId")
.notNull(true)
.manyToOne(userTable, "userid"));
addConstraint(new ConstrainedProperty("email")
.notNull(true)
.email(true)
.maxLength(100));
}
}
------------------------------------------------------------------
사용자 생성
가입폼에 email 필드를 추가하자. RIFE는 폼의 필드에 기반해 Account와 RoleUser객체를 인스턴스화 한다. 가입폼의 요소(elemet)정의는 아래와 같다.
------------------------------------------------------------------
가입폼 요소 정의
------------------------------------------------------------------
<element id="Register" implementation="com.foobar.elements.Register" url="register">
<submission name="register">
<bean classname="com.foobar.Account" />
<bean classname="com.uwyn.rife.authentication.credentials.RoleUser" />
</submission>
</element>
------------------------------------------------------------------
그리고, 가입을 위한 submission 핸들러는 다음과 같다.
------------------------------------------------------------------
사용자 계정 생성
------------------------------------------------------------------
public void doRegister() {
RoleUser user = getSubmissionBean(RoleUser.class);
/* Fetch the email address from the form. */
Account account = getSubmissionBean(Account.class);
/* If the login, password, and email are okay, create the user. */
if (user.validate() && ((Validated)account).validate()) {
Datasource ds = (Datasource) getProperty("datasource");
DatabaseUsers users = DatabaseUsersFactory.getInstance(ds);
/* We will also need to store the account, so get a query manager for it. */
ContentQueryManager<Account> accountManager =
new ContentQueryManager<Account>(ds, Account.class);
try {
/* Create the user as in the previous example (but more tersely). */
users.addUser(user.getLogin(), new RoleUserAttributes(user.getPassword(),
new String[] { "user" }));
/* The user is created. Now we can fetch its ID and remember it in the
Account object. */
account.setRifeUserId(users.getUserId(user.getLogin()));
/* Store the new account in the database. */
accountManager.save(account);
exit("success");
} catch (CredentialsManagerException e) {
/* Handle the error; in this example we just log the exception
and fall through to redisplaying the form. */
log.severe(ExceptionUtils.getExceptionStackTrace(e));
}
}
/* Redisplay the signup form with errors. */
generateForm(template, user);
generateForm(template, account);
print(template);
}
------------------------------------------------------------------
대부분의 경우 RIFE 사용자와 Account객체를 한 트랜잭션으로 처리하고 싶을 것이다. 그렇지 않으면 사용자 생성에 실패할 경우 Account가 생성되지 않을 수도 있다. 자세한 정보는 cookbook의 Chainable transactions 장을 참고하라.
부가정보에 접근하기
이제 사용자를 위해 Account를 추가했다. 이제 요소에서 이것을 읽을 수 있어야 한다. 요소가 인증이나 identified.xml을 확장했다고 가정할 경우 Generic query manager를 사용해 사용자의 Account객체를 가져올 수 있다.
------------------------------------------------------------------
Account 객체 가져오기
------------------------------------------------------------------
RoleUserIdentity identity;
Account account = null;
/* Since this is an identified or authenticated element, there is a
RoleUserIdentity assigned to the request if the user is logged in. */
identity = (RoleUserIdentity)getRequestAttribute(Identified.IDENTITY_ATTRIBUTE_NAME);
/* Anonymous users can hit identified (but not authentication-required) pages.
Don't try to fetch their account data. */
if (identity != null)
{
GenericQueryManager<Account> manager;
/* Fetch the data source using whatever technique you like. */
Datasource ds = (Datasource) getProperty("datasource");
manager = GenericQueryManagerFactory.getInstance(ds, Account.class);
/* Now we fetch the account using a simple query manager call.
The RoleUserIdentity has a child object which contains the user ID. */
account = mgr.restoreFirst(mgr.getRestoreQuery()
.where("rifeUserId", "=",
identity.getAttributes().getUserId()));
}
------------------------------------------------------------------
Database schema
만약 수동으로 인증시스템 데이터베이스 테이블을 생성한다면 아래의 정의를 참고하라. 정확한 데이터 타입은 데이터베이스 마다 조금씩 차이가 있다.
authentication
이 테이블은 현재 모든 활성화된 세션상태를 가지고 있다. 이것은 "혼합(mixed)" 백엔드를 사용할 경우 사용되지 않는다. 사용자가 로그인 또는 로그아웃시에 삽입또는 삭제된다. 모든 컬럼은 NOT NULL이며 테이블명은 TABLE_AUTHENTICATION 설정 파라미터를 오버라이드하여 변경될 수 있다.
authid varchar(32) Opaque session identifier, as stored in the authid cookie
or request parameter. Primary key.
userid integer (64-bit) User ID.
hostip varchar(40) Remote IP address of the session.
sessstart integer (64-bit) Session start time, in milliseconds since the
epoch (System.currentTimeMillis() format). Indexed
(non-unique) to allow fast deletion of expired sessions.
remembered boolean True if the user should be remembered after the
session expires.
authrole
이 테이블은 인증 역할(role)들을 가진다. 모든컬럼은 NOT NULL이며 TABLE_ROLE 설정 파라미터로 오버라이드 될수 있다.
roleid integer (32-bit) Unique numeric identifier of the role. Primary key.
name varchar(20) Name of the role. Has a unique constraint.
authrolelink
authrole 과 authuser 테이블 간의 링크 테이블; 어떤 사용자가 어떤 role을 가지는지 정의한다. 모든 컬럼은 NOT NULL이며 TABLE_ROLELINK 설정 파라미터로 오버라이드 할 수 있다.
userid integer (64-bit) User ID, foreign key to authuser.userid. First
part of composite primary key.
roleid integer (32-bit) Which role the user has, foreign key to
authrole.roleid with cascading deletes enabled.
Second part of composite primary key.
authuser
사용자ID와 비밀번호를 가지고 있다. 모두 NOT NULL이며 테이블명은 TABLE_USER설정 파라미터를 오버라이드할 수 있다.
userid integer (64-bit) User ID. Primary key.
login varchar(20) User's login name. Has a unique constraint.
passwd varchar(100) User's password, possibly encrypted
(see Password encryption).
RIFE 시작하기(9) - Adding authentication
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Adding authentication
친구 데이터베이스 예제 어플리케이션은 이제 완벽한 웹어플리케이션이 되었다. 하지만, 누구나 데이터베이스에 친구를 추가할 수 있어 그리 유용하지 않다. 웹페이지에 이 프로그램을 올리고 누구나 읽을수 있도록 하되 목록은 우리만 관리할 수 있도록 하고싶다. 이말은 어떤 종류의 비밀번호 보안이나 인증이 반드시 필요하다는 의미이다.
인증(Authentication)
인증처리는 매우 일반적이며 RIFE는 사용자와 암호의 개념을 훌륭히 추상화하였다. 이제부터 보겠지만, 내장된 기능은 사용자의 목록을 XML파일과 나아가 데이터베이스로 관리할 수 있다.
데이테베이스 사용자
대부분의 사이트들은 사용자의 목록을 데이터베이스에 보관하길 원한다. 데이터베이스 사용자에 관해서는 다음장에서 별도로 다룰것이다. 이장의 나머지에는 간단히 메모리(memory)기반의 사용자를 사용한다. 어플리케이션은 사용자정보가 어디로 부터 발생하는지 관심을 두지 않는다. 그러므로, 개발단계에서는 메모리상에 사용자정보를 사용하다가 필요할때 데이터베이스로 변경하면 된다. 어떤 사람들은 설정이나 설치과정을 위해 메모리사용자를 사용하기도 한다. 이런 경우는 대부분 일부 몇안되는 사용자를 위한 기능이며 실행시에 변경되지 않는다.
메모리 사용자.
"메모리 사용자"는 사용자를 XML파일에 저장하기 위한 RIFE의 용어이다. (이름이 의미하듯 사용자정보는 초기화된 후 메모리상에서 유지된다.) 이것은 사용자를 관리하는 가장 쉬운 방법이며 관리자나 방문자(guest)사용자와 같은 고정된 그룹의 사용자들을 관리하는데 적합하다.
이 방법으로, 당신은 간단히 사용자와 비밀번호를 ParticipantMemoryUsers에서 지정된 파일에 쉽게 보관할 수 있다. 참여자를 추가하면서 시작해 보자:
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">
<rep>
<!-- Add users participant -->
<participant param="rep/users.xml">ParticipantMemoryUsers</participant>
<participant param="rep/config.xml">ParticipantConfig</participant>
<participant param="rep/datasources.xml">ParticipantDatasources</participant>
<participant param="sites/friends.xml">ParticipantSite</participant>
</rep>
------------------------------------------------------------------
rep/users.xml은 사용자목록을 정의한 파일이다. 우선 관리자(administrator) 한명만 필요하며 이 사용자는 친구목록을 편집할 권한이 있다.
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE credentials SYSTEM "/dtd/users.dtd">
<credentials>
<user login="admin">
<password>password</password>
<role>admin</role>
</user>
</credentials>
------------------------------------------------------------------
사용자명과 비밀번호(user and password)태그 이외에 role태그가 있다. 여기서는 admin롤을 부여하였다. 역할(role)은 사이트내 특정영역에 사용자에 따라 접근권한을 부여하기 위해 사용된다. role이 어떻게 사용되는지는 이장의 뒷부분에서 다루겠다.
인증을 설정하기 위해 몇단계를 더 거쳐야 하는데, 가장 좋은 방법은 어플레케이션에 기능을 추가/변경해 보는것이다.
사이트 구조 변경하기
관리자만 변경을 할 수 있으므로, 방문자는 아래의 옵션들을 볼 필요가 없다. 메뉴를 서브사이트로 나누는것이 첫번째 작업이다.
서브사이트 사용하기
RIFE의 사이트(site)는 더 작은 서브사이트(subsite)들로 구성될 수 있다. 서브사이트를 이용하면 사이트를 더 모듈화 할 수 있으며 또한 다른 어플리케이션의 일부로 재사용하기 쉬워진다. 이 예제에서, 우리는 관리자부분을 서브사이트로 만들고 거기에 인증메커니즘을 추가할 것이다.
이를위해, ADMIN요소를 고유의 사이트파일(sites/admin.xml)로 옮기고, 메인사이트 파일에 subsite태그로 서브사이트를 가리킬 것이다.
------------------------------------------------------------------
메인 사이트 파일
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<arrival destid="DISPLAY"/>
<element id="DISPLAY" file="display.xml" url="/display">
<flowlink srcexit="admin" destid="ADMIN"/>
</element>
<!-- Add a subsite -->
<subsite id="ADMIN" file="admin.xml" urlprefix="/admin"/>
</site>
------------------------------------------------------------------
이제 /admin 으로 시작하는 위치는 관리자 서브사이트에 의해 처리될 것이다. add, install, remove요소를 여기로 옮긴후 사이트파일은 아래와 같이 될것이다.
------------------------------------------------------------------
ADMIN 요소 서브사이트
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<globalexit name="menu" destid="MENU"/>
<arrival destid="MENU"/>
<element id="MENU" file="admin/menu.xml" url="/menu">
<flowlink srcexit="install" destid="INSTALL"/>
<flowlink srcexit="add" destid="ADD"/>
<flowlink srcexit="remove" destid="REMOVE"/>
<flowlink srcexit="back_to_display" destid=".DISPLAY"/>
</element>
<element id="INSTALL" file="admin/install.xml" url="/install"/>
<element id="ADD" file="admin/add.xml" url="/add"/>
<element id="REMOVE" file="admin/remove.xml" url="/remove"/>
</site>
------------------------------------------------------------------
서브사이트에서 메인사이트를 거꾸로 참조할경우 .DISPLAY로 대상을 성정했다. dot(.)은 구분자로 사용된다. 그러므로, 대상은 메인사이트의 요소를 가르킨다. 맨앞의 dot가 없을경우 링크는 상대적이여서 현재 서브사이트의 요소를 참조한다.
dot(.)구분자는 파일시스템에서 다렉토리명을 구분하기 위한 slash(/)와 비슷하게 사용된다. 예를들어, .ADMIN.REMOVE는 REMOVE와 같은 요소를 가르킨다. 하지만, 이런방법은 유연성을 떨어뜨리므로 권장하지 않는다.
디렉토리와 유사하게, 상위의 서브사이트 요소를 앞의 dot(.)없이 참조하는것도 가능하다. 두개의 dot(..)는 상위 디렉토리를 가르키는것으로 매우 친숙하다. RIFE에서는 ^ 기호를 사용할 수 있다. 이것은 .DISPLAY를 대신해 ^DISPLAY를 사용할 수 있음을 의미한다.
인증 요소
이제 실질적인 인증을 처리할때이다. 이 일을 하는 요소는 매우 쉽게 작성된다. 우리에게 필요한것은 RIFE의 내장된 요소인 rife/authenticated/memory.xml을 확장하는 것이다.
------------------------------------------------------------------
elements/auth.xml 인증요소
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">
<element extends="rife/authenticated/memory.xml">
<property name="template_name">auth.form</property>
<property name="role">admin</property>
<submission name="credentials">
<param name="login"/>
<param name="password"/>
</submission>
<childtrigger name="authid"/>
</element>
------------------------------------------------------------------
내장된 인증요소는 로그인 템플릿의 출력에서 처리를 포함해 모든 인증로직을 처리한다. 여기에 두개의 속성이 있다. 하나는 템플릿 파일을 가르키고, 다른 하나는 role을 지정한다. 이 간단한 어플리케이션은 단지 하나의 role만을 사용한다. 하지만, 이것은 사이트의 다른 부분에 매우 세밀한 접근제어를 필요로 하는경우 매우 강력한 도구로 사용될 수 있다.
요소늬 submission 패라미터는 통상적이나 childtrigger태그는 처음보는 것이다. 이것은 잠시후 설명할 것이다. 그전에, 로그인을 위해 폼을 추가하도록 하자.
로그인 폼
사용자명과 비밀번호를 입력할 폼을가진 템플릿이 필요하다. 여기 새로울것이 없는 평범한 폼이 credentials라는 이름의 submission으로 정의되어 있다.
------------------------------------------------------------------
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head><title>Admin</title></head>
<body>
<table cellpadding="5" cellspacing="0">
<tr valign="top" nowrap="1">
<td width="100%">
<h3>Please provide a login and password</h3>
<!--I 'auth.error_area'/-->
<form name="credentials" action="[!V 'SUBMISSION:FORM:credentials'/]"
method="post">
<!--V 'SUBMISSION:PARAMS:credentials'/-->
login<br/>
<input name="login" value="[!V 'PARAM:login'][!/V]" type="text"
size="18" maxlength="10" /><br/>
password<br/>
<input name="password" type="password" size="18" maxlength="10" /><br/>
<input type="submit" value="Login" /><br/>
</form>
</td>
</tr>
</table>
</body>
</html>
------------------------------------------------------------------
조합하기.
이제 관리자기능을 갖기위해 사이트를 재정의 하였고 인증처리를 위한 요소도 갖추었다. 이제 조합할 준비가 되었다. 이를 위해 RIFE의 강력한 기능중 하나인 상속을 사용할 것이다.
상속
RIFE어플리케이션의 요소(element)나 서브사이트(subsite)는 다른 요소로 상속을 할 수 있다. 이제부터 상속받은 요소를 "자식(child)", 상속하는 요소를 "부모(parent)"로 부르겠다. 상속은 부모의 processElement메소드가 자식의 processElement를 대신해 호출되는 특수한 경우에 작동한다.
RIFE는 이것을 매우 똑똑하게 처리하고 이 과정을 자식요소가 직접 관여하지 못하도록 한다. 부모요소는 전체사이트 구조의 일부분이 될 수 있다. 이것은 exits, accept, submission을 활성화 할 수 있고 상속을 사용하는 다른 요소로 이동할 수도 있다. RIFE웹엔진의 모든 기능은 부모로부터 감추어질 수 있다. 만약 그것이 arrival요소를 가지고 있다면 심지어 전체 서브사이트를 상속할 수도 있다. 경우에 따라, 상속에 제한을 걸어 자식이 활성화 되도록 하여 처음에 전달된 파라미터값을 받도록 할수도 있다.
우리의 경우, 관리자 사이트는 인증요소를 상속받는다. 사용자가 로그인하지 않았을경우, 부모요소가 보여질것이며 그렇지 않은경우 자식의 관리사이트가 보여진다.
자식 트리거(Child triggers)
RIFE엔진은 "childtrigger"라는것을 통해 부모와 자식중 하나를 결정한다. 우리는 인증요소에서 간단히 child trigger를 간략히 언급했었다. 부모요소의 childtrigger태그는 어떤 요소가 실행할지 결정에 사용할 변수 또는 쿠키의 이름을 기술한다. 트리거가 설정되지 않았으면, 부모의 procesElement메소드가 직접 호출된다. 설정되었을 경우에는 부모의 childTriggered메소드가 호출된다. 여기에서 트리거우 값이 옳은지 그른지를 확인할 수 있다.
이 모든과정은 얼핏 복잡하게 들린다. 하지만, 다행히도 이 모든 구현이 위에 우리가 확장한 메모리 사용자 인증 요소에서 구현되었다. 우리는 인증을 위한 별도의 처리를 전혀 할필요가 없었다. 그 이면에서 어떤일이 일어나는지를 아는것은 매우 유용하며 이를 더욱 발전시켜 커스텀요소를 만들어 child trigger를 구현할 필요가 있을때도 유용하다.
authid 변수 추가
인증요소의 childtrigger는 authid라는 변수이다. 우리는 sites/friends.xml 사이트 정의에 전역변수로 이를 추가하고 요소에서 사용할 것이다:
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<!-- Add authid variable -->
<globalvar name="authid"/>
<!-- Refer to the AUTH element definition -->
<element id="AUTH" file="elements/auth.xml" />
...
</site>
------------------------------------------------------------------
이제 관리자 서브사이트가 인증요소를 상속받기 위한 마지막 단계까지 모두 설정되었다.
인증요소 상속
아직 메인사이트에서 관리자 서브사이트를 분리하는것을 제외하고 원본 friends어플리케이션의 아무것도 변경하지 않았다. 사실, 그것은 상속의 장점이다: 여기엔 재작성하거나 요소나 서브사이트를 추가하지 않고 스스로 자식이 된다. 필요한것은 단지 sites/friends.xml의 서브사이트 정의만 수정하면 된다:
------------------------------------------------------------------
<subsite id="ADMIN" file="admin.xml" urlprefix="/admin" inherits="AUTH"/>
------------------------------------------------------------------
inherits="AUTH"만 추가하면, 관리자 사이트는 다른것을 변경하지 않고도 비밀번호로 보호된다.
마무리하기
Friends 어플리케이션은 아직 완전히 다듬어 지지 않았다. 아직 로그아웃할 방법도 없으며 사용자가 로그인중인지 아닌지를 가르키는 표시도 없다. 이제 로그아웃 링크를 추가하고 나머지는 독자를 위해 남겨두도록 하겠다.
로그아웃
내장된 인증요소를 사용해 로그아웃 요소도 포함한다. 로그아웃은 요소를 사이트에 추가하고 템플릿에 링크만 하면 된다.
------------------------------------------------------------------
<element id="LOGOUT" file="rife/logout/passthrough/memory.xml" url="/logout">
<flowlink srcexit="logged_out" destid="DISPLAY"/>
</element>
------------------------------------------------------------------
관리자 메뉴는 로그아웃 링크를 위한 좋은 장소이다.
------------------------------------------------------------------
<p><a href="[!V 'EXIT:QUERY:logout'/]">logout</a></p>
------------------------------------------------------------------
로그아웃 요소는 간단히 child trigger를 초기화 시킨다.
결론
이제 우리는 상속을 이용해 얼마니 쉽게 완성된 어플리케이션에 약간의 중재로 인증기능을 추가할 수 있는지 보았다. 이것은 어플리케이션은 인증을 제외하고도 개발및 테스트, 디버그 될수 있으며 나중에도 인증기능을 상속을 통해 추가할 수 있음을 보여준다
RIFE 시작하기(8) - Adding database support
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Adding database support
지금까지 Friends application은 친구목록을 저장할 곳이 없어 상당히 제한적이었다. 마지막으로, 데이터베이스 지원 부분을 이 장에서 다룰 것이다. 우리는 당신이 이미 SQL database와 자바의 데이터베이스 프레임웍인 JDBC에 어느정도 익숙하다고 가정할 것이다.
웹어플리케이션을 개발할때 앞의 친구예제 처럼 동적인 데이터를 핸들링할 일일 종종 발생한다. 그것은 뉴스, 기사, 이미지등 웹사이트 내용의 무엇이든 될 수 있다. 데이터베이스를 저장소로 사용하게 되면 어플리케이션을 쉽게 설계하고 확장할 수 있다. 산발적으로 흩어진 데이터파일들을 더 쉽게 관리할 수 있게 된다.
우리는 RIFE와 함께 데이터베이스를 설계로 끌어들여 더 직관적이고 쉽게 할것이다.
datasource참여자
먼저, 저장소(repository)에 데이터소스(datasource) 참여자(participant)를 필요로 한다. 그리고, 약간의 설정정보를 참여자에게 제공해야 한다. rep/participants.xml 을 수정한 후의 모습이다.
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">
<rep>
<participant param="rep/config.xml">ParticipantConfig</participant>
<!-- Add a data sources participant: -->
<participant param="rep/datasources.xml">ParticipantDatasources</participant>
<participant param="sites/friends.xml">ParticipantSite</participant>
</rep>
------------------------------------------------------------------
그리고, rep/datasources.xml에 데이터소스 정의 파일을 아래와 같이 작성했다. 두개의 데이터소스를 설정하였다.
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE datasources SYSTEM "/dtd/datasources.dtd">
<datasources>
<datasource name="postgresql">
<driver>org.postgresql.Driver</driver>
<url>jdbc:postgresql://localhost:5432/rife</url>
<user>rife</user>
<password>rife</password>
<poolsize>5</poolsize>
</datasource>
<datasource name="mysql">
<driver>com.mysql.jdbc.Driver</driver>
<url>jdbc:mysql://localhost:3306/rife</url>
<user>rife</user>
<password>rife</password>
<poolsize>5</poolsize>
</datasource>
</datasources>
------------------------------------------------------------------
PostgreSQL과 MySQL 두개의 데이터소스를 설정한 이유는 어플리케이션의 코드를 변경하지 않고도 둘중 하나가 사용 불가능할 경우 쉽게 변경할 수 있게 하기 위함이다. 좋은예로, 개발용 기계와 운영용 기계가 환경에 따라 다른 데이터베이스를 사용해야 할때이다. 주의깊은 독자는 아마 이전장의 설정선택자(configuration selector)의 호스트명 선택자를 떠올릴것이다.
다른 혜택은 이 코드가 다른 데이터베이스 에서도 동일하게 작동하므로 다른 사람 또은 다른 프로젝트에도 유용하게 사용될 수 있다는 것이다. 코드 재사용은 시간을 절약하는 좋은 습관이다.
RIFE는 PostgreSQL과 MySQL, Oracle 데이터베이스에 대한 지원을 내장하고 있다. 그밖의 데이터베이스를 사용하기 위한 드라이버도 사용될수 있으나 이 문서의 범위는 벗어난다.
데이터소스(datasource) 인수(parameter)
: 데이터소스 정의를 위해서는 몇개의 인수를 필요로 한다.
- 사용자명과 비밀번호 : 데이터베이스와 연결하기 위해 user과 password태그에 사용자명과 비밀번호를 설정한다.
- 데이터베이스 드라이버 : driver태그에 어떤 데이터베이스 드라이버를 사용할지 설정한다. org.postgresql.Driver, com.mysql.jdbc.Driver, oracle,driver.OracleDriver 같은 것들중 하나가 사용될 수 있다.
- 주소(URL) : url태그는 데이터베이스의 위치를 설정한다. 맨앞의 jdbc는 주프로토콜을 의미하고 그 다음의 postgresql 또는 mysql은 서브프로토콜이다. 그 다음에 따라오는 localhost는 호스트명이며 포트번호가 뒤따라올 수 있다. 마지막의 rife는 데이터베이스명이다.
- 연결자 (Connection Pool) : 데이터베이스로 연결하는데 많은 시간이 소요되므로 RIFE는 열린 데이터베이스 연결에 대한 풀(pool)을 유지한다. 이것은 많은 트래픽이 발생하는 웹사이트에 특히 성능의 향상을 제공한다.poolsize태그는 연결될 풀의 갯수를 설정하며 0은 풀을 사용하지 않음을 의미한다. 일단, 평균적으로 5를 설정토록 하겠다.
데이터소스 선택
우리는 두개의 데이터소스를 가지고 있으므로 실제 사용할 하나를 선택해야 한다. RIFE의 설정시스템은 단지 파라미터값만 추가해서 어떤 데이터소스가 사용될지 가르키기만 하면 된다. 데이터베이스 코드는 이 파라미터를 읽어서 적절한 작업을 수행하게 된다. 설정을 마친 rep/config.xml파일은 아래와 같다.
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE config SYSTEM "/dtd/config.dtd">
<config>
<param name="DISPLAY_TITLE">Friends of Mikael</param>
<!-- Add a DATASOURCE parameter: -->
<param name="DATASOURCE">postgresql</param>
</config>
------------------------------------------------------------------
새로운 파라미터는 DATASOURCE이며 사용될 데이터소스의 이름을 설정한다.
Friend bean
우리는 각 친구에 대해 이름, 설명, 주소의 데이터구조를 유지할 것이다. 빈(bean)은 이러한 정보를 위한 완벽한 컨테이너이다. 왜냐하면, 많은 RIFE인터페이스는 많은 속성들을 개별로 다루지않고 빈과 작동하기 때문이다. 이것은 훨씬 명백해 보인다.
------------------------------------------------------------------
package tutorial.friends.backend;
public class Friend
{
private String mFirstname = null;
private String mLastname = null;
private String mDescription = null;
private String mUrl = null;
public Friend(String firstname, String lastname, String description, String url)
{
mFirstname = firstname;
mLastname = lastname;
mDescription = description;
mUrl = url;
}
public void setFirstname(String firstname)
{
mFirstname = firstname;
}
public String getFirstname()
{
return mFirstname;
}
// ... and so on for lastName, description and url
}
------------------------------------------------------------------
lastName, description, url에 대한 setter/getter도 firstName과 마챦가지 이다.
데이터 관리
데이터소스에 접근하기 위해서 테이블을 생성하고 추가, 조회등의 질의(queries)를 하는 작은 클래스를 하나 작성해야 한다. RIFE는 이것을 쉽게 한다. 우리는 단지 com.uwyn.rife.database.DbQueryManager만 상속하면 된다. 이것은 우리가 구현해야할 많은 기초적인 작업을 대신 하며 코드를 깔끔하고 이해쉽게 해준다. 실제 코드는 src/tutorial/friends/backend/FriendManager.java 에서 볼 수 있다. 아래에 중요한 부분에 대한 코드일부를 볼것이다.
올바른 데이터소스 사용하기
이 예제에서 두개의 데이터소스중 어떤것을 사용할지 선택할 것이다.
------------------------------------------------------------------
public FriendManager()
{
super(Datasources.getRepInstance().
getDatasource(Config.getRepInstance().getString("DATASOURCE")));
}
------------------------------------------------------------------
이제 쿼리매니저는 설정파일에 입력된 데이터소스와 함께 초기화될 것이다.
친구 출력하기
친구목록을 출력하기 위한 템플릿과 요소(element)는 이미 작성하였다. 필요한것은 단지 데이터베이스 기능만을 추가하는 것이다. SQL질의를 작성하고 이를 데이터베이스상에서 실행하여 검색된 형(row)들을 가공하면 된다. RIFE는 이런 반복작업을 도와줄 좋은 방벘을 가지고 있다. 가장쉬운 방법은 먼저 코드를 보는것이다. FriendManager의 display메소드를 보자.
------------------------------------------------------------------
public void display(DbRowProcessor processor)
{
Select select = new Select(getDatasource());
select
.from("Friend")
.fields(Friend.class)
.orderBy("firstname");
// fetch every row in the resultset and forward the processing
// of the data to the DisplayProcessor
executeFetchAll(select, processor);
}
------------------------------------------------------------------
display메소드는 요소(element)로 부터 호출되어 모든 친구들을 조회하고 템플릿에 값을 삽입한다. 질의는 Friend빈 클래스를 사용해 생성된다. 이것은 우리가 모든 필드를 서술하지 않아도 됨을 의미한다. 이것은 단지 빈의 모든 속성들을 사용한다.
한번에 한친구씩 보여주기
질의를 실행하는 동안, 우리는 모든 행(row)이 처리될 때까지 DbQueryManager의 fetch메소드를 호출하여 결과를 조회할 수 있다. 이때 DbRowProcessor객체가 인수로 전달된다.
행처리기(row processor)는 데이터베이스코드가 요소와 출력코드로 부터 분리할 수 있는 방법을 제공한다. FriendManager는 이것을 사용하는 요소에 관해 전혀 알지 못한다. 요소도 데이터베이스 코드에 관해 전혀 알지 못한다. 이러한 종류의 디자인은 대부분 어플리케이션의 유지보수를 쉽게 하기위해 항상 요구되는 것이다.
행처리기 작성하기
행처리기의 작성은 간단하다. DbRowProcessor를 확장하고 processRow메소드를 구현하기만 하면 된다. 이 처리기는 display요소 바깥쪽에서는 절대 쓰이지 않는다. 이 클래스는 tutorial/fiends/Display.java의 내부클래스(inner class)로 구현될 것이기 때문이다.
------------------------------------------------------------------
private class DisplayProcessor extends DbRowProcessor
{
private Template mTemplate = null;
/**
* The constructor requires a Template instance in which the retrieved
* data will be filled in.
*
* @param template the Template in which the results will be display
*/
DisplayProcessor(Template template)
{
mTemplate = template;
}
public boolean processRow(ResultSet resultSet)
throws SQLException
{
mTemplate.setValue("firstname",
encodeHtml(resultSet.getString("firstname")));
mTemplate.setValue("lastname",
encodeHtml(resultSet.getString("lastname")));
mTemplate.setValue("description",
encodeHtml(resultSet.getString("description")));
mTemplate.setValue("url",
encodeHtml(resultSet.getString("url")));
mTemplate.appendBlock("rows", "row");
return true;
}
}
------------------------------------------------------------------
데이터베이스와 HTML생성은 매우 명확하게 분리되었다. 행처리기(row processor)가 하는 일은 결과값을 템플릿에 채우는것 뿐이다. 이제 남은일은 요소의 processElement메소드를 호출하는 것이다. 여기서 manager를 생성하고 행처리기와 함께 display 메소드를 호출하게 된다.
------------------------------------------------------------------
FriendManager manager = new FriendManager();
DisplayProcessor processor = new DisplayProcessor(template);
manager.display(processor);
------------------------------------------------------------------
친구 추가
친구를 추가할 수 없다면 조회는 아무런 의미가 없다. 요소와 함께 어플리케이션을 확장해 친구를 추가할 수 있도록 하겠다.
새로운 요소 정의
ADD요소는 매우 단순하다. 이것은 다른것들 처럼 출구(exit)나 데이터링크(datalink)를 가지지 않는다. 유일하게 지금까지 다루지 않았던 submission bean의 사용에 관한 것이다. 이제 사이트파일과 요소파일을 보고 submission bean이 어떻게 작동하는지 설명하겠다.
------------------------------------------------------------------
갱신된 사이트 파일
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<arrival destid="DISPLAY"/>
<globalexit name="menu" destid="MENU"/>
<element id="DISPLAY" file="display.xml" url="/display"/>
<element id="MENU" file="menu.xml" url="/menu">
<!-- Add a flowlink to the new add element -->
<flowlink srcexit="add" destid="ADD"/>
<flowlink srcexit="display" destid="DISPLAY"/>
</element>
<!-- Add the new element -->
<element id="ADD" file="add.xml" url="/add"/>
</site>
------------------------------------------------------------------
사이트파일의 주석문이 변경된 내용을 가르키고 있다: 새로운 요소에 대한 메뉴와 요소가 추가되었다.
------------------------------------------------------------------
추가된 요소파일
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">
<element implementation="tutorial.friends.Add">
<submission name="friend_data">
<bean classname="tutorial.friends.backend.Friend"/>
</submission>
</element>
------------------------------------------------------------------
Submission beans
submission빈은 다른 많은 RIFE의 기능들 처럼 공통된 웹어플리케이션 작업중 폼으로부터의 입력을 핸들리하는 특별히 추상화된 개념이다. submission bean은 빈(bean)을 사용하여 간단하게 셋업된다. classname은 빈의 클래스명을 가르킨다. 이경우 Friend의 표현이다. 빈을 설정한 다음에는 getSubmissionBean이 사용자가 입력한 폼을 빈의 속성값으로 가져오기 위해 사용된다.
------------------------------------------------------------------
Friend friend = (Friend)getSubmissionBean("friend_data", Friend.class);
------------------------------------------------------------------
이방법은 적은 라인만을 필요로 하며 친구객체에 어떤 속성이 추가되거나 삭제되어도 상관없다.
추가 템플릿
마지막으로, 입력을 위한 템플릿을 작성하는 것이다. 이것은 숫자맞추기 게임의 템플릿과 비슷하다.
------------------------------------------------------------------
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head><title>Add a new friend to the database</title></head>
<body>
<h3>Add a friend</h3>
<!--V 'content'/-->
<!--BV 'content'-->
<form name="friend_data" action="[!V 'SUBMISSION:FORM:friend_data'/]"
method="post">
<!--V 'SUBMISSION:PARAMS:friend_data'/-->
Firstname<br/>
<input type="text" name="firstname" length="51" maxlength="50"/><br/>
Lastname<br/>
<input type="text" name="lastname" length="51" maxlength="50"/><br/>
Description<br/>
<textarea name="description" cols="80" rows="6"></textarea><br/>
Url<br/>
<input type="text" name="url" length="80" maxlength="255"/><br/>
<input type="submit" name="Add this friend."/>
</form>
<!--/BV-->
<!--B 'content_added'-->
<p>The friend has been added.</p>
<!--/B-->
<p><a href="[!V 'EXIT:QUERY:menu'/]">back to menu</a></p>
</body>
</html>
------------------------------------------------------------------
SUBMISSION:FORM: 과 SUBMISSION:PARAMS: 가 submission bean처리에 사용된다.
친구를 데이터베이스에 추가
삽입쿼리를 작성하고 실행하는것은 빈에 모든 데이터가 있을때 쉽게 할 수 있다.
------------------------------------------------------------------
public void add(Friend friend)
throws DatabaseException
{
Insert insert = new Insert(getDatasource());
insert
.into("Friend")
.fieldsParameters(Friend.class);
DbPreparedStatement insert_stmt = getConnection().getPreparedStatement(insert);
insert_stmt.setBean(friend);
insert_stmt.executeUpdate();
}
------------------------------------------------------------------
마무리하기
테이블 생성과 삭제
------------------------------------------------------------------
<element id="INSTALL" file="install.xml" url="/install"/>
<element id="REMOVE" file="remove.xml" url="/remove"/>
------------------------------------------------------------------
이 두개의 단순한 요소는 설치와 제거를 위한 확인버튼만 있다. 목록을 보여주는 작업같은건 필요가 없다. 이 요소들에 대한 정의파일과 템플릿은 classes/elements/install.xml, classes/elements/remove.xml, classes/templates/install.html, classes/templates/remove.html 에 있다.
테이블 생성
install요소는 manager의 install메소드를 호출한다. 이 메소드는 데이터베이스테이블을 생성한다.
------------------------------------------------------------------
public void install()
throws DatabaseException
{
CreateTable create = new CreateTable(getDatasource());
create
.table("Friend")
.columns(Friend.class)
.precision("firstname", 50)
.precision("lastname", 50)
.precision("url", 50)
.nullable("firstname", CreateTable.NOTNULL)
.nullable("lastname", CreateTable.NOTNULL)
.nullable("description", CreateTable.NOTNULL)
.nullable("url", CreateTable.NOTNULL);
executeUpdate(create);
// ...
}
------------------------------------------------------------------
다른 질의들 처럼 빈클래스가 컬럼생성에 사용되었다. 이경우 문자열의 길이나 널(NULL)속성과 같은 더 상세한 속성들을 설정하기 위해 컬럼명이 서술되었다. 그럼에도 불구하고, 빈클래스의 사용은 Friend빈을 위한 저장소로 테이블을 사용하기에 매우 명확하고 분명한 명분을 제공한다.
정리
테이블을 삭제하는 것은 CreateTable대신 DropTable을 사용하는 것만 제외하면 생성하는것과 같다.
------------------------------------------------------------------
public void remove()
throws DatabaseException
{
DropTable drop = new DropTable(getDatasource());
drop.table("Friend");
executeUpdate(drop);
}
------------------------------------------------------------------
RIFE 시작하기(7) - Adding configuration to the friends application
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Adding configuration to the friends application
- Some explanation on RIFE participants
이 장에서는 friends어플리케이션에 설정(configuration)정보를 추가하는 방법을 설명한다. 앞에서 우리는 출력페이지의 타이틀을 설정가능토록 하기로 했다. 이런 기능은 하나의 어플리케이션을 여러명이 사용할 경우 My friends" 와 같은 제목 봐 "Friends of Mikael"과 같이 출력하고자 할때 유용하다.
설정(Configuration)은 ParticipantConfig참여자(paticipant)로 제어할 수 있다. 앞서 ParticipantSite를 살펴보고 간단히 참여자에 관해 설명하였다. 그러므로, 조금더 참여자들에 대한 설명을 하도록 하겠다.
참여자의 다양한 타입
참여자(participant)는 특정 작업을 처리하는 어플리케이션의 일부분이다. 간단한 어플리케이션은 ParticipantSite를 제외하고 다른 어떤 참여자도 사용하지 않을 수 있다. 하지만, 복잡도가 높아질수록 참여자(participant)는 필요하게 된다. 간혹, RIFE가 제공하는것 이외에 사용자정의 참여자(custom participant)를 개발할 필요가 있을수도 있다.
RIFE에는 다양한 내장된 참여자(built-in participant)가 다양한 작업을 처리한다. 이 장에서는 그중에서 설정처리를 위한 참여자에 대해 자세히 알아보겠다. 다음장에서는 데이터베이스 참여자를 사용할 것이다. 모든 참여자에 대한 자세한 내용은 com.ueyn.rife.rep.participants 패키지의 API문서를 보도록 하라.
참여자의 실행
참여자는 저장소(repository)파일에 정의되어 시작된다. 각 참여자는 고유의 쓰레드로 실행되고 따라서, 이들은 모두 동시에 실행된다. 만약 동시에 실행되지 않도록 하고 싶다면 paticipant태그에 blocking="true"속성을 추가하면 된다. 예를들어, 설정 참여자(config paticipant)가 우선 시작되길 원한다면 ParticipantConfig정의를 다음과 같이 하면 된다:
------------------------------------------------------------------
<participant param="rep/config.xml" blocking="true">ParticipantConfig</participant>
------------------------------------------------------------------
RIFE참여자들은 경우에 따라 상호 종속성을 가진다. 특정 참여자가 다른 참여자가 제공하는 정보에 의존하는 경우가 있으며, 이경우 RIFE는 의존하는 참여자가 완전히 로딩될때 까지 기다리기 위해 쓰레드를 멈출수(block) 있다. 하지만, 이런경우는 매우 드물다.
- Configuration in the friends application
이제 참여자에 대해 좀 더 알게 되었으므로 편하게 저장소(repository)에 설정참여자를 추가해 보자
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">
<rep>
<participant param="rep/config.xml">ParticipantConfig</participant>
<participant param="sites/friends.xml">ParticipantSite</participant>
</rep>
------------------------------------------------------------------
설정 참여자는 설정데이터를 포함하는 파일을 파라미터로 받는다. 위의 경우 rep/config.xml이 된다. 설정파일의 내용은 다음과 같다:
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE config SYSTEM "/dtd/config.dtd">
<config>
<param name="DISPLAY_TITLE">Friends of Mikael</param>
</config>
------------------------------------------------------------------
DISPLAY_TITLE 파라미터는 요소구현에서 이용가능하다. 이제는 단지 display.html 템플릿의 제목부분에 값(value)태그만 추가하면 된다.
------------------------------------------------------------------
<html>
<head><title><!--V 'title'-->My Friends<!--/V--></title></head>
<body>
<h3><!--V 'CONFIG:DISPLAY_TITLE'-->My Friends<!--/V--></h3>
<!-- Rest of the template -->
</body>
</html>
------------------------------------------------------------------
템플릿 챕터에서 시작태그와 종료태그 사이의 문자열인 "My friends"은 아무런 값을 제공하지 않았을때 보여지는 기본값이라 명시했었다. 이 템플릿은 두가지 방법으로 제목을 설정한다. 첫번째 방법은 템플릿의 값(value)태그로 설정값을 직접 읽어온다:
------------------------------------------------------------------
<!--V 'CONFIG:DISPLAY_TITLE'-->
------------------------------------------------------------------
다른 방법으로는 자바 구현부의 setValue에 의해 템플릿에 값을 설정하는 것이다.
------------------------------------------------------------------
Config config = Config.getRepInstance();
if (config.hasParameter("DISPLAY_TITLE"))
{
template.setValue("title", config.getString("DISPLAY_TITLE"));
}
------------------------------------------------------------------
설정파일에 값이 설정되었다면 이것을 제목으로 사용할 것이며 그렇지 않다면 "My Firends"가 출력될 것이다.
- More on configuration
설정파일의 파라미터에 접근하기
Friends예제에서 우리는 모든 페이지(Display, Add, Remove, ...)의 타이틀을 설정하려 한다. 예를들면 추가(Add) 페이지에서는 "Add a Friend of Mikael"과 같이 할수도 있다. 모든 페이지의 타이틀에 사용자명은 반드시 포함될 것이다. 사용자가 변경될때마다 이것을 모두 변경하는 것은 금방 지루한 일이 될것이다. RIFE는 이 문제에 대한 해결책을 갖고 있다: 바로, 설정파일에서 스스로 접근하여 파라미터를 가져오는 것이다.
------------------------------------------------------------------
설정파일로 부터 값을 사용하기
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE config SYSTEM "/dtd/config.dtd">
<config>
<param name="USERNAME">Mikael</param>
<param name="DISPLAY_TITLE">Friends of <value name="USERNAME"/></param>
<param name="ADD_TITLE">Add a Friend of <value name="USERNAME"/></param>
</config>
------------------------------------------------------------------
USERNAME 파라미터는 Display페이지와 Add페이지 모두에 제목을 생성할 것이다. 만약 누군가 이름을 변경하고자 한다면 한곳만 변경하면 될것이다.
설정 선택자(selector)
어플리케이션 개발시, 종종 다른 설정정보를 옵션으로 제공할 필요가 있을 수 있다. 이것은 개발과정에서 다른 설정과정과 컴퓨터상에서 작동할 소프트웨어를 테스트하는데 유용하다.
RIFE의 설정시스템은 이를 위한 해결책을 제공한다. Config 참여자는 호스트명이나 운영체제 또는 사용자명에 따라 다른 설정파일을 선택할 수 있도록 한다.
------------------------------------------------------------------
<participant param="XmlSelectorHostname">ParticipantConfig</participant>
------------------------------------------------------------------
이 설정 참여자는 config-{host}.xml 이라는 파일을 찾게된다. 다른경우 XmlSelectorOs 선택자를 사용하면 운영체제에 따른 설정세트를 불러올수도 있다. 사용자명에 따를 경우에는 XmlSelectorUser가 사용된다.
설정선택자를 사용할 경우 다음과 같은 패턴의 파일들이 사용된다.
------------------------------------------------------------------
선택자를 사용할 경우의 파일명 규칙
------------------------------------------------------------------
Host name: rep/config-{host}.xml
Operation system: rep/config-{OS}.xml
User name: rep/config-{user}.xml
------------------------------------------------------------------
호스트, 운영체제, 사용자 문자열은 모두 소문자로 치환되며 점(.)이나 공백(space)은 밑준(_)문자로 대체된다.
설정파일 포함
설정선택자를 사용할때, 다른 설정의 경우 모든 파라미터가 필수사항은 아니다. 이런경우 일반적으로 공통된 설정은 별도의 파일로 분리하여 다른 설정파일들이 이를 공유하다록 한다. RIFE는 설정파일의 포함을 지원한다. 이것은 모든 선택자가 공통설정 파일을 포함할 수 있도록 한다.
------------------------------------------------------------------
<include file="rep/config-common.xml"/>
------------------------------------------------------------------
이제 공통옵션을 변경하려면 모든 선택자 파일을 변경하지 않고 하나만 변경하면 된다.
RIFE 시작하기(6) - A friends database
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
A friends database
- Introduction
숫자맞추기 게임에서 submission과 템플릿을 이용해 사용자의 입력을 처리하는과정을 통해 몇가지 RIFE의 강력한 기능을 살펴보았다. 이제 friend 데이터베이스 어플리케이션을 통해 좀 더 발전된 어플리케이션을 개발해 보기로 하겠다.
이 어플리케이션은 제목과 친구들 목록을 보여주고 또 새로운 친구를 데이터베이스에 추가할 수 있다. 또한, 어플리케이션의 사본을 친구에게 보내어 그가 이것을 사용할 수 있도록 할것이다. 하지만, 그가 제목을 변경하기 위해 코드를 수정하는것은 원치 않는다. 단지, 설정(configuration)을 통해 이를 쉽게 할 수 있도록 할것이다.
이 예제의 소스 역시 RIFE예제 패키지에 포함되어 있다. 이해를 돕기위해 어플리케이션의 디렉토리 구조를 살펴 보겠다.
------------------------------------------------------------------
friend 어플리케이션의 디렉토리 구조
------------------------------------------------------------------
src/
elements/
add.xml
display.xml
install.xml
menu.xml
remove.xml
implementations/
tutorial/
friends/
Add.java
Display.java
Install.java
Remove.java
rep/
config.xml
datasources.xml
participants.xml
sites/
friends.xml
templates/
add.html
display.html
install.html
menu.html
remove.html
tutorial/
friends/
backend/
Friend.java
FriendManager.java
------------------------------------------------------------------
어플리케이션은 다섯개의 요소로 구성되어 있다. 그 다섯개의 요소는 고유의 자바 구현체를 가지고 있으며 각 구현체는 이미 RIFE를 통해 요소(element)에 접근한다. 모든 컨텐츠는 템플릿을 통해 생성되고 각 요소는 하나의 템플릿과 연동된다. 친구는 Friend객체로 표현되며, 모든 친구들은 FriendManager에 의해 관리되어 진다. 먼저 site정의를 성정하는것으로 시작해 친구들을 출력하기 위한 DISPLAY요소를 정의하고 어플리케이션을 활용(navigating)하기 위해 MENU요소도 정의할 것이다.
이전 예제에서 처럼 ParticipantSite 참여자를 사용해 메인사이트 파일을 정의한다.
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">
<rep>
<participant param="sites/friends.xml">ParticipantSite</participant>
</rep>
------------------------------------------------------------------
- Defining and connection the elements
이제 DISPLAY와 MENU요소를 정의하고 연결할 것이다. 데이터베이스장이 끝날때쯤 나머지 요소들도 모두 추가되어 모든 기능이 구현될 것이다.
------------------------------------------------------------------
DISPLAY 요소
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">
<element implementation="tutorial.friends.Display"/>
------------------------------------------------------------------
------------------------------------------------------------------
MENU 요소
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">
<element implementation="com.uwyn.rife.engine.elements.PrintTemplate">
<property name="template_name">menu</property>
<exit name="install"/>
<exit name="add"/>
<exit name="remove"/>
<exit name="display"/>
</element>
------------------------------------------------------------------
DISPLAY요소는 전혀 놀랍지 않지만 MENU요소에는 뭔가 새로운 것이 있다. 이것은 내장요소구현체(built-in element implementation)인 PrintTemplate를 사용하고 있다. 이것은 속성(properties)을 초기화 하는데 사용하기 위해 파라미터를 받는다.
- Properties
속성(property)을 사용하면 요소에 고정된 값을 제공할 수 있다. 이것은 데이터흐름의 일부가 아닌 대부분 요소에 특화된 설정을 목적으로 사용된다. 위의 경우 템플릿의 이름을 제공하는데 사용되었다. PrintTemplate은 속성값을 읽고, 템플릿의 인스턴스를 생성하는데 사용한 후 출력하였다. 자바 코드에서 property속성이 어떻게 사용되는지는 아래의 PrintTemplate구현 자바코드를 보라.
------------------------------------------------------------------
package com.uwyn.rife.engine.elements;
import com.uwyn.rife.engine.Element;
import com.uwyn.rife.engine.exceptions.PropertyRequiredException;
import com.uwyn.rife.template.Template;
public class PrintTemplate extends Element
{
public void processElement()
{
if (hasProperty("name"))
{
Template template;
template = getHtmlTemplate(getPropertyString("name"));
print(template);
}
else
{
throw new PropertyRequiredException(getDeclarationName(), "name");
}
}
}
------------------------------------------------------------------
속성을 조회하는 대신, 속성명과 setter를 이용해 RIFE는 값을 자동으로 주입(inject)할 수도 있다.
------------------------------------------------------------------
package com.uwyn.rife.engine.elements;
import com.uwyn.rife.engine.Element;
import com.uwyn.rife.engine.exceptions.PropertyRequiredException;
import com.uwyn.rife.template.Template;
public class PrintTemplate extends Element
{
private String name;
public void setName(String name) { this.name = name }
public void processElement()
{
if (null == name)
{
throw new PropertyRequiredException(getDeclarationName(), "name");
}
Template template;
template = getHtmlTemplate(name);
print(template);
}
}
------------------------------------------------------------------
이처럼 내장된(built-in)요소를 사용하면 자바요소에 대해 매번 구현하지 않고 템플릿을 출력할 수 있다. 이와 똑같은 작업은 고유의 구현으로도 분명히 가능하며 이것은 요소의 재사용과 Data links, Flow links의 흐름을 통해 어플리케이션을 전반적으로 통제할 수 있는 강력한 기능을 제공한다.
- The site definition
MENU 요소 정의에서 본것과 같이, 네개의 출그(exit)를 가지고 있다: 하나는 어플리케이션의 각 페이지 이다. 아직, INSTALL, ADD, REMOVE요소를 위한 요소를 추가하지 않았다. 지금은 그냥 놔두자.
------------------------------------------------------------------
사이트 정의의 맨첨부분
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<arrival destid="DISPLAY"/>
<globalexit name="menu" destid="MENU"/>
<element id="DISPLAY" file="display.xml" url="/display"/>
<element id="MENU" file="menu.xml" url="/menu">
<flowlink srcexit="display" destid="DISPLAY"/>
</element>
</site>
------------------------------------------------------------------
- Global exits
사이트 정의는 주된 요소가 DISPLAY인것을 보여주며 일반적인 HTML인 index.html 페이지로 보여지길 원한다. 브라우저는 특정 url을 생략했을 경우 이페이지가 홈페이지가 될것이다. 그리고, 공통출구를 globalexit태그로 정의하였다. 공통출구(global exit)는 모든 요소에 적용된다. 이경우 우리는 display.html과 같은 모든 페이지에서 메뉴로 링크될 수 있도록 할 수 있다.
------------------------------------------------------------------
친구목록 출력을 위한 템플릿
------------------------------------------------------------------
<html>
<head><title>My friends</title></head>
<body>
<h3>My Friends</h3>
<p>This is a list of my friends that have interesting websites to visit.</p>
<!--V 'content'/-->
<!--BV 'content'-->
<table border="1" cellpadding="5" cellspacing="0">
<tr>
<th>firstname</th>
<th>lastname</th>
<th>website</th>
</tr>
<!--V 'rows'--><!--/V-->
<!--B 'row'-->
<tr valign="top">
<td><!--V 'firstname'/--></td>
<td><!--V 'lastname'/--></td>
<td><a href="[!V 'url'/]"><!--V 'description'/--></a></td>
</tr>
<!--/B-->
</table>
<!--/BV-->
<!--B 'content_error'-->
<p>The display couldn't be performed due to the following error:</p>
<p style="color: #990000;"><!--V 'error'/--></p>
<!--/B-->
<p><a href="[!V 'EXIT:QUERY:menu'/]">go to menu</a></p>
</body>
</html>
------------------------------------------------------------------
마지막 부분에서 보듯, 메뉴링크가 일반적인 출구와 같이 처리되었다.
- Displaying the friends
위의 템플릿에서, rows값과 row블랙을 주의해서 보라. 각 row는 한명의 친구를 나타내며 데이터베이스의 모든 친구를 순환하며 rows에 추가로 출력 할 것이다. 이것은 자바구현체에서 appendBlock을 호출하면 된다. DISPLAY요소의 구현부분을 보자.
------------------------------------------------------------------
mTemplate.setValue("firstname", encodeHtml(resultSet.getString("firstname")));
mTemplate.setValue("lastname", encodeHtml(resultSet.getString("lastname")));
mTemplate.setValue("description", encodeHtml(resultSet.getString("description")));
mTemplate.setValue("url", encodeHtml(resultSet.getString("url")));
mTemplate.appendBlock("rows", "row");
------------------------------------------------------------------
이 코드는 친구 한명당 한번 호출된다. 여기에서 흥미로운 부분은 이전예제에서 처럼 setValue로 값을 설정한다는 것이다. 하지만 마지막에 setBlock대신 appendBlock을 사용했다. 이것은 RIFE가 블럭의 값의 마지막 부분을 교체하지 않고 추가하도록 한다. 결과적으로 데이터베이스로 부터 불러온 모든 친구는 하나씩 목록으로 출력될 것이다.
이 예제는 RIFE가 어떻게 로직과 출력을 분리하는지를 잘 보여준다. 템플릿에는 단지 블럭과 값위치지정자만 정의되어 있다. 이것은 나중에 자바코드에서 요소의 논리적 흐름에 따라 페이지를 생성한다.
이제 기본적인 요소는 자리잡았으니 어플리케이션을 설정(configuration)할 필요가 있다.
RIFE 시작하기(5) - Templates
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Templates
앞장에서 템플릿에 관해 조금 살펴 보았다. 하지만 이 부분은 매우 중요한 부분이므로 지금부터 더 깊이 알아볼 것이다. 먼저 이전의 숫자맞추기 템플릿을 다시 보자:
-----------------------------------------------------------------
guess.html
-----------------------------------------------------------------
<html>
<head><title>Perform a guess</title></head>
<body>
<p>Guess a number from 0 to 100</p>
<!--V 'warning'--><!--/V-->
<!--B 'invalid'-->
<p><font color="#990000">The guess is invalid.</font></p>
<!--/B-->
<p><i><!--V 'indication'--><!--/V--></i></p>
<!--B 'lower'-->The answer is lower.<!--/B-->
<!--B 'higher'-->The answer is higher.<!--/B-->
<form action="[!V 'SUBMISSION:FORM:performGuess'/]" method="post">
<!--V 'SUBMISSION:PARAMS:performGuess'/-->
<input type="text" name="guess"
value="[!V 'PARAM:guess'][!/V]" /><br />
<input type="submit" value="Guess" /><br />
</form>
<script language="javascript">document.forms[0].guess.focus();</script>
</body>
</html>
-----------------------------------------------------------------
이 HTML템플릿에서는 V 와 B 두개의 템플릿 태그가 사용되었다. 앞장을 보면 값(value)태그와 구역(block)태그에 관한 간단한 설명을 볼 수 있다.
- The block tag
B태그는 구역(block)을 정의한다. 이제부터 단지 block태그라 부르겠다. block은 완전히 제거되어 생성된 HTML상에는 나타나지 않는다. 하지만 이 컨텐츠는 value태그에 할당될 수 있다. 이것은 자바 코드상에서 setBlock을 호출하여 작동한다. 다시말해, block은 빌딩블럭처럼 생각할 수 있으며 이것은 나중에 페이지생성시에 사용된다. guess템플릿에는 몇개의 block이 정의되어 있지만 실행시에는 하나만 사용된다.
-----------------------------------------------------------------
Guess.java에서 setBlock()의 사용예
-----------------------------------------------------------------
if (game.getAnswer() < guess)
{
mTemplate.setBlock("indication", "lower");
}
else if (game.getAnswer() > guess)
{
mTemplate.setBlock("indication", "higher");
}
-----------------------------------------------------------------
이 자바구현체의 코드조각은 guess요소가 어떻게 lower와 higher block을 value위치지정자에 사용하는지를 보여준다. block의 컨텐츠는 값에 할당되었을 때에만 나타난다는 것을 기억해라.
- The value tag
value 태그(V)는 어디에 컨텐츠가 위치할지를 지정하는데 사용된다. 따라서, 이것은 단순히 위치만 지정할 뿐 무언가가 보여지고 난다음에는 보여지지 않는다. 숫자맞추기 템플릿에서는 warning과 indication 위치지정자가 정의되었다. 이들은 나중에 form ubmission의 결과에 따라 값을 설정하는데 사용되어 졌다. value태그는 다음과 같이 사용된다:
-----------------------------------------------------------------
<!--V 'name'-->Default Value<!--/V-->
-----------------------------------------------------------------
숫자맞추기 예제에서는 단순히 빈기본값을 가졌었다. 이것은 값이 설정되기 전에는 아무것도 표현하지 않는다는 것을 의미한다. 만약 기본값을 설정하게 되면 이것은 값이 변경되기 전까지 사용되어 진다. 그런 예는 다음 섹션에서 보게 될것이다. 값은 자바코드의 setValue를 호출함으로서도 설정될 수 있다.
아래는 짧은 사용 예이다. 이것은 기본값을 가지지 않는다.
-----------------------------------------------------------------
<!--V 'name'/-->
-----------------------------------------------------------------
이처럼 아무값도 설정되지 않은 태그는 단지 정확한 결과만 출력해줄 뿐이다. 이것은 값의 명확한 위치를 표시하고 다른 처리는 하지 않는다.
- Overriding the default value
value태그의 기본값은 템플릿내에서 스스로 설정될 수 있다. 이것은 BV태그를 사용한다. 이것은 컨텐츠가 같은 이름을 사용하는 value태그에 자동으로 기본값 설정이 된다는것만 제외하면 일반적인 block태그와 같은 속성을 가지고 있다. 물론 모든 block태그의 기능들을 여기에서도 사용할 수 있다.
-----------------------------------------------------------------
<!--V 'warning'/-->
<!--BV 'warning'-->No warnings!<!--/BV-->
-----------------------------------------------------------------
이렇게 바꾸면 warning의 기본값은 "No warning!"이 된다. 새로운 기본값은 자바코드에서 값이 설정되지 않았을때 사용되어 진다.
위의 템플릿 코드는 아래와 같이 쓰여질 수 도 있으며 같은 결과를 출려한다:
-----------------------------------------------------------------
<!--V 'warning'-->No warnings!<!--/V-->
<!--B 'warning'-->No warnings!<!--/B-->
-----------------------------------------------------------------
하지만, 진짜 BV태그의 장점은 include태그와 함께 사용했을때 나타난다. 다음 섹션에서 자세히 다룰 것이다.
- The include tag
지금까지 한번도 다루지 않았던 " I 태그 "라는 것이 있다. 이것은 현재 페이지에 다른 템플릿을 포함시키고자 할때 사용된다. 이 태그는 예를들어 사이트 전체에 공통적으로 사용하는 표준 페이지가 있을때 매우 유용하다. 예제에서, page템플릿은 공통헤더와 푸터, 메인메뉴를 정의한다.
-----------------------------------------------------------------
page.html 표준페이지로 사용될 템플릿
-----------------------------------------------------------------
<html>
<head>
<title><!--V 'window_title'-->the window title<!--/V--></title>
<link rel="stylesheet" type="text/css" href="/style/site.css"/>
</head>
<body>
<div id="menu">
<a class="[!V 'menu_home']inactive[!/V]"
href="[!V 'EXIT:QUERY:home'/]">HOME</a>
<a class="[!V 'menu_other']inactive[!/V]"
href="[!V 'EXIT:QUERY:other'/]"OTHER</a>
</div>
<!--V 'page_content'-->content goes here<!--/V-->
</body>
</html>
-----------------------------------------------------------------
"Home" 과 "Other"라는 서로 다른 페이지가 있고 우리는 현재 메뉴명이 하이라이트 되기를 원한다. 이것은 사용자가 사이트를 둘러보기 쉽게한다. 다른 CSS class를 메뉴에 사용하여 메뉴아이템에 시각효과를 정의할 것이다.
위의 템플릿에서 menu_home 과 menu_other 에 메뉴링크의 CSS class속성을 제어하기 위해 value태그를 사용했다. 그것은 active이거나 inactive일 수 있다.
기본값은 "inactive"로 설정되어 있어 페이지가 보여지지 않음을 의미한다.
우리는 이 표준페이지를 다른 두 페이지에서 포함하길 원한다. 언급했던대로, 이것은 I태그를 써서 해결할 수 있다.
-----------------------------------------------------------------
home.html I태그를 사용해 다른 페이지를 포함하기
-----------------------------------------------------------------
<!--I 'page'/-->
<!--BV 'menu_home'-->active<!--/BV-->
<!--BV 'page_content'-->
My content on the Home page
<!--/BV-->
-----------------------------------------------------------------
I 태그를 사용해 "page"템플릿을 포함시켰다. 그리고, menu_home의 기본값을 "active"로, page_content를 "My content on the Home page"로 설정했다. 이런 접근법으로 많은 페이지를 이용해 사이트를 쉽게 구축할 수 있게 되었다.
이제 템플릿에 관한 모든것을 알게 되었으므로 friends database application을 시작할 준비가 되었다.
RIFE 시작하기(4) - Number Guess 어플리케이션 (3/3)
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
- Reading data from a form
이제 우리는 사용자로부터 웹페이지 폼으로 입력을 받아야 한다. 이것은 Guess요소의 submission태그에 정의 되어 있다.
------------------------------------------------------------------
Guess요소에 submission 추가
------------------------------------------------------------------
<element implementation="tutorial.numberguess.Guess">
<input name="gameid"/>
<submission name="performGuess">
<param name="guess">
<default>-1</default>
</param>
</submission>
<exit name="start"/>
<exit name="success"/>
<output name="gameid"/>
</element>
------------------------------------------------------------------
하나의 submission파라미터가 정의되어 있다. 이름처럼 사용자가 추측한 숫자를 의미한다. submission은 항상 이전의 원본요소를 참조한다. 하나의 요소안에 모든 submission의 핸들링을 집중하는것은 폼을 보여주기 위한 페이지와 서브밋된 데이터를 다루는 두개의 페이지를 대신한다.
폼출력과 데이터 처리가 같은 요소에서 처리되므로 어떤 데이터가 누락되거나 잘못되었을때 이전페이지로 돌아가지 않고 폼을 다시 출력하기가 쉽다.
템플릿에도 submission을 위해 다음의 코드를 추가해야 한다.
------------------------------------------------------------------
폼 템플릿
------------------------------------------------------------------
<form action="[!V 'SUBMISSION:FORM:performGuess'/]" method="post">
<!--V 'SUBMISSION:PARAMS:performGuess'/-->
<input type="text" name="guess" value="[!V 'PARAM:guess'][!/V]" /><br />
<input type="submit" value="Guess" /><br />
</form>
------------------------------------------------------------------
action이 특별한 값으로 설정되었다: SUBMISSION:FORM:performGuess. RIFE는 이 값을 정확한 URL로 대채시킨다. 이 경우 대응되는 URL은 GUESS요소가 된다. 이 특별한 값의 마지막 부분은 일찍이 요소정의파일에 선언한 submission의 이름을 참조한다.
input HTML태그의 값은 PARAM:guess 로 설정되었다. 이것은 다시 submission정의에서 guess파라미터와 대응된다. 이 값은 자동으로 파라미터의 마지막값으로 변환되며 사용자에게 항상 마지막 숫자를 볼수 있도록 허락한다. 우리가 여기에서 더긴 value태그를 사용했음을 주목하라. 이것에 대해서는 뒤에 자세히 설명할 것이다. 여기에서 짚고 넘어갈 부분은 guess파라ㅣ터가 없을 경우 값은 빈문자열이 될것이며 사용자에게는 비어있는 필드를 보여줄 것이라는 것이다.
*주의사항*
항상 SUBMISSION:FORM:name 태그와 함께 SUBMISSION:PARAMS:name태그를 사용하라. 그렇지 않으면 RIFE는 경고를 보여줄 것이다. 이것은 미리 알아 두는것이 좋다.
SUBMISSION:과 같은 접두어를 동반한 값은 RIFE에서 특별히 취급된다. 예를들어, 폼액션과 파라미터가 세팅되었다. 여기에 우리가 곧 보게될 EXIT:와 CONFIG:와 같은 다른 접두어가 있다. 이 태그들은 filtered value태그라 부른다. RIFE는 자동으로 이것들을 걸러 특별한 컨텐츠로 치환한다.
페이지에 들어가면 요소는 submission이 존재하는지 확인하고 그렇다면 그것을 처리한다. 처리는 hasSubmission메소드를 통해 이루어 지며 값들은 getParameter와 같은 메소드들로 가져온다. 정수값을 가져오기 위해서는 getParameterInt가 사용된다. 다른 API들에 대한 정보는 javadoc의 com.ueyn.rife.engine.Element클래스를 보면 알 수 있다.
------------------------------------------------------------------
Guess요소를 위한 자바코드
------------------------------------------------------------------
package tutorial.numberguess;
import com.uwyn.rife.engine.Element;
import com.uwyn.rife.template.Template;
import tutorial.numberguess.backend.Contest;
import tutorial.numberguess.backend.Game;
public class Guess extends Element
{
private Template mTemplate = null;
private void handleGuess(Game game)
{
int guess = getParameterInt("guess", -1);
if (guess < 0 || guess > 100)
{
mTemplate.setBlock("warning", "invalid");
return;
}
game.increaseGuesses();
if (game.getAnswer() < guess)
{
mTemplate.setBlock("indication", "lower");
}
else if (game.getAnswer() > guess)
{
mTemplate.setBlock("indication", "higher");
}
else
{
setOutput("gameid", getInput("gameid"));
exit("success");
}
}
public void processElement()
{
Game game = Contest.getGame(getInput("gameid"));
if (null == game)
{
exit("start");
}
mTemplate = getHtmlTemplate("guess");
if (hasSubmission("performGuess"))
{
handleGuess(game);
}
print(mTemplate);
}
}
------------------------------------------------------------------
위의 자바코드에서 우리는 몇가지 흥미로운 점을 볼수 있다. 먼저 processElement메소드 내부의 exit호출을 보자. 이 명령은 start출구와 연결된 Flow link를 활성화 한다. 요소안에서 더이상의 처리를 하지않고, 즉시 종료하여 로직이 Flow link가 가리키는 요소로 넘어간다. 이 예저를 통해, 게임ID를 발견하지 못했을 경우 시작페이지를 보여줌을 알 수 있다.
만약, 게임이 성공적으로 검색되었다면, 다음에는 현재요청이 "perform_guess"라는 submission을 포함하고 있는지 확인하게 된다. 그렇다면, handleGuess가 호출되고 출구명이 "success"로 설정되면 요소는 해당 출구로 빠져나와 결과를 출력 할것이다.
submission을 찾지 못한다면 기본 템플릿 페이지가 보여지며 사용자에게 숫자를 입력하라고 요청할 것이다.
- Specifying the starting point
사용자들이 처음부터 http://localhost:8080/03_numberguess/start/ 으로 들어온다면 대행이지만 대부분 http://localhost:8080/03_numberguess/ 까지만 입력할 것이다.
이경우 사이트(site)를 위한 arrival요소를 기술하여 해결할 수 있다. arrival 요소는 arrival태그로 어플리케이션의 루트를 설정하는데 사용된다. 마지막으로 이것을 추가하면 사이트파일은 아래와 같이 될것이다.
------------------------------------------------------------------
최종 사이트 파일
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<arrival destid="Start"/>
<element id="Start" file="start.xml" url="/start">
<flowlink srcexit="started" destid="Guess"/>
<datalink srcoutput="gameid" destid="Guess" destinput="gameid"/>
</element>
<element id="Guess" file="guess.xml" url="/guess">
<flowlink srcexit="start" destid="Start"/>
<datalink srcoutput="gameid" destid="Start" destinput="gameid"/>
<flowlink srcexit="success" destid="Success"/>
<datalink srcoutput="gameid" destid="Success" destinput="gameid"/>
</element>
<element id="Success" file="success.xml">
<flowlink srcexit="start" destid="Start"/>
</element>
</site>
------------------------------------------------------------------
- Wrapping it Up
지금까지 간단한 웹게임을 작성해 보았다. 이것이 RIFE의 프로세스를 배우는데 많은 도움이 되었기를 바란다. 우리는 흐름(flow)과 데이터(data)의 연결(link)를 정의하고 웹페이지 생성을 위해 템플릿(template)을 사용했다. 그리고, 폼을 통한 사용자 입력을 처리했다. 이제 복잡한 템플릿 작성(writing complex templates)과 같은 더 발전된 주제를 위해 옮겨갈 준비가 되었다.
RIFE 시작하기(3) - Number Guess 어플리케이션 (2/3)
데이터와 프레젠테이션층을 분리하기 위해 RIFE는 템플릿(template)을 사용한다. 템플릿은 일반적인 HTML파일과 함께 웹어플리케이션을 위한 구조화된 특별한 마크업(markup)을 사용한다. 여기엔 특별한 스크립팅언어가 포함되지 않으며 어떠한 로직도 포함시킬 수 없다. 템플릿은 단지 컨텐츠(값)를 위한 블럭과 위치지정만 가능할 뿐이다. 로직은 element에 있다.블럭은 생성되며 컨텐츠는 위치지정자 내부에 할당되거나 생성된다. block과 value태그는 브라우저로 보내지기 직전에 생성된 출력값으로 변환된다.
이것이 RIFE템플릿이 다른 솔루션들과의 차이점이다. JSP를 예를 들면, 페이지들은 어떤 HTML에디터에서난 편집될 수 있다. 왜냐하면 이 파일들은 올바른 HTML형식이기 때문이다. 이것은 당연히 전체 어플리케이션을 실행하지 않고도 쉽게 브라우저에서 확인해 볼수 있다.
Hello World 예제에서 모든 컨텐츠는 자바클래스 구현의 processElement메소드로부터 직접 출력되어 졌다. 만약 모든 컨텐츠가 이러한 방식으로 생성되어야 한다면 매우 성가신 일이 될것이다. 템플릿은 이보다 손쉬운 방법을 제시한다.
숫자맞추기 게임에는 두개의 템플릿이 있다. 하나는 맞추기 페이지(guess page)이고 다른 하나는 성공 페이지(success page)이다. guess템플릿의 일부를 빨리 보자.
-----------------------------------------------------------------
맞추기 페이지 템플릿
-----------------------------------------------------------------
<p>Guess a number from 0 to 100</p>
<!--V 'warning'--><!--/V-->
<!--B 'invalid'-->
<p><font color="#990000">The guess is invalid.</font></p>
<!--/B-->
<p><i><!--V 'indication'--><!--/V--></i></p>
<!--B 'lower'-->The answer is lower.<!--/B-->
<!--B 'higher'-->The answer is higher.<!--/B-->
-----------------------------------------------------------------
보시다시피, RIFE는 일반적인 HTML 주석을 사용한다. 여기에서, 템플릿 태그를 위한 다른 문법을 표현하는 V 와 B 두개의 태그를 볼 수 있다. V 는 값(value)을 위한 위치지정자이고 B 는 구역(block)을 정의한다. 자바 요소코드에서 문자열을 값(value)태그에 삽입하기 위해서는 다음과 같이 코딩한다.
-----------------------------------------------------------------
template.setValue("warning", "The answer is lower.");
-----------------------------------------------------------------
이 예제에서, 우리는 B 태그와 함께 정의한 block을 사용하고 컨텐츠의 값으로 이것들 중 하나를 값에 삽입했다.
-----------------------------------------------------------------
template.setBlock("indication", "lower");
-----------------------------------------------------------------
이 방식으로 우리는 한줄의 HTML코드도 자바코드에 포함시키지 않았으며 템플릿에는 어떤 로직도 포함되지 않았다. 이런 명확한 컨텐츠와 로직의 분리가 RIFE의 대단한 능력중 하나이다.
RIFE 시작하기(2) - Number Guess 어플리케이션 (1/3)
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Creating a more advanced RIFE application
- Laying out the structure
이제 가능한 간단한 어플리케이션 부터 시작해서 점점 현실적인 예제로 옮겨갈 것이다. 실세계에 우리는 더욱 동적인 어플리케이션을 필요로 한다.
지금부터 간단한 숫자 맞추기 게임을 이용해 실세계 예제를 좀 더 단순화 해보겠다. 게임은 사용자가 숫자를 추측하게 하고 맞출때 까지 계속 "too high" 또는 "too low"를 출력하며 반응할 것이다.
번호 맞추기 게임은 앞서 살펴본 RIFE예제 다운로드 파일에 포함되어 있다. HelloWorld 예제에서와 같이 Jetty와 RIFE를 설치하는 과정을 반복해 보자.
------------------------------------------------------------------
cd ~/tutorial/rife-examples-{version}
mkdir 03_numberguess/WEB-INF/lib
cp ROOT/WEB-INF/lib/rife-{version}.jar 03_numberguess/WEB-INF/lib/
mv 03_numberguess ../jetty-{version}/webapps/
------------------------------------------------------------------
Jetty를 재시작 한다음 웹브라우저에서 http://localhost:8080/03_numberguess/ 를 확인해 보자.
파일의 갯수가 증가 되었다. 진행하기 전에 디렉토리 구조를 살펴보자.
------------------------------------------------------------------
numberguess 예제의 디렉토리 구조
------------------------------------------------------------------
classes/
rep/
participants.xml
sites/
numberguess.xml
elements/
start.xml
guess.xml
success.xml
templates/
guess.html
success.html
implementations/
tutorial/
numberguess/
Start.java
Guess.java
Success.java
src/
tutorial/
numberguess/
backend/
Contest.java
Game.java
------------------------------------------------------------------
elements 디렉토리를 보면 세계의 XML파일을 볼 수 있다. 이들은 각 게임의 요소(element)를 나타낸다. elements들은 사이트(site) 정의에서 서로간의 실행흐름(flow)을 생성해서 연결된다. 이 흐름을 "Flow links"라 부르며 flowlink 태그로 생성된다. 만약, 각 element간에 데이터를 전달은 "Data links"를 사용하며 datalink 태그를 사용한다.
우리는 element를 다음과 같이 연결 하려고 한다.
[ Start->Guess->Success 흐름도 ]
링크(link)로 다는 요소(element)에 연결을 하려면 element는 반드시 하나 또는 그 이상의 "출구(exits)"를 제공해야 한다. 하나의 exit는 Flow links의 시작점이며 다른 지점에는 다른 element가 존재한다.
위의 그림에서 Data links는 파란선, Flow links는 검은선으로 표현되어 있다. START는 "started"라는 출구를 가짐을 알 수 있고, GUESS는 "start"와 "success"라는 두개의 출구를 가진다. 그리고 SUCCESS는 "start"라는 출구를 가진다. "started"라는 출구가 START라는 element로 부터 GUESS라는 element로 연결됨을 볼 수 있다.
- Linking elements
앞서 언급했듯, element는 다양한 출구를 가질 수 있으며 element가 각각 연결을 할 수 있도록 한다. 이 출구들은 element선언 파일에 정의되어 있다. elements/guess.xml 예제에서 "start"와 "sucess" 출구를 찾아 보자.
------------------------------------------------------------------
element를 위한 출구정의
------------------------------------------------------------------
<element implementation="tutorial.numberguess.Guess">
<exit name="start"/>
<exit name="success"/>
</element>
------------------------------------------------------------------
링크생성
세 요소 Start, Guess, Success 간의 관계는 sites/numberguess.xml 이라는 사이트파일에 정의되어 있다. 여기에 element들은 서로 연결되어 있고 사이트의 흐름이 정의되어 있다. 이 모든것은 적절한 flowlink태그로 완성된다.
------------------------------------------------------------------
Flowlinks 정의
------------------------------------------------------------------
<site>
<element id="Guess" file="guess.xml" url="/guess">
<flowlink srcexit="start" destid="Start"/>
<flowlink srcexit="success" destid="Success"/>
</element>
</site>
------------------------------------------------------------------
GUESS요소는 START요소로 향하는 연결과 SUCCESS요소로 향하는 연결을 가지고 있다. 흐름은 요소의 출구와 요소간에 항상 연결되어 있다. 그리고, 각 요소의 출구는 요소의 정의 파일에 반드시 기술되어 있어야 한다.
이 모든것은 사이트파일만 보면 사이트 내의 요소들간의 관계를 명확하고 쉽게 알 수 있도록 해준다. 이것은 웹어플리케이션을 개발하고 유지보수하는 동안 대단한 매우 큰 장점을 제공한다.
- Passing data between elements
Flow links 설정에 더해 우리는 각 요소간에 데이터를 전달할 필요가 있다. numberguess게임에서는 game ID를 사용한다. 이것은 각 게임을 구분하기 위한 구분자로 사용된다. 우리는 이 ID를 게임이 활성화 되었을ㄹ래 전달하기를 원한다. 이것은 모든 요소가 어떤 게임에 관여되었는지 알수 있게끔 한다. Data links는 이런 목적으로 사용되며 이 정의는 Flow links와 매우 유사하다.
입력과 출력 정의
요소로부터 전달받은 입력변수를 전달하려면 입력값들과 출력값들을 이 변수들에 정의되야 한다. 이것을 하기위해 element선언내에 input 과 output 태그를 사용한다.Guess 요소에 input과 output태그를 추가하면 아래와 같다.
------------------------------------------------------------------
입력과 출력 정의
------------------------------------------------------------------
<element implementation="tutorial.numberguess.Guess">
<input name="gameid"/>
<exit name="start"/>
<exit name="success"/>
<output name="gameid"/>
</element>
------------------------------------------------------------------
Data link 생성
실제로 하나의 출력 요소로부터 다른 입력 요소로 연결하려면 반드시 Data link를 생성해야 한다. 이 예제에서는 Guess요소의 "gameid"라는 출력으로 부터 Start요소의 "gameid"라는 입력으로 연결되었다.
------------------------------------------------------------------
<datalink srcoutput="gameid" destid="Start" destinput="gameid"/>
------------------------------------------------------------------
예제에서 Data link를 추가한 다음에는 아래의 결과를 얻을 수 있다.
------------------------------------------------------------------
GUESS요소에 Data link추가
------------------------------------------------------------------
<site>
<element id="Guess" file="guess.xml" url="guess">
<flowlink srcexit="start" destid="Start"/>
<datalink srcoutput="gameid" destid="Start" destinput="gameid"/>
<flowlink srcexit="success" destid="Success"/>
<datalink srcoutput="gameid" destid="Success" destinput="gameid"/>
</element>
</site>
------------------------------------------------------------------
datalink태그로 연결하는 동안 출력명을 반드시 입력명과 같을 필요가 없다는 점을 주의하라.
RIFE 시작하기(1) - Getting started.
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
- Installing RIFE
앞서 안급했듯이 RIFE는 자바 플랫폼에서 빌드되었으므로 RIFE어플레케이션을 실행하기 위해서는 JDK를 설치해야 한다. Java는 http://java.sun.com/ 에서 다운로드 할 수 있다.
또한, 서블릿을 지원하는 웹서버도 필요하다. 이 튜토리얼에서는 Jetty를 선택했다. Jetty는 설정이 간편하고 사용하기가 쉽다. 이것은 http://jetty.mortbay.com/ 에서 다운로드 할 수 있다.
RIFE는 RIFE 사이트 http://rifers.org/downloads 에서 받을 수 있다. 여기엔 몇가지 다른 패키지들 중 하나를 선택할 수 있다.
새로운 어플리케이션을 시작할때 기본 패키지(base package)는 좋은 출발점이 된다. 이것은 RIFE 프레임웍을 비롯해 웹어플리케이션을 시작하는데 필요한 모든것을 모두 포함하기 때문이다. 압축파일 내부의 디렉토리구조는 단지 권장사항을 뿐이다. 본인의 취향에 따라 얼마든지 변경 할 수 있다. 단, 권장위치에 있는 web.xml파일은 war포맷으로 패키징 하기 쉬우며 서블릿엔진에 설치되기 쉽게 한다. jar파일은 단지 RIFE만 포함되어 있다. 이것은 RIFE를 손쉽게 업그래이드 할 수 있게 하기위한 배려이다.
홈디렉토리 내부에 Jetty와 함께 웹어플리케이션 디렉토리를 설정하자.
--------------------------------------------------------------------
mkdir ~/tutorial
cd ~/tutorial
jar xf {download directory}/jetty-{version}.zip
--------------------------------------------------------------------
Debian 3.1(Sarge)을 사용한다면 다음 명령으로 Jetty를 설치할 수 있을 것이다. Jetty는 /usr/share/jetty 디렉토리에 설치될 것이다. Ubuntu와 같은 Debian계열의 리눅스라면 모두 마챤가지일 것이다.
--------------------------------------------------------------------
apt-get install jetty
--------------------------------------------------------------------
Debian에서 Jetty패키지를 설치했다면 서비스(service)가 자동으로 시작된다. 기본적으로 8280과 8079포트를 사용하게 된다.
이제 테스트 어플리케이션을 webapps 서브디렉토리에 설치하자.
--------------------------------------------------------------------
cd jetty-{version}
cd webapps
--------------------------------------------------------------------
RIFE배포판의 압축을 푼다음 웹어플레케이션을 위한 기본 디렉토리들을 설정해야 한다.
--------------------------------------------------------------------
jar xf {download directory}/rife-base-{version}.zip
mv rife-base-{version} my-application
cd my-application
cd WEB-INF
mkdir classes
cd classes
mkdir rep
mkdir sites
mkdir elements
mkdir templates
--------------------------------------------------------------------
지금까지 잘해왔다. 위의 디렉토리 구조를 그대로 활용하는것은 좋은 출발점이 될 것이다.
--------------------------------------------------------------------
cd ../../..
cp -r my-application ../../blueprint
------------------------------------------------------------------
이제 간단한 RIFE어플레케이션을 만들기 위한 조각들을 채워넣을 준비가 되었다. 그전에 간단히 웹어플레케이션과 RIFE에 관한 약간의 설명을 하고싶다.
RIFE zip파일의 내용을 보았다면 거기에 RIFE를 위한 자바 클래스파일들을 비롯해 web.xml파일을 보았을 것이다. 이렇게 패키징된 어플리케이션은 어떤 어플리케이션 서버 에서나 웹어플리케이션 디렉토리에 바로 배치되어 사용될 수 있다.
zip파일에서 찾을 수 없는 유일한 실제 웹어플리케이션의 실제 구현부분은 빠른시일내에 스스로 작성해야 할것이다.
그전에, 완성된 RIFE어플리케이션들을 실행해 보길 바란다. 우리는 RIFE 웹사이트에서 예제패키지(example package)를 다운로으 받아 사용할 수 있다. 이 패키지들은 실행 및 테스트 할 수 있는 많은 예제를 포함하고 있다. 첫번째 예제인 "Hello World" 어플리케이션은 다음의 과정을 따른다.
먼저, webapps디렉토리로 돌아가 예제압축파일을 풀도록 한다.
------------------------------------------------------------------
cd ~/tutorial
jar xf {download directory}/rife-examples-{version}.zip
cd rife-examples-{version}
------------------------------------------------------------------
RIFE를 웹어플리케이션의 lib디렉토리에 설치한다. jar파일을 RIFE사이트에서 다운로드받아 정확한 디렉토리에 넣어라.
------------------------------------------------------------------
mkdir 01_helloworld/WEB-INF/lib
cp ROOT/WEB-INF/lib/rife-{version}.jar 01_helloworld/WEB-INF/lib/
mv 01_helloworld ../jetty-{version}/webapps/
------------------------------------------------------------------
*주의사항*
jar파일을 서블릿엔진의 lib나 ext 또는 서블릿컨테이너의 공용라이브러리 영역에 넣지마라. 각 어플리케이션은 고유의 RIFE jar파일을 필요로 한다.
이제 어플리케이션을 시작하고 테스트할 수 있다.
------------------------------------------------------------------
cd ~/tutorial/jetty-{version}
java -jar start.jar
------------------------------------------------------------------
Debian사용자의 경우 위와같이 Jetty를 실행하면 log4j의 에러메시지를 보게 될것이다. Debian에서 Jetty를 실행하기 위해서는 아래와 같이 하라.
------------------------------------------------------------------
/etc/init.d/jetty start
------------------------------------------------------------------
Jetty를 실행할때 예외상황(Exception)을 보게될것이다. my-application웹어플리케이션은 추가 라이브러리들을 필요로 한다. 이것들은 잠시후에 추가하게 될것이다. 이 파트에서 중요한것은 HelloWorld예제를 살펴보는 것이다.
예제를 실행해보기 위해 웹브라우저에서 http://localhost:8080/01_helloworld/home 을 보라. "Hello world." 라는 메세지를 보게 될것이다.
다음 섹션에서는 이 예제에 대해 자세히 설명할 것이다.
- A simple RIFE application
모든 RIFE어플리케이션의 중심은 저장소(repository)이다. 이것은 설정데이터, 사용자, 데이터소스와 같은 각기 다른 어플리케이션의 영역을 유지시켜 준다. 이 영역들을 참가자(participants)라 부른다.
repository는 어플리케이션의 어디서나 이용가능하며 초기화와 어플리케이션 전반에 걸친 자원(resources)들에 접근할 수 있는 편리한 방법을 제공한다.
Hello World 예제에서 repository파일은 ~/tutorial/jetty-{version}/webapps/01_helloworld/WEB-INF/classes/rep/participants.xml 에 위치한다. 지금부터는 rep/participants.xml과 같이 앞의 경로 부분은 생략하고 표기하겠다.
------------------------------------------------------------------
Hello World를 위한 participants.xml
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">
<rep>
<participant param="sites/helloworld.xml">ParticipantSite</participant>
</rep>
------------------------------------------------------------------
이것은 정말 단순한 예제이다. 사이트를 위한 participant 하나만을 가지고 있다. param속성에 site 파일이 sites/helloworld.xml 에서 찾을 수 있음을 명시했다. 실제 어플리케이션 에서는 이보다 더 많은 participants를 가지게 될것이다.
한 사이트는 여러개의 요소(elements)들로 이루어 진다. element는 어플리케이션에서 가장작은 논리적 단위이다. Elements는 서로 연결되어 웹사이트의 흐름(flow)으로 표현된다.
이것은 전자부품들이 쓸모있으려면 서로 조립되어야 하는것과 마챦가지로 생각할 수 있다. 전자부품은 하나만으로는 아무런 쓸모가 없지만 다른것들과 결합되었을땐 놀라운 제품이 탄생할 수 있다.
같은 부품도 다른곳에 놓여지거나 순서를 달리할 수 있다. 이것은 극적인 변경을 부품의 파괴나 다른 새로운것을 생산하지 않고 가능하게 하는 기능을 구현할 수 있게 한다.
다시말해, 제품을 개선하기 위해 기존의 연결된 부품사이에 새로운 부품을 삽입해야 할 경우가 있다. 이것은 복잡한 작업이 아니다. 단지, 기존의 연결선을 제거하고 새로운 부품에 연결하기만 하면 된다. 당신을 골치아프게 하지 않고도 제품의 다른 기능들은 계속 작돌할 것이다.
RIFE의 element는 전자부품에 비유되며 flow는 연결선(wiring)에 비유할 수 있다. 어떤 어플리케이션의 element도 어느 어플리케이션에서나 손쉽게 연결시킬 수 있다.(rewiring)
각 element는 입력(input)과 출력(output)을 가질 수 있다. 이것들은 연결되며 데이터는 서로 연결고리에 따라 전달될 것이다.
sites/helloworld.xml 에서와 같은 사이트 구조를 보면 HELLOWORLD라는 하나의 element만 포함한 것을 보게될 것이다. 이것은 /home 이라는 URL로 매핑되어 있다.
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<element id="HELLOWORLD" implementation="tutorial.helloworld.HelloWorld" url="home"/>
</site>
------------------------------------------------------------------
첫번째 예제가 단지 하나의 element만 가지므로 거기엔 입력이나 출력, 연결고리가 없다. 그러므로, 전체 element가 site파일 안에 포함되어 있다. 이 경우를 제외하고 재사용가능한 element를 생성하려면 이것을 고유한 xml파일로 분리해야 한다. 이것에 대해서는 다음예제에서 자세히 다루기로 하자.
여기에서 지금 주목해야 할 부분은 element태그의 implementation 속성이다. element의 구현(implementaion)은 자바클래스 이거나 자바소스일 수 있으며 자동으로 컴파일될 것이다. 또는 Groovy와 같은 지원 가능한 자바 스크립팅 언어일 수도 있다.
현재 구현부는 단순히 인사말을 출력하는 자바 클래스 이다. 이것은 HelloWorld.java에 구현되어 있다.
------------------------------------------------------------------
Hello World 구현체
------------------------------------------------------------------
package tutorial.helloworld;
import com.uwyn.rife.engine.Element;
import com.uwyn.rife.template.Template;
public class HelloWorld extends Element
{
public void processElement()
{
Template template = getHtmlTemplate("tutorial.helloworld");
template.setValue("hello", "Hello world.");
print(template);
}
}
------------------------------------------------------------------
요소(element) 구현체(implementation)는 반드시 com.uwyn.rife.engine.Element 클래스를 상속하고 processElement()메소들를 구현해야 한다. 보는바와 같이, 이 요소(element)는 템플릿을 이용하여 "Hello world."를 출역한다. 템플릿에 대해서는 뒤에 자세히 다루자.
위 자바코드는 tutorial폴더에 정의된 helloworld란는 템플릿(HTML파일)을 사용한다.
------------------------------------------------------------------
helloworld 템플릿(HTML)
------------------------------------------------------------------
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title><r:v name="hello"/></title>
</head>
<body>
<r:v name="hello"/>
</body>
</html>
------------------------------------------------------------------
RIFE는 element 정의를 자바코드와 연결하여 템플릿에 전달할 책임이 있다.
다음에는 number guess 어플리케이션에 대해 살펴 보겠다.
RIFE에 대한 소개글은...
다음 포스팅은 RIFE의 사용법에 대한 강좌들로 역시 이전 개일 블로그에서 옮겨온 내용들입니다.
그리고는 이 두가지를 병행해서 계속 포스팅할 것입니다.
RIFE 소개 (6) - Acceptable session support
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Acceptable session support
사람들은 왜 RIFE가 전통적인 세션에 직접 접근하는 방법을 제공하지 않는지 질문한다. 지금부터 우리가 어떻게 현재의 솔루션에 도달했는지 상세한 설명을 할것이다.
전통적인 세션의 문제점.
먼저, 왜 RIFE가 전통적인 세션에 접근하는것을 막았는지 이유를 알아보자
세션은 어플리케이션 실행 흐름과 동기화되지 않는다.
하나의 세션 아이디는 어플리케이션 어디에서나 가져다 수정할 수 있는 상태정보를 포함한다. 방문자는 뒤로가기 버튼이나 페이지 북마크를 누를때 어플리케이션의 실행흐름중 이전 위치를 가져와 같은 세션아이디를 가진다. 그러나, 실제 어플리케이션은 다음 위치의 세션 데이타를 사용한다. 이것은 예측할수 없고, 원하지 않은 상태를 만들 수 있다.
세션을 휴지통처럼 되려는 경향이 있다.
코더들은 단지 나중에 필요할 것 같은 모든것들을 세션에 던져 넣으려는 경향이 있다. 그러나, 한번 이것이 생성되면, 이것을 언제 버려야 할지 결정하기 매우 어려워 진다. 이런 상황은 가볍게 보이지 않는다. 사이트의 어디에서 어떤 세션 파라미터를 필요로 할지 알수 없으므로 늘 모든 값이 붙어 다니는 경향이 있다.
비록 몇개월 뒤, 모든것을 기억 하지 못할때 한가지가 세션에 남아있다면 가장 빠른 해결책은 충돌이 발생하지 않는것을 보장하는 수상한 이름을 찾기 시작하는 것이다. 따라서, 개발자가 철저히 방법론에 따라 세션의 영향과 컨텐츠를 적당히 문서화 하지 않는다면 무서운 혼란을 초래할 것이다.
세션은 의미없는 URL들을 생성한다.
이것은 매우 작은 관점이다. 그리고, 어떤 서버 기반 상태 보관 엔진으로도 이 문제를 피하는것은 현실적으로 불가능하다. 이것이 진짜 문제가 아니다. 진짜 문제는 RIFE없이 일반적인 질의 파라미터를 통해 데이타를 주고 받는것은 매우 성가신 일이란 것이다. 따라서, 세션은 데이타를 가져오는데 사용되나 사실은 URL을 통해 전달된다.
전통적인 세션의 해택들
하지만 세션은 서버측 상태를 보관하는 솔루션으로써 몇가지 장점들을 가진다.
세션은 클라이언트로 부터 전송된 데이터를 숨긴다.
방문자에게 보안상 또는 기타 민감한 이유로 전송된 데이터를 보이지 않게 하는것이 중요한 어플리케이션이 있다. 신용카드 번호를 하나 받았다고 가정하자. 실제 이 번호가 사용되기 전에 몇단계의 부가적인 절차가 필요하다. 이 번호가 단계마다 계속 전달되어 그 값이 다른 누군가에게 북마크 되거나 캐쉬에 저장될 수 있다은 것은 끔찍한 일이다. 임시 데이터베이스 저장 메커니즘을 이용하여 수동적으로 풀수도 있지만 이것은 분명히 개발자에게 친숙하지도 않고 시간을 줄여줄 수도 없다.
세션은 대량의 데이타가 전달 되는 것을 전달 되는것을 방지한다.
파라미터를 쿼리스트링이나 폼객체로 전달하는 것은 생각처럼 가벼운 일이 아니다. 이것은 대역폭을 소비하고 반복되어 전달될 필요가 있다. 많은 양의 데이터를 이렇게 전달하다 보면 문제가 된다. 또한, URL은 길이에 제한이 있다. 만일 이 제한에 걸리면 어플리케이션이 정상적으로 작동하지 않을 수 있다.
세션은 상태의 변화 과정을 쉽게 핸들링 할 수 있게 도와준다.
웹사이트에 데이터를 입력시 마법사형식의 접근이 필요한 어플리케이션을 개발한다고 가정하자. 사용자에게 각 단계마다 필요한 데이터를 입력하도록 해야 할것이다. 비록 RIFE의 빈을 이용해 자동으로 엔트리에 데이터를 채운다 하더라도 시작부터 모인 모든 데이터가 각 단계 마다 모든 데이터가 확인(validate)되어야 한다. 자체적으로 확인하는 메커니즘의 빈(validated bean)을 이용하는 것은 이 작업을 좀 더 쉽게 하긴 하지만 대부분의 개발자에게 친숙하지 않을 뿐더러 일부 중복된 코딩을 요구한다. 세션은 받은 데이터를 서버에 저장할 수 있게 해주고 서버에 저장된 데이터는 클라이언트로 부터 수정되지 않았음을 보장할 수 있다. (그러므로, 각 요청마다 모든 데이터를 확인할 필요가 없다.)
RIFE의 해결책
디음의 사항을 주의깊게 보라 :
* RIFE는 데이터의 흐름에 관해 전혀 여지를 남기지 않는다. 따라서, 이것은 문제를 야기하거나 세션을 불가능한 상태로 남겨두지 않는다. 데이터는 필요할때 필요한 정보만 전송되어 질 것이다.
* RIFE는 데이터 링크 뿐만 아니라 전역변수 사이에서 데이터의 전송을 쉽게 한다. 이것은 섬세하고 간편한 개발 환경을 제공하여 개발자의 간섭을 최소화 한다.
* RIFE는 웹 연속성을 지원한다. 이것은 상태의 변화를 매우 쉽게 제어한다.
* RIFE는 클라이언트를 통해 기본적으로 모든것을 전송한다. 그리고 URL또는 폼객체가 어플리케이션을 제어하도록 한다.
* RIFE는 모든 데이터 흐름 기능을 이용하는 동안 설정가능한 상태 저장 메커니즘을 통해 서버측에서 상태 저장을 허락한다. 이것은 민감하고 대량의 데이터가 전송되어지지 않도록 하는것을 허락한다.
RIFE 소개 (5) - Web application engine.
http://rifers.org 에서 발췌한 내용을 번역 하였습니다.
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
웹 어플리케이션 엔진
RIFE웹 엔진은 생산성에 해를 끼치지 않고도 유지보수를 용이하게 하는 솔루션을 제공한다. 이것은 개발자에게 큰 부담을 주지 않고 명확하게 구조화된 어플리케이션의 생성을 보강하도록 설계되었다. 사실, 사이트 구조에서 모든 상태 전이를 선언한것 덕분에 웹엔진과 모든 개발자들이 어플리케이션의 전체 로직과 데이터의 흐름에 대한 지식을 가지게 되었다. 이것은 유일무이한 기능을 제공할 것이다. 예를 들면 다음과 같다.
* 모듈화된 어플리케이션 구축. 이것은 이진(binary) 라이브러리처럼 재사용 될 수 있고 빠르게 새로운 솔루션을 개발할 수 있도록 한다.
* 페이지내의 특정영역을 고립시켜 독립적이고 어디서나 내장가능한 요소로 활용 할 수 있다.
* 팀내에서 작업할때 웹 어플리케이션 전체에 대한 상세한 문서를 가질 필요가 없다.
* 존재하는 어플리케이션에 새로운 단계를 삽입하고자 할때 기존의 소스코드를 고칠필요가 없다.
* 변경되거나 지역화하된 URL을 적용하기 위해 모든 소스코드를 고칠 필요가 없다.
* 몇몇 복잡한 어플리케이션 흐름을 각기 맨위에 놓도록 한다.
* 같은 어플리케이션 내에서 다른 JVM을 사용할 수 있다.
* 기타 등등...
RIFE의 사이트 구조화는 웹엔진과 소스코드의 변화가 발생했을때 어플리케이션을 재시작하지 않고도 변화된 부분이 즉시 반영되도록 한다. 개발자들기 소스코드를 복제하지 않고 사이트구조 정보에 의존하는것을 쉽게 함으로써 모든것이 한 장소에 집중되는것을 보장할 수 있다.
RIFE의 웹엔진은 HTTP가 제한적으로 가능했던것 처럼 클라이언트측에 상태 보관 기능을 제공한다. 사이트 구조 덕분에 엔진은 컨텍스트와 목적에 따라 상태보관을 위해 질의와 쿠키, 웹연속성 그리고 세션을 조합하여 사용할 수 있게 되었다.
RIFE 소개 (4) - Features
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
기능
웹 어플리케이션 엔진
통합된 웹 연속성
IoC 지원
컨테이너 외부 테스팅
컨텐츠 관리 프레임워크
양방향 다중포맷 템플릿 엔진
템플릿 내용 변환
집중된 메타데이터의 용이함
인증 프레임워크
JDBC 추상화 레이어
데이터베이스 질의 빌더
영속 레이어
환경설정(Configuration) 프레임워크
중앙 집중형 어플리케이션 생명주기 관리
Cron형식의 스케쥴러
비동기 메일 큐
컨텐츠 배급(syndicate) 프레임워크
자료 추상화
웹 서비스 표준 지원
RIFE 소개 (3) - Why RIFE's web engine?
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Why RIFE's web engine?
RIFE의 설계는 최고의 요청기반(request-based)과 컴포넌트기반의 접근성을 제공하며 동일한 컴포넌트 객체 모델(consistent component object model)상에서 서로 혼합되어 사용될 수 있다. 웹 엔진은 생산성을 양보하지 않고도 유지보수를 할 수 있는 솔루션을 제공한다. 중앙집중식 사이트 구조를 통해 어플리케이션은 쉽게 재사용 가능한 실행모듈로 쪼개진다. 이 모듈들은 그대로 다른 프로젝트에 통합될 수 있다.
현재 웹개발 프레임워크는 두가지 종류로 구분된다 :
요청기반(request-based)과 컴포넌트기반(component-based).
요청기반 프레임워크는 이전의 CGI스펙과 매우 가깝다. 이것은 들어오는 요청들을 직접처리하는 컨트롤러들과 액션들을 이용한다.각 요청은 기본적으로 무상태(stateless)이다. 서버기반 세션 명령으로 한정된 범위의 상태유지를 구현하기도 한다. 각 프레임워크들은 로직을 URL과 매핑하고 데이터를 구성하여 개발자에게 제공되는 방법에 따라 차별화한 된다.
컴포넌트기반의 프레임워크는 개발자로 하여금 요청처리의 내부로 부터 멀리하고 종종 웹과 상관없이 로직을 캡슐화(encapsulate)하여 재사용한 컴포넌트에 적용함으로써 추상화한다. 상태는 데이터를 기반으로한 각 컴포넌트의 인스턴스의 형태로 프레임워크에 의해 자동으로 유지된다. 이벤트 처리의 특정 형태와 함께 이 개발 모델은 데스크탑 GUI 툴킷에 의해 제공되는 기능들과 매우 유사하다. 각 프레임워크들은 제공되는 컴포넌트 API와 컴포넌트들이 서로 어떻게 결합되는지에 따라 차별화 된다.
RIFE는 요청기반 모델의 모든 데이터 제어와 로직 흐름를 획득하여 이 둘을 결합한다. 개발자는 CGI어플레케이션 구조에 가깝게 남겨지고 주소, 폼, 파라미터, 쿠키와 경로정보들을 이용해 모든 제어권을 가진다.
매핑액션과 컨트롤러가 직접 요청을 처리하는 대신에 RIFE는 컴포넌트 객체 모델을 제공한다. 이것은 개별 페이지, 요청 가로채기, 포탈과 같은 페이지 조각과 통합가능한 위젯들 처람 많은 다양한 상황에서 독자적인 형태를 띈다. 컴포넌트들은 서로 연결될 수 있으며 그룹으로 포장(packaged)된다. 그것들은 분할하여 배포되고 연속적으로 다른 RIFE프로젝트에 통합될 수 있다. 이것은 컴포넌트기반의 재사용가능한 형태를 제공하면서도 요청기반접근의 저수준제어를 함께 제공한다.