Raible's Wiki

Raible Designs
Wiki Home
News
Recent Changes

AppFuse

Homepage
  - Korean
  - Chinese
  - Italian
  - Japanese

QuickStart Guide
  - Chinese
  - French
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish
  - Japanese

User Guide
  - Korean
  - Chinese

Tutorials
  - Chinese
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish

FAQ
  - Korean

Latest Downloads

Other Applications

Struts Resume
Security Example
Struts Menu

Set your name in
UserPreferences

Edit this page


Referenced by
Articles
Articles_ko
CreateActions_ko
CreateDAOiBATIS_ko
CreateManager_ko
LeftMenu




JSPWiki v2.2.33

[RSS]


Hide Menu

CreateDAO_ko


Part I: AppFuse에서 새로운 DAO들과 Object들을 작성하기 - table을 나타내는 Java Object와 이 Object들을 데이타 베이스에 저장 시키는 Java 클래스들을 만드는 방법.

About this Tutorial

이 튜토리얼에서는 데이타베이스에 새로운 테이블을 만드는 방법과 그 테이블에 접근하는 자바 코드를 작성하는 방법을 알려줍니다.

우리는 어떤 객체와 그 객체를 데이타베이스에 persist (save/retrieve/delete) 하기 위한 몇가지 클래스들을 만들것입니다. 자바에서는 이 객체를 Plain Old Java Object (a.k.a. a POJO) 라고 부릅니다. 이 객체는 기본적으로 데이타베이스 테이블을 표현합니다.

우리가 만들 몇가지 클래스들은 다음과 같습니다:

  • Data Access Object (a.k.a. a DAO) Interface 와 그 Interface의 Hibernate 구현
  • DAO 를 테스트 하기 위한 JUnit class

NOTE: 만약 당신이 MySQL을 사용하면서 트랜젝션 처리를 원한다면 반드시 InnoDB 테이블을 사용하여야 합니다. InnoDB를 사용 하려면 mysql 설정 파일(/etc/my.cnf or c:\Windows\my.ini)에 다음과 같은 라인을 추가합니다. 두번째 설정(UTF-8 character set) 은 mysql 4.1.7+ 버젼에서 필요한 것입니다.

[mysqld]
default-table-type=innodb
default-character-set=utf8
만약 PostgreSQL 디비를 사용하면서 batch processing 에 관련된 에러가 나게 되면 src/dao/**/hibernate/applicationContext-hibernate.xml 파일에 <prop key="hibernate.jdbc.batch_size">0</prop> 라인을 추가해서 batch processing기능을 끄도록 합니다.

AppFuse 는 persistence layer 로 Hibernate 를 사용 합니다. Hibernate 는 자바 객체와 데이타베이스 테이블을 관련짓게 하는 Object/Relational (O/R) Framework 로써 자바 객체에 CRUD (Create, Retrieve, Update, Delete) 기능을 매우 쉽게 다루게 합니다.

자 그럼 Object, DAO 그리고 Test 를 작성해 봅시다.

목차

  • [1] Object 를 만들고 XDoclet 태그를 추가합니다.
  • [2] Ant 를 이용하여 Object 로 부터 데이타베이스 테이블을 만듭니다.
  • [3] JUnit 으로 DAO 를 테스트 하기위해 DaoTest 클래스를 만듭니다.
  • [4] Object 의 CRUD 처리를 위한 DAO 클래스를 만듭니다.
  • [5] Person object와 PersonDao를 Spring 프레임워크에 알리기 위해 설정파일을 구성합니다.
  • [6] DaoTest 를 실행시합니다.

Object 를 만들고 XDoclet 태그를 추가합니다 [#1]

먼저 persist 할 object 를 작성합니다.
id 와 firstName 그리고 lastName 속성을 가지고 있는 간단한 "Person" 객체를 작성합니다.(in the src/dao/**/model directory)


package org.appfuse.model;

public class Person extends BaseObject {
  private Long id;
  private String firstName;
  private String lastName;

    /*
     Generate your getters and setters using your favorite IDE: 
     In Eclipse:
     Right-click -> Source -> Generate Getters and Setters
    */
}

이 클래스는 3개의 abstract method (equals(), hashCode() and toString())를 가지고 있는 BaseObject 를 상속 받았고 이것은 Person class에서 반드시 구현 되어져야 합니다.
이중 두 메소드(equals(), hashCode())는 하이버네이트에 의해 요구되어지는 메소드 입니다.
이 메소드들을 추가 하는 가장 쉬운 방법은 Commonclipse 를 사용하는 것입니다. 이 툴의 사용법은 Lee Grey's site 에서 찾아볼 수 있습니다.
다른 이클립스 플러그인으로는 Commons4E 가 있습니다.

만약 IntelliJ IDEA를 사용 한다면 equals() 과 hashCode() 메소드는 만들어 낼 수 있지만 toString() 은 기본적으로 제공되지 않으므로 ToStringPlugin 을 사용 하시면 될겁니다. 전 개인적으로 이 플러그인을 사용해 보지는 않았습니다.

이제 우리가 만든 POJO 클래스에 대한 Hibernate 맵핑 파일을 만들기 위해 XDoclet 태그를 추가합니다.
이 맵핑 파일은 Hibernate 가 objects → tables 그리고 properties (variables) → columns 을 맵핑하기 위해 사용 됩니다.

첫째로, Hibernate 에게 이 object 와 관련된 데이타베이스 테이블이 무엇인지 알려주기 위하여 @hibernate.class 태그를 추가합니다:


/**
 * @hibernate.class table="person"
 */
public class Person extends BaseObject {

다음으로 primary key 맵핑 태그를 추가해합니다. 모든 @hibernate.* 태그는 반드시 POJO 클래스의 getters' Javadoc 에 적어야 한다는걸 기억하세요


    /**
     @return Returns the id.
     * @hibernate.id column="id"
     *  generator-class="increment" unsaved-value="null"
     */

    public Long getId() {
        return this.id;
    }

다른 데이타베이스에서 "native"를 사용할때의 문제들 때문에 나는 generate-class="native" 대신 generator-class="increment" 를 사용했습니다. 만약 당신이 오직 MySQL 만을 사용한다면 "native" 를 사용할 것을 권장합니다. 이 튜토리얼에서는 increment 를 사용합니다.

Ant 를 이용하여 Object 로 부터 데이타베이스 테이블을 만듭니다 [#2]

이제 콘솔에서 "ant setup-db" 를 실행해서 person 데이타베이스 테이블을 만들 수 있습니다. "ant setup-db" 태스크는 Person.hbm.xml 파일과 "person." 테이블을 만듭니다. 태스크를 실행 시킴으로써 Hibernate 가 만드는 테이블 스키마를 볼 수 있습니다:
[schemaexport] create table person (
[schemaexport]    id bigint not null,
[schemaexport]    primary key (id)
[schemaexport] );

만약 Hibernate 가 만들어준 Person.hbm.xml 파일을 보고 싶다면 build/dao/gen/**/model 디렉토리를 보십시오 여기 Person.hbm.xml의 내용이 있습니다.


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 2.0//EN" 
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>
    <class
        name="org.appfuse.model.Person"
        table="person"
        dynamic-update="false"
        dynamic-insert="false"
    >

        <id
            name="id"
            column="id"
            type="java.lang.Long"
            unsaved-value="null"
        >
            <generator class="increment">
            </generator>
        </id>

        <!--
            To add non XDoclet property mappings, create a file named
                hibernate-properties-Person.xml
            containing the additional properties and place it in your merge dir.
        -->

    </class>

</hibernate-mapping>

이제 다른 columns (first_name, last_name) 을 위한 추가적인 @hibernate.property 태그를 작성합니다:


    /**
     * @hibernate.property column="first_name" length="50"
     */
    public String getFirstName() {
        return this.firstName;
    }

    /**
     * @hibernate.property column="last_name" length="50"
     */
    public String getLastName() {
        return this.lastName;
    }

위 예제에서 column 속성을 기술한 이유는 데이타베이스 컬럼 이름과 POJO 클래스의 속성 이름이 다르기 때문입니다. 만약 같다면 column 속성을 기술할 필요가 없습니다.
이 외에 이 태그에서 사용할 수 있는 다른 속성들에 대해 알고 싶으면 @hibernate.property 를 참조하세요.

새로 만든 속성들을 데이타베이스 테이블의 column 으로 추가하기 위하여 "ant setup-db" 을 다시 실행 합니다.

[schemaexport] create table person (
[schemaexport]    id bigint not null,
[schemaexport]    first_name varchar(50),
[schemaexport]    last_name varchar(50),
[schemaexport]    primary key (id)
[schemaexport] );
만약 column 의 사이즈를 바꾸고 싶다면 @hibernate.property 태그에 length="size" 속성을 기술해 주면 됩니다. 또 필수항목(NOT NULL)으로 설정하고 싶다면 not-null="true" 속성을 기술해 주면 됩니다.

JUnit 으로 DAO 를 테스트 하기위해 DaoTest 클래스를 만듭니다 [#3]

The AppGen Tool 이 튜토리얼의 나머지 부분의 모든 클래스들을 자동으로 생성해 주는 AppGen 툴을 AppFuse 버젼 1.61 의 일부로 포함 시켰습니다. 이 툴은 Lance LavandowskaBen Gill으로 부터 기증받은 것을 토대로 만들어 졌습니다.

처음에 나는 tables/pojos, DAOs와 Managers 들의 관계를 1-to-1 으로 만들게 하는 이와 같은 코드 생성 툴을 추가하고 싶지 않았었습니다. 대부분 나의 프로젝트에서는 POJOs 보다 DAOs 와 Managers 들의 수가 훨씬 적었습니다.

기본적으로 AppGen 은 오직 Actions/Controllers와 그 테스트, 그리고 테스트 데이타, i18n 키값들 그리고 JSP 들을 만듭니다. 또한 이 툴은 Actions/Controllers을 구성(설정)해줍니다.
이 툴은 생성되는 파일의 수를 줄이기 위해 포괄적인 BaseManager 와 BaseDaoHibernate 클래스를 사용합니다.
(즉, 여러 POJO에 대해 이 툴을 사용 하더라도 한개의 BaseManager 과 한개의 BaseDaoHibernate 를 사용한다는 뜻인듯.)
그러나 여러분이 가끔은 모든 DAO 와 Manager 클래스들을 만들기 원하는걸 알았기 때문에, 나는 그 기능을 옵션으로 추가 했습니다.

이 툴을 사용하기 위해서는 다음의 순서를 따라해 주세요:

  1. POJO 를 model 디렉토리에 작성하고 applicationContext-hibernate.xml 파일에 매핑 파일(*.hbm.xml)을 [설정]합니다.
  2. extras/appgen 디렉토리로 이동한뒤 "ant -Dobject.name=Person -Dappgen.type=pojo" 태스크를 실행시킵니다. 이 예제에서, Person 클래스는 반드시 model 패키지에 존재해 있어야 합니다. 이 태스크는 이 튜토리얼의 모든 파일들을 만들어 줍니다.
  3. "ant install -Dmodel.name=Person -Dmodel.name.lowercase=person" 을 실행하면 모든 파일들이 인스톨 됩니다.(기존 파일의 수정도 이루어짐)
    WARNING: 이 태스크를 실행하기전에 지금까지 작업한 내용을 백업해 두세요 - 아니면 적어도 소스코드저장소에 올리기 전에 제대로 돌아가는지 기존코드에는 이상이 없는지 등을 확인해 보세요. 내가 이 태스크를 테스트 했을때는 아주 잘 작동한다고 판단했습니다만 이 태스크는 여러분의 소스트리를 수정한다는것을 알아두세요.

The reason for the "lowercase" parameter is to rename the JSPs to begin with a lowercase letter. If I tried to rename them and change the filename programmatically, it took 1MB worth of BSF and Rhino JARs (+5 lines of code) and this just seemed easier. Speaking of JSPs - it's up to you to modify the *Form.jsp and make it look pretty. This is covered in Step 5 of each respective web framework's "Create Action/Controller" tutorial: Struts, Spring and WebWork.

NOTE: If you'd like to generate all the DAOs/Managers/Tests, run "ant install-detailed" instead of "ant install". Before you install anything, the files will be created in the extras/appgen/build/gen directory (in case you want to look at them before installing). If you just want to test the tool, you can cd to this directory and run "ant test" to see the contents of these tutorials created.

I encourage you to read these tutorials even if you decide to generate all your code. That way you'll understand what's being generated for you and you'll only need to mailing list for asking smart questions. ;-) Hopefully this tool will remove the pain of writing simple CRUD code and let you concentrate on developing your business logic and fancy UIs!

이제 DAO 클래스를 테스트 하기 위한 DaoTest 클래스를 작성합니다.
"잠깐! 우리는 DAO 클래스를 만든적이 없자나" 라고 하는 소리가 들리는군요.
맞습니다.
그러나 나는 Test-Driven Development 이 보다 높은 퀄리티를 가지는 소프트웨어를 만들어 낸다는 것을 알았습니다.
몇년간 나는 클래스를 작성하기 전에 테스트를 먼저 작성하라는 것은 바보같은 짓이라고 생각했었습니다.
그러나 나는 그렇게 해 보았고 그 결과 그것이 훌륭하게 작동되는 것을 알았습니다.
내가 지금 test-driven 을 한는 이유는 그것이 소프트웨어 개발 프로세스의 속도를 무척 빠르게 해준다는걸 알았기 때문입니다.

시작하기 위해서, test/dao/**/dao 디렉토리에 PersonDaoTest.java 를 만듭니다. 이 클래스는 같은 패키지에 들어있는 BaseDaoTestCase 클래스를 확장해야 합니다. 이 부모 클래스는 Spring's ApplicationContext을 읽어 들이는 것과 *Test.class 와 같은 이름을 가지는 .properties 파일 (ResourceBundle)을 자동으로 읽어들이는데 사용 됩니다. 이 예제에서는 PersonDaoTest.properties 파일을 PersonDaoTest.java 와 같은 디렉토리에 놓으면 이 파일에 기술된 속성들은 rb 변수에 의해 사용될 수 있게 됩니다.

나는 주로 이미 만들어 놓은 test (i.e. UserDaoTest.java) 를 copy (열기 → 다른이름으로 저장) 한 후에 find/replace 기능을 이용해 [Uu]ser 를 [Pp]erson(object name)으로 바꿉니다.


package org.appfuse.dao;

import org.appfuse.model.Person;
import org.springframework.dao.DataAccessException;

public class PersonDaoTest extends BaseDaoTestCase {
    
    private Person person = null;
    private PersonDao dao = null;

    protected void setUp() throws Exception {
        super.setUp();
        dao = (PersonDaoctx.getBean("personDao");
    }

    protected void tearDown() throws Exception {
        super.tearDown();
        dao = null;

    }

}

JUnit 테스트를 위해서 PersonDao 를 initializes(초기화) 하고 destroys (소멸) 하는 기본적인 코드입니다. "ctx" object 는 Spring's ApplicationContext 입니다. 이것은BaseDaoTestCase 클래스의 static block 에서 초기화 됩니다.

자 이제는 DAO의 CRUD(create, retrieve, update, delete) 메소드를 테스트를 해야 합니다. 이 테스트를 위해서 우리는 test(모두 소문자)로 시작하는 메소드를 만들어야 합니다. 이 메소드가 Ant build.xml 파일의 <junit> 태스크에 의해 불려 질려면 몇가지 규칙이 필요합니다.

  • 메소드 이름이 test(모두 소문자)로 시작해야 한다
  • public 메소드여야 한다.
  • 리턴타입이 void 이여야 한다.
  • 전달 인자가 없어야 한다.

여기 CRUD 테스트를 위한 간단한 테스트 코드가 있습니다. 중요한 것은 각각의 메소드들은 자발적으로 일어난다는 점 입니다. 다음의 메소드들을 PersonDaoTest.java 파일에 추가 합니다:


    public void testGetPerson() throws Exception {
        person = new Person();
        person.setFirstName("Matt");
        person.setLastName("Raible");

        dao.savePerson(person);
        assertNotNull(person.getId());

        person = dao.getPerson(person.getId());
        assertEquals(person.getFirstName()"Matt");
    }

    public void testSavePerson() throws Exception {
        person = dao.getPerson(new Long(1));
        person.setFirstName("Matt");

        person.setLastName("Last Name Updated");

        dao.savePerson(person);

        if (log.isDebugEnabled()) {
            log.debug("updated Person: " + person);
        }

        assertEquals(person.getLastName()"Last Name Updated");
    }

    public void testAddAndRemovePerson() throws Exception {
        person = new Person();
        person.setFirstName("Bill");
        person.setLastName("Joy");

        dao.savePerson(person);

        assertEquals(person.getFirstName()"Bill");
        assertNotNull(person.getId());

        if (log.isDebugEnabled()) {
            log.debug("removing person...");
        }

        dao.removePerson(person.getId());

        try {
            person = dao.getPerson(person.getId());
            fail("Person found in database");
        catch (DataAccessException dae) {
            log.debug("Expected exception: " + dae.getMessage());
            assertNotNull(dae);
        }
    }

testGetPerson 메소드에서 우리는 person 을 생성 한 후에 get 메소드를 호출했습니다. 나는 주로 내가 항상 사용할 수 있는 레코드를 데이타 베이스에 넣습니다.
그러나 데이타베이스 테스트를 위하여 DBUnit 이 사용된 이후로는 test 를 실행하기 전에 metadata/sql/sample-data.xml 파일에 간단히 추가할 레코드 정보를 입력해 놓으면 됩니다:

<table name='person'>
    <column>id</column>
    <column>first_name</column>
    <column>last_name</column>
    <row>
      <value>1</value>
      <value>Matt</value>
      <value>Raible</value>
    </row>
</table>
이것이 testGetPerson 메소드에서 Person 미리 만들어 놔야 하는 코드를 없앨 수 있는 하나의 방법입니다. 만약 SQL 이나 GUI 툴 등을 이용하여 직접 데이타베이스에 레코드를 넣고 싶다면 "ant db-export" 태스크를 실행 시킨 후 "cp db-export.xml metadata/sql/sample-data.xml" 함으로서 sample-data.xml 새로 만들 수 있습니다.

위 예제에서 우리는 객체를 저장하기 전에 그 값들을 설정하기 위하여 person.set*(value) 를 호출하였습니다. 위 예제에서는 무척 간단했지만(속성이 몇개 없기 때문에) 만약 10개의 필수 필드(not-null="true")를 가지는 개체를 저장한다면 이는 보통 성가신 일이 아닐겁니다. 이것이 내가 BaseDaoTestCase 에서 ResourceBundle 을 생성했던 이유입니다. 간단히 PersonDaoTest.java 와 같은 디렉토리에 PersonDaoTest.properties 파일을 넣고 이 파일에 속성 값들을 설정합니다.

firstName=Matt
lastName=Raible
그러고 나서 person.set* 메소드 대신 BaseDaoTestCase.populate(java.lang.Object) 을 호출 함으로서 object의 값들을 설정할 수 있습니다.


person = new Person();
person = (Personpopulate(person);

아직 PersonDao.class 를 만들지 않았기 때문에 PersonDaoTest 클래스는 컴파일 되지 않을겁니다. 우리는 지금 그것을 작성해야만 합니다. PersonDao.java 는 interface 입니다. 그리고 PersonDaoHibernate.java 는 그 인터페이스의 Hibernate 를 이용한 구현입니다. 자 이것들을 만들어 봅시다.

Object 의 CRUD 처리를 위한 DAO 클래스를 만듭니다 [#4]

첫째로 구현 클래스들을 위한 기본적인 CRUD 메소드들을 정의한 PersonDao.java interface 를 src/dao/**/dao 에 만들고 CRUD 메소드를 정의합니다. 나는 요점만 보여주기 위하여 아래 클래스에 JavaDoc 들을 삭제했습니다.


package org.appfuse.dao;

import org.appfuse.model.Person;

public interface PersonDao extends Dao {
    public Person getPerson(Long personId);
    public void savePerson(Person person);
    public void removePerson(Long personId);

위 클래스의 모든 메소드에 예외를 기술하지 않은것은 Spring 프레임워크가 RuntimeException 들을 감싸고(wrap) 있기 때문입니다. 여기서 "ant compile-dao" 태스크를 이용하여 src/dao 와 test/dao 에 있는 모든 소스코드들을 컴파일 할 수 있을겁니다. 그러나 만약 "ant test-dao -Dtestcase=PersonDao" 태스크를 실행 하려 한다면 에러가 날 것입니다:No bean named 'personDao' is defined. 이것은 Spring 에 의해서 나타난 에러 메세지입니다. 이 메세지의 의미는 applicationContext-hibernate.xml 에 personDAO 라는 bean 을 정의 해야 한다는 것입니다. 이 bean 을 정의하기 전에 먼저 우리는 PersonDao 를 구현한 클래스를 작성해야 할 것입니다.

태스트를 위한 ant task 는 "test-module." 태스크를 호출합니다. 만약 testcase 파라미터(-Dtestcase=name)에 패스 값을 기술한다면 "test-module." 태스크는 **/*${testcase}* - 이것은 우리가 Person, PersonDao, PersonDaoTest 중 어떤것을 패스에 넣어도 상관 없다는 것을 말합니다. - 위 세가지 모두 PersonDaoTest 를 수행할 것입니다.

PersonDao interface의 메소드를 구현하는 PersonDaoHibernate class 를 작성합시다. 그리고 Hibernate 를 사용하여 Person object 를 get/save/delete 하는 메소드를 작성해봅시다.
src/dao/**/dao/hibernate 이곳에 PersonDaoHibernate.java 클래스를 만듭니다. 그리고 이를 반드시 BaseDaoHibernate 를 확장(extends)하고 PersonDao 를 구현(implements)하도록 작성합니다.


package org.appfuse.dao.hibernate;

import org.appfuse.model.Person;
import org.appfuse.dao.PersonDao;
import org.springframework.orm.ObjectRetrievalFailureException;

public class PersonDaoHibernate extends BaseDaoHibernate implements PersonDao {

    public Person getPerson(Long id) {
        Person person = (PersongetHibernateTemplate().get(Person.class, id);

        if (person == null) {
            throw new ObjectRetrievalFailureException(Person.class, id);   
        }

        return person;
    }

    public void savePerson(Person person) {
        getHibernateTemplate().saveOrUpdate(person);
    }

    public void removePerson(Long id) {
        // object must be loaded before it can be deleted
        getHibernateTemplate().delete(getPerson(id));
    }
}

여기서 "ant test-dao -Dtestcase=PersonDao" 태스크를 실행하려 하면 똑같은 에러가 나올 겁니다. 우리는 PersonDao 의 구현 클래스는 PersonDaoHibernate 라는 것과 Person object 에 대해 Spring 프레임워크가 알수 있도록 몇가지 파일들을 구성해야 합니다.

Person object와 PersonDao를 Spring 프레임워크에 알리기 위해 설정파일을 구성합니다 [#5]

첫째로, 어디에 Hibernate 매핑파일이 있는지 Spring 프레임 워크에 알려줘야 합니다.
src/dao/**/dao/hibernate/applicationContext-hibernate.xml 파일을 열어서 Person.hbm.xml 을 다음 코드블럭처럼 추가 합니다.


<property name="mappingResources"
    <list> 
        <value>org/appfuse/model/Person.hbm.xml</value> 
        <value>org/appfuse/model/Role.hbm.xml</value> 
        <value>org/appfuse/model/User.hbm.xml</value> 
    </list> 
</property> 

다음으로 PersonDaoHibernate 와 PersonDao 를 연결하기 위해서 다음의 XML 태그를 이 파일에 끝에 추가 합니다.


<!-- PersonDao: Hibernate implementation --> 
<bean id="personDao" class="org.appfuse.dao.hibernate.PersonDaoHibernate"
    <property name="sessionFactory"><ref local="sessionFactory"/></property> 
</bean>   

태그에 autowire="byName" 속성을 기술함으로써 "sessionFactory" property 설정을 없앨수도 있습니다. Personally, I like having the dependencies of my objects documented (in XML).

DaoTest 를 실행 합니다. [#6]

수정한 모든 파일을 저장하고 다시한번 "ant test-dao -Dtestcase=PersonDao" 태스크를 실행 합니다.

Yeah Baby, Yeah:
BUILD SUCCESSFUL
Total time: 9 seconds


Next Up: Part II: 새로운 매니저 작성하기 - (Session Facades 와 비슷하지만 EJBs 를 사용하지 않는)Business Facades 를 작성하는 방법. DAO 계층과 front-end 계층을 연결하는 창구 역할을 합니다.



Go to top   Edit this page   More info...   Attach file...
This page last changed on 06-Nov-2006 13:53:00 MST by MattRaible.