최초 커밋
commit
83c23f0f7c
@ -0,0 +1,272 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cokr.xit</groupId>
|
||||
<artifactId>xit-foundation</artifactId>
|
||||
<version>23.04.01</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>xit-foundation</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
|
||||
<spring.maven.artifact.version>5.3.20</spring.maven.artifact.version>
|
||||
<org.egovframe.rte.version>4.1.0</org.egovframe.rte.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>mvn2s</id>
|
||||
<url>https://repo1.maven.org/maven2/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>egovframe</id>
|
||||
<url>http://maven.egovframe.kr:8080/maven/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>egovframe2</id>
|
||||
<url>https://www.egovframe.go.kr/maven/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.egovframe.rte</groupId>
|
||||
<artifactId>org.egovframe.rte.fdl.excel</artifactId>
|
||||
<version>${org.egovframe.rte.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.egovframe.rte</groupId>
|
||||
<artifactId>org.egovframe.rte.ptl.mvc</artifactId>
|
||||
<version>${org.egovframe.rte.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.egovframe.rte</groupId>
|
||||
<artifactId>org.egovframe.rte.fdl.property</artifactId>
|
||||
<version>${org.egovframe.rte.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet.jsp</groupId>
|
||||
<artifactId>jsp-api</artifactId>
|
||||
<version>2.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>jstl</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-fileupload</groupId>
|
||||
<artifactId>commons-fileupload</artifactId>
|
||||
<version>1.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>${spring.maven.artifact.version}</version>
|
||||
<!--
|
||||
<scope>provided</scope>
|
||||
-->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-dbcp</groupId>
|
||||
<artifactId>commons-dbcp</artifactId>
|
||||
<version>1.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bgee.log4jdbc-log4j2</groupId>
|
||||
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
|
||||
<version>1.16</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.16</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.9.2</version>
|
||||
<!--
|
||||
<scope>test</scope>
|
||||
-->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>2.7.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<defaultGoal>install</defaultGoal>
|
||||
<directory>${basedir}/target</directory>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
|
||||
<resources>
|
||||
<resource><directory>${basedir}/src/main/resources</directory></resource>
|
||||
</resources>
|
||||
|
||||
<testResources>
|
||||
<testResource><directory>${basedir}/src/test/resources</directory></testResource>
|
||||
<testResource><directory>${basedir}/src/main/resources</directory></testResource>
|
||||
</testResources>
|
||||
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.tomcat.maven</groupId>
|
||||
<artifactId>tomcat7-maven-plugin</artifactId>
|
||||
<version>2.2</version>
|
||||
<configuration>
|
||||
<port>80</port>
|
||||
<path>/</path>
|
||||
<systemProperties>
|
||||
<JAVA_OPTS>-Xms256m -Xmx768m -XX:MaxPermSize=256m</JAVA_OPTS>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>hibernate3-maven-plugin</artifactId>
|
||||
<version>2.1</version>
|
||||
<configuration>
|
||||
<components>
|
||||
<component>
|
||||
<name>hbm2ddl</name>
|
||||
<implementation>annotationconfiguration</implementation>
|
||||
</component>
|
||||
</components>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<!-- EMMA -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>emma-maven-plugin</artifactId>
|
||||
<version>1.0-alpha-3</version>
|
||||
</plugin>
|
||||
<!-- PMD manven plugin -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-pmd-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>**/*.class</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- EMMA -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
<reportFormat>xml</reportFormat>
|
||||
<excludes>
|
||||
<exclude>**/Abstract*.java</exclude>
|
||||
<exclude>**/*Suite.java</exclude>
|
||||
</excludes>
|
||||
<includes>
|
||||
<include>**/*Test.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>emma-maven-plugin</artifactId>
|
||||
<inherited>true</inherited>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>2.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- Javadoc -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.9.1</version>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
@ -0,0 +1,208 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import org.egovframe.rte.fdl.property.EgovPropertyService;
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
import cokr.xit.foundation.data.Convert;
|
||||
|
||||
/**업무 컴포넌트나 기능 구현 컴포넌트를 정의할 때 상속받는 클래스.
|
||||
* <p>AbstractComponent를 상속받는 클래스는
|
||||
* <ul><li>{@link Assert}</li>
|
||||
* <li>{@link #log() 로깅} 서비스</li>
|
||||
* <li>{@link #currentAccess() 현재 접속 정보}</li>
|
||||
* <li>{@link #currentUser() 현재 접속한 사용자 정보}</li>
|
||||
* <li>{@link #properties 프로퍼티 서비스}</li>
|
||||
* <li>{@link #message(String, String...) 메시지 처리}</li>
|
||||
* </ul>
|
||||
* 등의 기능을 사용할 수 있다.
|
||||
* <p>AbstractComponent의 상속 클래스는 '업무 용어 또는 기능 이름' + 'Bean'과 같이 이름을 명시한다.<br />
|
||||
* 그리고 정의한 클래스를 애플리케이션 컨텍스트에 등록하려면, @Component 어노테이션을 적용하고 카멜 표기법으로 이름을 지정한다.<br />
|
||||
* 다음은 그 예이다.
|
||||
* <pre><code> package cokr.xit.example.business.service.bean;
|
||||
* {@code @Component("businessBean")}
|
||||
* public class BusinessBean extends AbstractComponent {
|
||||
* ...
|
||||
* }</code></pre>
|
||||
* <p>정의한 컴포넌트를 사용하려면 @Resource 어노테이션에 이름을 명시해 객체에 주입한다.
|
||||
* <pre><code> @Resource(name = "businessBean")
|
||||
* BusinessBean businessBean;</code></pre>
|
||||
*/
|
||||
public class AbstractComponent {
|
||||
/** 프로퍼티 서비스 */
|
||||
@Resource(name="propertyService")
|
||||
protected EgovPropertyService properties;
|
||||
/** 메시지 소스 */
|
||||
@Resource(name="messageSource")
|
||||
protected MessageSource messages;
|
||||
private String logName;
|
||||
|
||||
/**로그이름을 설정한다.
|
||||
* @param logName 로그이름
|
||||
*/
|
||||
public void setLogName(String logName) {
|
||||
this.logName = logName;
|
||||
}
|
||||
|
||||
/**현재 접근한 사용자를 반환한다.
|
||||
* @param <T> 사용자 정보 유형
|
||||
* @return 현재 접근한 사용자
|
||||
*/
|
||||
protected <T extends UserInfo> T currentUser() {
|
||||
return UserInfo.current();
|
||||
}
|
||||
|
||||
/**현재 사용자의 접근정보를 반환한다.
|
||||
* @return 현재 사용자의 접근정보
|
||||
*/
|
||||
protected static Access currentAccess() {
|
||||
return Access.current();
|
||||
}
|
||||
|
||||
/**lv와 rv가 같은 지 반환한다.
|
||||
* @param lv 좌측 값
|
||||
* @param rv 우측 값
|
||||
* @return
|
||||
* <ul><li>lv와 rv가 같으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected static final boolean equals(Object lv, Object rv) {
|
||||
return lv == rv ? true : lv != null && lv.equals(rv);
|
||||
}
|
||||
|
||||
/**{@link Assert#isEmpty(Object)} 참고
|
||||
*/
|
||||
protected static final boolean isEmpty(Object obj) {
|
||||
return Assert.isEmpty(obj);
|
||||
}
|
||||
|
||||
/**{@link Assert#ifEmpty(Object, Supplier)} 참고*/
|
||||
protected static final <T> T ifEmpty(T t, Supplier<T> nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/**{@link Assert#ifEmpty(Object, Object)} 참고.
|
||||
*/
|
||||
protected static final <T> T ifEmpty(T t, T nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/**obj가 빈 값이면 공백문자("")를 반환한다. 빈 값이 아니면 obj를 String으로 캐스팅하여 반환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>obj가 빈 값이면 공백문자("")</li>
|
||||
* <li>빈 값이 아니면 String으로 캐스팅한 obj</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected static final String blankIfEmpty(Object obj) {
|
||||
return ifEmpty((String)obj, "");
|
||||
}
|
||||
|
||||
/**{@link Assert#notEmpty(Object, String)} 참고.
|
||||
*/
|
||||
protected static final <T> T notEmpty(T t, String name) {
|
||||
return Assert.notEmpty(t, name);
|
||||
}
|
||||
|
||||
/**See {@link Convert#toShort(Object)}.*/
|
||||
protected static final short toShort(Object obj) {
|
||||
return Convert.toShort(obj);
|
||||
}
|
||||
|
||||
/**See {@link Convert#toInt(Object)}.*/
|
||||
protected static final int toInt(Object obj) {
|
||||
return Convert.toInt(obj);
|
||||
}
|
||||
|
||||
/**See {@link Convert#toLong(Object)}.*/
|
||||
protected static final long toLong(Object obj) {
|
||||
return Convert.toLong(obj);
|
||||
}
|
||||
|
||||
/**See {@link Convert#toDouble(Object)}.*/
|
||||
protected static final double toDouble(Object obj) {
|
||||
return Convert.toDouble(obj);
|
||||
}
|
||||
|
||||
/**See {@link Convert#toFloat(Object)}.*/
|
||||
protected static final float toFloat(Object obj) {
|
||||
return Convert.toFloat(obj);
|
||||
}
|
||||
|
||||
/**See {@link Convert#toByte(Object)}.*/
|
||||
protected static final byte toByte(Object obj) {
|
||||
return Convert.toByte(obj);
|
||||
}
|
||||
|
||||
/**See {@link Convert#toBoolean(Object)}.*/
|
||||
protected static final boolean toBoolean(Object obj) {
|
||||
return Convert.toBoolean(obj);
|
||||
}
|
||||
|
||||
/**메시지 리소스 파일에서 key로 정의된 문자열과 args로 메시지를 만들어 반환한다.
|
||||
* @param key 메시지 리소스 파일의 문자열 key
|
||||
* @param args 해당 문자열에 전달할 인자
|
||||
* @return 메시지
|
||||
*/
|
||||
protected String message(String key, String... args) {
|
||||
return messages.getMessage(key, ifEmpty(args, () -> null), "", currentAccess().getLocale());
|
||||
}
|
||||
|
||||
/**t가 공백이 아닌지 확인하고 t를 반환한다.
|
||||
* t가 공백이면, NullPointerException을 발생시키며, 메시지는 message-common 프로퍼티 파일의 'valueRequired'키에 설정된 값으로 만든다.
|
||||
* @param <T> 객체 유형
|
||||
* @param t 값이나 객체
|
||||
* @param name assertion 실패 시 오류 메시지에 사용할 이름
|
||||
* @return t
|
||||
*/
|
||||
protected final <T> T required(T t, String name) {
|
||||
if (!isEmpty(t)) return t;
|
||||
throw new NullPointerException(message("valueRequired", name));
|
||||
}
|
||||
|
||||
/**{@link Assert#rootCause(Throwable)} 참고
|
||||
*/
|
||||
protected static final Throwable rootCause(Throwable t) {
|
||||
return Assert.rootCause(t);
|
||||
}
|
||||
|
||||
/**{@link ApplicationException#get(Throwable)} 참고
|
||||
*/
|
||||
protected static final ApplicationException applicationException(Throwable t) {
|
||||
return ApplicationException.get(t);
|
||||
}
|
||||
|
||||
/**{@link Assert#runtimeException(Throwable)} 참고
|
||||
*/
|
||||
protected static final RuntimeException runtimeException(Throwable t) {
|
||||
return Assert.runtimeException(t);
|
||||
}
|
||||
|
||||
/**klass와 연계된 Log를 반환한다.
|
||||
* @param klass Log와 연계된 클래스
|
||||
* @return Log
|
||||
*/
|
||||
protected static Log log(Class<?> klass) {
|
||||
return Log.get(klass);
|
||||
}
|
||||
|
||||
/**현재 클래스와 연계된 Log를 반환한다.
|
||||
* @return Log
|
||||
*/
|
||||
protected Log log() {
|
||||
return isEmpty(logName) ? log(getClass()) : Log.get(logName);
|
||||
}
|
||||
|
||||
/**현재 객체를 반환한다.
|
||||
* @param <T> AbstractComponent 유형
|
||||
* @return 현재 객체
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T extends AbstractComponent> T self() {
|
||||
return (T)this;
|
||||
}
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**현재 클라이언트(사용자)의 접근정보를 추출하여 제공하는 클래스.
|
||||
*/
|
||||
public class Access extends AbstractComponent {
|
||||
protected static final ThreadLocal<Access> current = new ThreadLocal<>();
|
||||
|
||||
private String
|
||||
id,
|
||||
sessionID,
|
||||
action,
|
||||
ipAddress;
|
||||
private boolean
|
||||
newSession,
|
||||
mobile,
|
||||
ajaxRequest,
|
||||
jsonResponse;
|
||||
private Locale locale;
|
||||
|
||||
private static final String
|
||||
LOCALHOST_V4 = "127.0.0.1",
|
||||
LOCALHOST_V6 = "0:0:0:0:0:0:0:1";
|
||||
|
||||
/**현재 클라이언트의 IP주소가 로컬호스트와 같으면 로컬호스트의 주소를 반환한다.
|
||||
* @param address 클라이언트의 IP 주소
|
||||
* @param localhost 로컬호스트 주소
|
||||
* @return
|
||||
* <ul><li>현재 클라이언트의 IP주소가 로컬호스트와 같으면 로컬호스트의 주소</li>
|
||||
* <li>그렇지 않으면 클라이언트 주소</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static String getClientAddress(String address, String localhost) {
|
||||
return LOCALHOST_V4.equals(address) || LOCALHOST_V6.equals(address) ? localhost : address;
|
||||
}
|
||||
|
||||
/**현재 클라이언트의 접근 아이디를 반환한다.
|
||||
* @return 현재 클라이언트의 접근 아이디
|
||||
*/
|
||||
public String getId() {
|
||||
return ifEmpty(id, id = "access-" + System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**세션 아이디를 반환한다.
|
||||
* @return 세션 아이디
|
||||
*/
|
||||
public String getSessionID() {
|
||||
return sessionID;
|
||||
}
|
||||
|
||||
/**세션 아이디를 설정한다.
|
||||
* @param sessionID 세션 아이디
|
||||
* @return Access
|
||||
*/
|
||||
public Access setSessionID(String sessionID) {
|
||||
this.sessionID = sessionID;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**접근 후 실행할 액션이름(예: uri나 url)을 반환한다.
|
||||
* @return 접근 후 실행할 액션이름
|
||||
*/
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
/**접근 후 실행할 액션이름(예:uri나 url)을 설정한다.
|
||||
* @param action 접근 후 실행할 액션이름
|
||||
* @return Access
|
||||
*/
|
||||
public Access setAction(String action) {
|
||||
this.action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**클라이언트의 IP주소를 반환한다.
|
||||
* @return 클라이언트의 IP주소
|
||||
*/
|
||||
public String getIpAddress() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
/**클라이언트의 IP주소를 설정한다.
|
||||
* @param ipAddress 클라이언트의 IP주소
|
||||
* @return Access
|
||||
*/
|
||||
public Access setIpAddress(String ipAddress) {
|
||||
this.ipAddress = ipAddress;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**현재 스레드의 클라이언트의 Access 정보를 반환한다.
|
||||
* @return 현재 스레드의 클라이언트의 Access
|
||||
*/
|
||||
public static Access current() {
|
||||
return ifEmpty(current.get(), Access::new);
|
||||
}
|
||||
|
||||
/**현재 Access를 현재 스레드의 클라이언트의 접근정보로 설정한다.
|
||||
* @return Access
|
||||
*/
|
||||
public Access setCurrent() {
|
||||
current.set(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**현재 Access의 세션이 새 세션인지 반환한다.
|
||||
* @return
|
||||
* <ul><li>새 세션이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean inNewSession() {
|
||||
return newSession;
|
||||
}
|
||||
|
||||
/**현재 Access의 세션이 새 세션인지 설정한다.
|
||||
* @param newSession
|
||||
* <ul><li>새 세션이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
* @return Access
|
||||
*/
|
||||
public Access setNewSession(boolean newSession) {
|
||||
this.newSession = newSession;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**클라이언트가 모바일 기기로 접근한 것인지 반환한다.
|
||||
* @return
|
||||
* <ul><li>모바일 기기로 접근했으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isMobile() {
|
||||
return mobile;
|
||||
}
|
||||
|
||||
/**클라이언트가 모바일 기기로 접근한 것인지 설정한다.
|
||||
* @param mobile
|
||||
* <ul><li>모바일 기기로 접근했으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
* @return Access
|
||||
*/
|
||||
public Access setMobile(boolean mobile) {
|
||||
this.mobile = mobile;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**접근요청이 AJAX로 된 것인지 반환한다.
|
||||
* @return
|
||||
* <ul><li>AJAX로 요청됐으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isAjaxRequest() {
|
||||
return ajaxRequest;
|
||||
}
|
||||
|
||||
/**접근요청이 AJAX로 된 것인지 설정한다.
|
||||
* @param header HTTP 요청의 'X-Requested-With' 헤더값
|
||||
* @return Access
|
||||
*/
|
||||
public Access setAjaxRequest(String header) {
|
||||
ajaxRequest = "XMLHttpRequest".equalsIgnoreCase(header);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**접근요청의 응답을 JSON으로 하는지 반환한다.
|
||||
* @return
|
||||
* <ul><li>JSON 응답이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isJsonResponse() {
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
/**접근요청의 응답을 JSON으로 하는지 설정한다.
|
||||
* @param header HTTP 요청의 'accept' 헤더값
|
||||
* @return Access
|
||||
*/
|
||||
public Access setJsonResponse(String header) {
|
||||
this.jsonResponse = !isEmpty(header) && header.contains("json");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**접근 요청의 Locale을 반환한다.
|
||||
* @return 접근 요청의 Locale
|
||||
*/
|
||||
public Locale getLocale() {
|
||||
return locale != null ? locale : Locale.getDefault();
|
||||
}
|
||||
|
||||
/**접근 요청의 Locale을 설정한다.
|
||||
* @param locale Locale
|
||||
* @return Access
|
||||
*/
|
||||
public Access setLocale(Locale locale) {
|
||||
this.locale = locale;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**접근요청이 디버그 모드에서 된 것인지 반환한다.
|
||||
* @return
|
||||
* <ul><li>디버그 모드면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isDebug() {
|
||||
return "true".equals(System.getProperty("debug"));
|
||||
}
|
||||
|
||||
/**현재 Access를 현재 스레드에서 제거한다.
|
||||
* @return 제거된 Access
|
||||
*/
|
||||
public static Access release() {
|
||||
Access access = current.get();
|
||||
current.remove();
|
||||
return access;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s(id:'%s', clientIP:'%s', action:'%s')", getClass().getSimpleName(), getId(), getIpAddress(), getAction());
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**애플리케이션이 동작하는 다음과 같은 환경정보를 제공한다.
|
||||
* <ul><li>{@link #getApplicationContext() ApplicationContext}</li>
|
||||
* <li>{@link #getHostName() 호스트 정보}</li>
|
||||
* <li>{@link #getActiveProfiles() 프로필 정보}</li>
|
||||
* <li>{@link #getStartupDate() 애플리케이션 시작 일시}</li>
|
||||
* </ul>
|
||||
* @author mjkhan
|
||||
*/
|
||||
@Component("applicationContainer")
|
||||
public class ApplicationContainer implements ApplicationContextAware {
|
||||
private static ApplicationContainer obj;
|
||||
|
||||
public static ApplicationContainer get() {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private String
|
||||
hostAddress,
|
||||
hostName,
|
||||
canonicalHostName;
|
||||
private ApplicationContext actx;
|
||||
private String applicationName;
|
||||
private List<String> activeProfiles;
|
||||
private Date startupDate;
|
||||
private boolean secured;
|
||||
|
||||
private ApplicationContainer() {
|
||||
try {
|
||||
init();
|
||||
} catch (Exception e) {
|
||||
throw ApplicationException.get(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**호스트의 IP 주소를 반환한다.
|
||||
* @return 호스트의 IP 주소
|
||||
*/
|
||||
public String getHostAddress() {
|
||||
return hostAddress;
|
||||
}
|
||||
|
||||
/**호스트 이름을 반환한다.
|
||||
* @return 호스트 이름
|
||||
*/
|
||||
public String getHostName() {
|
||||
return hostName;
|
||||
}
|
||||
|
||||
/**{@link InetAddress#getCanonicalHostName()} 참고
|
||||
* @return Canonical host name
|
||||
*/
|
||||
public String getCanonicalHostName() {
|
||||
return canonicalHostName;
|
||||
}
|
||||
|
||||
/**ApplicationContainer를 초기화한다.
|
||||
* @throws Exception
|
||||
*/
|
||||
protected void init() throws Exception {
|
||||
InetAddress inetAddress = InetAddress.getLocalHost();
|
||||
hostAddress = inetAddress.getHostAddress();
|
||||
hostName = inetAddress.getHostName();
|
||||
canonicalHostName = inetAddress.getCanonicalHostName();
|
||||
}
|
||||
|
||||
/**ApplicationContext를 반환한다.
|
||||
* @return ApplicationContext
|
||||
*/
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return actx;
|
||||
}
|
||||
|
||||
private Log log() {
|
||||
return Log.get(getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
actx = applicationContext;
|
||||
applicationName = actx.getApplicationName();
|
||||
String[] apf = Assert.ifEmpty(actx.getEnvironment().getActiveProfiles(), actx.getEnvironment()::getDefaultProfiles);
|
||||
activeProfiles = Stream.of(apf).collect(Collectors.toList());
|
||||
startupDate = new Date(actx.getStartupDate());
|
||||
secured = actx.containsBean("authorityService");
|
||||
log().debug("{} loaded", this);
|
||||
obj = this;
|
||||
}
|
||||
|
||||
/**애플리케이션 이름을 반환한다. 애플리케이션 이름은 'spring.application.name' 프로퍼티로 설정한다.
|
||||
* @return 애플리케이션 이름
|
||||
*/
|
||||
public String getApplicationName() {
|
||||
return applicationName;
|
||||
}
|
||||
|
||||
/**애플리케이션에 활성화된 프로필을 반환한다.
|
||||
* @return 애플리케이션에 활성화된 프로필
|
||||
*/
|
||||
public List<String> getActiveProfiles() {
|
||||
return activeProfiles;
|
||||
}
|
||||
|
||||
/**지정하는 프로필이 활성화 돼있는지 반환한다.
|
||||
* @param profiles 프로필
|
||||
* @return 프로필의 활성화 여부
|
||||
* <ul><li>활성화 돼있으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isProfileActive(String... profiles) {
|
||||
for (String profile: profiles) {
|
||||
if (activeProfiles.contains(profile))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSecured() {
|
||||
return secured;
|
||||
}
|
||||
|
||||
/**애플리케이션이 시작한 일시를 반환한다.
|
||||
* @return 애플리케이션이 시작한 일시
|
||||
*/
|
||||
public Date getStartupDate() {
|
||||
return startupDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[hostAddress=" + hostAddress + ", hostName=" + hostName + ", applicationName=" + applicationName + (secured ? ", secured" : "") + "]";
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import cokr.xit.foundation.data.DataObject;
|
||||
|
||||
/**애플리케이션이 발생시키는 예외
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class ApplicationException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**주어진 오류를 최초 원인으로 하는 ApplicationException을 반환한다.
|
||||
* @param cause 최초 원인이 되는 오류
|
||||
* @return ApplicationException
|
||||
*/
|
||||
public static ApplicationException get(Throwable cause) {
|
||||
return cause instanceof ApplicationException ?
|
||||
ApplicationException.class.cast(cause) :
|
||||
new ApplicationException(cause);
|
||||
}
|
||||
|
||||
private DataObject objs;
|
||||
|
||||
private ApplicationException(Throwable cause) {
|
||||
super(Assert.rootCause(cause));
|
||||
}
|
||||
|
||||
/**추가정보를 갖는 DataObject를 반환한다.
|
||||
* @return 추가정보를 갖는 DataObject
|
||||
*/
|
||||
public DataObject info() {
|
||||
if (objs == null)
|
||||
objs = new DataObject();
|
||||
return objs;
|
||||
}
|
||||
|
||||
/**추가정보를 설정한다.
|
||||
* @param key 추가정보의 키
|
||||
* @param value 추가정보
|
||||
* @return ApplicationException
|
||||
*/
|
||||
public ApplicationException info(String key, Object value) {
|
||||
info().set(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**에러코드를 반환한다.
|
||||
* @return 에러코드
|
||||
*/
|
||||
public String getCode() {
|
||||
return objs != null ? objs.string("code") : null;
|
||||
}
|
||||
|
||||
/**에러코드를 설정한다.
|
||||
* @param errorCode 에러코드
|
||||
* @return ApplicationException
|
||||
*/
|
||||
public ApplicationException setCode(String errorCode) {
|
||||
return info("code", errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
String msg = objs != null ? objs.string("msg") : "";
|
||||
if (Assert.isEmpty(msg)) {
|
||||
Throwable cause = getCause();
|
||||
if (cause != null)
|
||||
msg = cause.getMessage();
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**에러 메시지를 설정한다.
|
||||
* @param msg 에러 메시지
|
||||
* @return ApplicationException
|
||||
*/
|
||||
public ApplicationException setMessage(String msg) {
|
||||
return info("msg", msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**Assertion 유틸리티
|
||||
* */
|
||||
public class Assert {
|
||||
private Assert() {}
|
||||
|
||||
/**obj가 null인지, 공백문자인지, 빈 collection인지, 빈 배열인지 반환한다.
|
||||
* @param obj 오브젝트
|
||||
* @return
|
||||
* <ul><li>obj가 null인지, 공백문자인지, 빈 collection인지, 빈 배열이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final boolean isEmpty(Object obj) {
|
||||
if (obj == null) return true;
|
||||
if (obj instanceof String) {
|
||||
String str = (String)obj;
|
||||
return str.trim().isEmpty();
|
||||
}
|
||||
if (obj instanceof Iterable) {
|
||||
Iterable<?> objs = (Iterable<?>)obj;
|
||||
return !objs.iterator().hasNext();
|
||||
}
|
||||
if (obj.getClass().isArray()) {
|
||||
return Array.getLength(obj) < 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**t가 {@link #isEmpty(Object) 공백이면} nt가 반환하는 값을, 그렇지 않으면 t를 반환한다.
|
||||
* @param <T> 타입
|
||||
* @param t 오브젝트
|
||||
* @param nt 공백일 경우 nt를 반환할 Supplier
|
||||
* @return
|
||||
* <ul><li>t가 공백이 아니면 t</li>
|
||||
* <li>t가 공백이면 Supplier가 반환하는 값</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final <T> T ifEmpty(T t, Supplier<T> nt) {
|
||||
return !isEmpty(t) ? t : nt != null ? nt.get() : t;
|
||||
}
|
||||
|
||||
/**t가 {@link #isEmpty(Object) 공백이면} nt를, 그렇지 않으면 t를 반환한다.
|
||||
* @param <T> 타입
|
||||
* @param t 오브젝트
|
||||
* @param nt 공백일 경우 반환할 값
|
||||
* @return
|
||||
* <ul><li>t가 공백이 아니면 t</li>
|
||||
* <li>t가 공백이면 nt</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final <T> T ifEmpty(T t, T nt) {
|
||||
return ifEmpty(t, () -> nt);
|
||||
}
|
||||
|
||||
/**t가 공백이 아닌지 확인하고 t를 반환한다.<br />
|
||||
* 올바로 동작하려면 JVM을 시작할 때 <code>-enableassertion:cokr.xit.Assert</code>나 <code>-ea:cokr.xit.Assert</code> 옵션을 지정해야 한다.<br />
|
||||
* t가 {@link #isEmpty(Object) 공백이면}, NullPointerException을 발생시킨다.
|
||||
* @param t 오브젝트
|
||||
* @param name assertion 실패 시 오류 메시지에 사용할 이름
|
||||
* @param <T> 객체 타입
|
||||
* @return t
|
||||
*/
|
||||
public static final <T> T assertNotEmpty(T t, String name) {
|
||||
try {
|
||||
assert !isEmpty(t);
|
||||
return t;
|
||||
} catch (AssertionError e) {
|
||||
throw new NullPointerException(name + " 값이 없습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**t가 공백이 아닌지 확인하고 t를 반환한다.<br />
|
||||
* t가 {@link #isEmpty(Object) 공백이면}, NullPointerException을 발생시킨다.
|
||||
* @param t 오브젝트
|
||||
* @param name assertion 실패 시 오류 메시지에 사용할 이름
|
||||
* @param <T> 객체 타입
|
||||
* @return t
|
||||
*/
|
||||
public static final <T> T notEmpty(T t, String name) {
|
||||
if (!isEmpty(t)) return t;
|
||||
throw new NullPointerException(name + " 값이 없습니다.");
|
||||
}
|
||||
|
||||
/**t의 최초원인이 되는 Throwable을 반환한다.
|
||||
* @param t Throwable
|
||||
* @return t의 최초원인이 되는 Throwable
|
||||
*/
|
||||
public static final Throwable rootCause(Throwable t) {
|
||||
Throwable cause = t != null ? t.getCause() : null;
|
||||
if (cause == null
|
||||
&& t instanceof InvocationTargetException)
|
||||
cause = ((InvocationTargetException)t).getTargetException();
|
||||
return cause == null || cause == t ? t : rootCause(cause);
|
||||
}
|
||||
|
||||
/**t의 최초 원인이 된 예외를 RuntimeException에 담아 반환한다.
|
||||
* @param t 예외
|
||||
* @return RuntimeException
|
||||
*/
|
||||
public static final RuntimeException runtimeException(Throwable t) {
|
||||
t = rootCause(t);
|
||||
return t instanceof RuntimeException ? RuntimeException.class.cast(t) : new RuntimeException(t);
|
||||
}
|
||||
|
||||
/**{@link Assert}에 대한 인터페이스.
|
||||
* <p>이 인터페이스의 모든 메소드는 디폴트로 Assert 클래스의 메소드를 호출하도록 구현돼 있다.<br />
|
||||
* Assert를 상속받지 않는 클래스에서 쉽게 Assert의 메소드를 사용하도록 하기 위한 것이다.
|
||||
* </p>
|
||||
*/
|
||||
public static interface Support {
|
||||
/** {@link Assert#isEmpty(Object)} 참고 */
|
||||
default boolean isEmpty(Object obj) {
|
||||
return Assert.isEmpty(obj);
|
||||
}
|
||||
|
||||
/** {@link Assert#ifEmpty(Object, Supplier)} 참고 */
|
||||
default <T> T ifEmpty(T t, Supplier<T> nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/** {@link Assert#ifEmpty(Object, Object)} 참고 */
|
||||
default <T> T ifEmpty(T t, T nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/** {@link Assert#assertNotEmpty(Object, String)} 참고 */
|
||||
default <T> T assertNotEmpty(T t, String name) {
|
||||
return Assert.assertNotEmpty(t, name);
|
||||
}
|
||||
|
||||
/** {@link Assert#notEmpty(Object, String)} 참고 */
|
||||
default <T> T notEmpty(T t, String name) {
|
||||
return Assert.notEmpty(t, name);
|
||||
}
|
||||
|
||||
/** {@link Assert#rootCause(Throwable)} 참고 */
|
||||
default Throwable rootCause(Throwable t) {
|
||||
return Assert.rootCause(t);
|
||||
}
|
||||
|
||||
/** {@link Assert#runtimeException(Throwable)} 참고 */
|
||||
default RuntimeException runtimeException(Throwable t) {
|
||||
return runtimeException(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**org.slf4j.Logger를 wrapping하여 로그 기능을 제공한다.<br />
|
||||
* 로그를 활성화하려면 slf4j가 지원하는 로그 api를 정하여 해당 라이브러리를 추가하고 설정파일을 작성한다.
|
||||
* <p>로그를 남길 문자열이 복잡할 경우
|
||||
* <ul><li>문자열의 '+' 연산을 하면 안된다.</li>
|
||||
* <li>대신 문자열 포맷에 '{}' 파라미터를 이용해 값을 제공한다.</li>
|
||||
* </ul>
|
||||
* <pre><code> debug("디버그 내용");
|
||||
* debug("복잡한 디버그 내용 => {}, {}, {}", "a", "b", "c");</code></pre>
|
||||
*/
|
||||
public class Log {
|
||||
private static final HashMap<Class<?>, Log> byClass = new HashMap<>();
|
||||
private static final HashMap<String, Log> byName = new HashMap<>();
|
||||
|
||||
/**klass와 연계된 Log를 반환한다.
|
||||
* @param klass 클래스
|
||||
* @return Log
|
||||
*/
|
||||
public static final Log get(Class<?> klass) {
|
||||
Log log = byClass.get(klass);
|
||||
if (log == null) {
|
||||
byClass.put(klass, log = new Log(LoggerFactory.getLogger(klass)));
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
/**지정한 이름과 연계된 Log를 반환한다.
|
||||
* @param logName 로그이름
|
||||
* @return Log
|
||||
*/
|
||||
public static final Log get(String logName) {
|
||||
Log log = byName.get(logName);
|
||||
if (log == null) {
|
||||
byName.put(logName, log = new Log(LoggerFactory.getLogger(logName)));
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
private Logger logger;
|
||||
|
||||
private Log(Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**로그 레벨이 INFO로 설정돼있으면 주어진 포맷과 인자로 메시지를 로그한다.
|
||||
* @param format 메시지 포맷
|
||||
* @param args 메시지 인자
|
||||
*/
|
||||
public void info(String format, Object... args) {
|
||||
if (logger.isInfoEnabled())
|
||||
logger.info(format, args);
|
||||
}
|
||||
|
||||
/**로그 레벨이 WARN로 설정돼있으면 주어진 포맷과 인자로 메시지를 로그한다.
|
||||
* @param format 메시지 포맷
|
||||
* @param args 메시지 인자
|
||||
*/
|
||||
public void warn(String format, Object... args) {
|
||||
if (logger.isWarnEnabled())
|
||||
logger.warn(format, args);
|
||||
}
|
||||
|
||||
/**로그 레벨이 DEBUG로 설정돼있으면 주어진 포맷과 인자로 메시지를 로그한다.
|
||||
* @param format 메시지 포맷
|
||||
* @param args 메시지 인자
|
||||
*/
|
||||
public void debug(String format, Object... args) {
|
||||
if (logger.isDebugEnabled())
|
||||
logger.debug(format, args);
|
||||
}
|
||||
|
||||
/**로그 레벨이 TRACE로 설정돼있으면 주어진 포맷과 인자로 메시지를 로그한다.
|
||||
* @param format 메시지 포맷
|
||||
* @param args 메시지 인자
|
||||
*/
|
||||
public void trace(String format, Object... args) {
|
||||
if (logger.isTraceEnabled())
|
||||
logger.trace(format, args);
|
||||
}
|
||||
|
||||
/**로그 레벨이 ERROR로 설정돼있으면 주어진 포맷과 인자로 메시지를 로그한다.
|
||||
* @param format 메시지 포맷
|
||||
* @param args 메시지 인자
|
||||
*/
|
||||
public void error(String format, Object... args) {
|
||||
if (logger.isErrorEnabled())
|
||||
logger.error(format, args);
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**사용자 정보.
|
||||
*/
|
||||
public class User implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String UNKNOWN = "anonymous";
|
||||
public static final User unknown = new User().seal();
|
||||
|
||||
private String
|
||||
id,
|
||||
name,
|
||||
password;
|
||||
private boolean sealed;
|
||||
|
||||
/**User가 인증된(로그인한) 사용자인지 반환한다.
|
||||
* @return
|
||||
* <ul><li>인증된 사용자이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isAuthenticated() {
|
||||
return !UNKNOWN.equals(getId());
|
||||
}
|
||||
|
||||
/**사용자의 아이디를 반환한다.
|
||||
* @return 사용자의 아이디
|
||||
*/
|
||||
public String getId() {
|
||||
return id != null ? id : UNKNOWN;
|
||||
}
|
||||
|
||||
/**사용자의 아이디를 설정한다.
|
||||
* @param id 사용자의 아이디
|
||||
*/
|
||||
public void setId(String id) {
|
||||
notSealed().id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return getId();
|
||||
}
|
||||
|
||||
/**사용자의 이름을 반환한다.
|
||||
* @return 사용자의 이름
|
||||
*/
|
||||
public String getName() {
|
||||
return isAuthenticated() ? name : getId();
|
||||
}
|
||||
|
||||
/**사용자의 이름을 설정한다.
|
||||
* @param name 사용자의 이름
|
||||
*/
|
||||
public void setName(String name) {
|
||||
notSealed().name = name;
|
||||
}
|
||||
|
||||
/**사용자의 비밀번호를 반환한다.
|
||||
* @return 사용자의 비밀번호
|
||||
*/
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
/**사용자의 비밀번호를 설정한다.
|
||||
* @param password 사용자의 비밀번호
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
notSealed().password = password;
|
||||
}
|
||||
|
||||
private User seal() {
|
||||
sealed = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
private User notSealed() {
|
||||
if (sealed)
|
||||
throw new IllegalStateException(this + " is sealed");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s('%s', '%s')", getClass().getSimpleName(), getId(), getName());
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
package cokr.xit.foundation;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cokr.xit.foundation.data.DataObject;
|
||||
|
||||
/**현재 접속한 사용자의 상세정보를 갖는 객체.
|
||||
*/
|
||||
public class UserInfo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public static final String
|
||||
NAME = "userInfoProvider",
|
||||
SEC_NAME = "securedUserInfoProvider";
|
||||
public static final List<String> ALL_ACTIONS = Stream.of("all-actions").collect(Collectors.toList());
|
||||
|
||||
/**현재 접속한 사용자의 상세정보를 찾는 객체.
|
||||
* @author mjkhan
|
||||
*/
|
||||
@Component(NAME)
|
||||
public static class Provider {
|
||||
private static Provider obj;
|
||||
|
||||
/**현재 접속한 사용자의 상세정보를 찾는 Provider를 반환한다.
|
||||
* @return 현재 접속한 사용자의 상세정보를 찾는 Provider
|
||||
*/
|
||||
public static Provider get() {
|
||||
if (obj == null) {
|
||||
ApplicationContainer container = ApplicationContainer.get();
|
||||
String name = container.isSecured() ? SEC_NAME : NAME;
|
||||
obj = Provider.class.cast(container.getApplicationContext().getBean(name));
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** 사용자 상세정보 */
|
||||
protected UserInfo userInfo;
|
||||
|
||||
/**현재 접속한 사용자의 상세정보를 반환한다.
|
||||
* @return 현재 접속한 사용자의 상세정보
|
||||
*/
|
||||
public UserInfo currentUser() {
|
||||
return Assert.ifEmpty(userInfo, () -> userInfo = new UserInfo());
|
||||
}
|
||||
|
||||
/**현재 접속한 사용자의 상세정보를 설정한다.
|
||||
* @param userInfo 현재 접속한 사용자의 상세정보
|
||||
*/
|
||||
public void setUserInfo(UserInfo userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
}
|
||||
}
|
||||
|
||||
/**현재 접속한 사용자의 상세정보를 반환한다.
|
||||
* @param <T> 사용자 상세정보 유형
|
||||
* @return 사용자 상세정보
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends UserInfo> T current() {
|
||||
return (T)Provider.get().currentUser();
|
||||
}
|
||||
|
||||
private User user;
|
||||
private DataObject info;
|
||||
|
||||
/**현재 접속한 사용자를 반환한다.
|
||||
* @return 현재 접속한 사용자
|
||||
*/
|
||||
public User getUser() {
|
||||
return user != null ? user : User.unknown;
|
||||
}
|
||||
|
||||
/**현재 접속한 사용자를 설정한다.
|
||||
* @param user 현재 접속한 사용자
|
||||
*/
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
/**사용자가 인증된(로그인한) 사용자인지 반환한다.
|
||||
* @return
|
||||
* <ul><li>인증된 사용자이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isAuthenticated() {
|
||||
return getUser().isAuthenticated();
|
||||
}
|
||||
|
||||
/**사용자의 아이디를 반환한다.
|
||||
* @return 사용자의 아이디
|
||||
*/
|
||||
public String getId() {
|
||||
return getUser().getId();
|
||||
}
|
||||
|
||||
/**사용자의 아이디를 설정한다.
|
||||
* @param id 사용자의 아이디
|
||||
*/
|
||||
public void setId(String id) {
|
||||
getUser().setId(id);
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return getUser().getUsername();
|
||||
}
|
||||
|
||||
/**사용자의 이름을 반환한다.
|
||||
* @return 사용자의 이름
|
||||
*/
|
||||
public String getName() {
|
||||
return getUser().getName();
|
||||
}
|
||||
|
||||
/**사용자의 이름을 설정한다.
|
||||
* @param name 사용자의 이름
|
||||
*/
|
||||
public void setName(String name) {
|
||||
getUser().setName(name);
|
||||
}
|
||||
|
||||
/**사용자의 비밀번호를 반환한다.
|
||||
* @return 사용자의 비밀번호
|
||||
*/
|
||||
public String getPassword() {
|
||||
return getUser().getPassword();
|
||||
}
|
||||
|
||||
/**사용자의 비밀번호를 설정한다.
|
||||
* @param password 사용자의 비밀번호
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
getUser().setPassword(password);
|
||||
}
|
||||
|
||||
/**사용자의 추가정보를 갖는 map을 반환한다.
|
||||
* @return 사용자의 추가정보를 갖는 map
|
||||
*/
|
||||
public Map<String, Object> getInfo() {
|
||||
return info != null ? info : Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**사용자의 추가정보 중 key에 해당하는 값을 반환한다.
|
||||
* @param key 추가정보 키
|
||||
* @return 추가정보
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T info(String key) {
|
||||
return (T)getInfo().get(key);
|
||||
}
|
||||
|
||||
/**사용자의 추가정보를 설정한다.
|
||||
* @param key 추가정보 키
|
||||
* @param value 추가정보
|
||||
* @return User
|
||||
*/
|
||||
public <T extends UserInfo> T setInfo(String key, Object value) {
|
||||
if (info == null)
|
||||
info = new DataObject();
|
||||
info.put(key, value);
|
||||
return self();
|
||||
}
|
||||
|
||||
/**map이 포함하는 정보를 사용자의 추가정보로 설정한다.
|
||||
* 설정되는 추가정보의 키는 map의 키와 동일하다.
|
||||
* @param map 추가정보를 갖는 map
|
||||
* @return User
|
||||
*/
|
||||
public <T extends UserInfo> T setInfo(Map<String, ?> map) {
|
||||
if (map != null)
|
||||
map.forEach(this::setInfo);
|
||||
return self();
|
||||
}
|
||||
|
||||
/**현재 사용자가 접근할 수 있는 액션 url을 반환한다.
|
||||
* @return 현재 사용자가 접근할 수 있는 액션 url
|
||||
*/
|
||||
public List<String> getAccessibleActions() {
|
||||
return ALL_ACTIONS;
|
||||
}
|
||||
|
||||
/**현재 객체를 반환한다.
|
||||
* @param <T> 현재 객체 유형
|
||||
* @return 현재 객체
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T extends UserInfo> T self() {
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s('%s', '%s')", getClass().getSimpleName(), getId(), getName());
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
package cokr.xit.foundation.component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import org.egovframe.rte.fdl.property.EgovPropertyService;
|
||||
import org.egovframe.rte.psl.dataaccess.EgovAbstractMapper;
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
import cokr.xit.foundation.Access;
|
||||
import cokr.xit.foundation.ApplicationException;
|
||||
import cokr.xit.foundation.Assert;
|
||||
import cokr.xit.foundation.Log;
|
||||
import cokr.xit.foundation.UserInfo;
|
||||
import cokr.xit.foundation.data.DataObject;
|
||||
|
||||
/**DAO(Data Access Object)의 베이스 클래스.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public abstract class AbstractDao extends EgovAbstractMapper {
|
||||
/** 프로퍼티 서비스 */
|
||||
@Resource(name="propertyService")
|
||||
protected EgovPropertyService properties;
|
||||
/** 메시지 소스 */
|
||||
@Resource(name="messageSource")
|
||||
protected MessageSource messages;
|
||||
/** 로그이름 */
|
||||
private String logName;
|
||||
/** SQL mapper 네임스페이스 */
|
||||
private String namespace;
|
||||
|
||||
/**로그이름을 설정한다.
|
||||
* @param logName 로그이름
|
||||
*/
|
||||
public void setLogName(String logName) {
|
||||
this.logName = logName;
|
||||
}
|
||||
|
||||
/**DAO가 사용하는 SQL문의 namespace를 반환한한다.
|
||||
* @return DAO가 사용하는 SQL문의 namespace
|
||||
*/
|
||||
protected String namespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
/**DAO가 사용하는 SQL문의 namespace를 설정한다.
|
||||
* @param namespace DAO가 사용하는 SQL문의 namespace
|
||||
*/
|
||||
protected void setNamespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
/**DAO가 사용하는 SQL문의 ID를 {@link #namespace()} + '.' + sqlID로 조합하여 반환한다.
|
||||
* @param sqlID DAO가 사용하는 SQL문의 ID. '.'는 제외
|
||||
* @return {@link #namespace()} + '.' + sqlID
|
||||
* @see #setNamespace(String)
|
||||
*/
|
||||
protected String sqlID(String sqlID) {
|
||||
if (isEmpty(namespace))
|
||||
return sqlID;
|
||||
|
||||
notEmpty(sqlID, "sqlID");
|
||||
if (namespace.endsWith(".") || sqlID.startsWith("."))
|
||||
return namespace + sqlID;
|
||||
if (sqlID.indexOf(".") > 0)
|
||||
return sqlID;
|
||||
return namespace + "." + sqlID;
|
||||
}
|
||||
|
||||
/**현재 접근한 사용자를 반환한다.
|
||||
* @param <T> 사용자 정보 유형
|
||||
* @return 현재 접근한 사용자
|
||||
*/
|
||||
protected <T extends UserInfo> T currentUser() {
|
||||
return UserInfo.current();
|
||||
}
|
||||
|
||||
/**현재 사용자의 접근정보를 반환한다.
|
||||
* @return 현재 사용자의 접근정보
|
||||
*/
|
||||
protected static Access currentAccess() {
|
||||
return Access.current();
|
||||
}
|
||||
|
||||
/**메시지 리소스 파일에서 key로 정의된 문자열과 args로 메시지를 만들어 반환한다.
|
||||
* @param key 메시지 리소스 파일의 문자열 key
|
||||
* @param args 해당 문자열에 전달할 인자
|
||||
* @return 메시지
|
||||
*/
|
||||
protected String message(String key, String... args) {
|
||||
return messages.getMessage(key, ifEmpty(args, () -> null), currentAccess().getLocale());
|
||||
}
|
||||
|
||||
/**{@link Assert#isEmpty(Object)} 참고
|
||||
*/
|
||||
protected static boolean isEmpty(Object obj) {
|
||||
return Assert.isEmpty(obj);
|
||||
}
|
||||
|
||||
/**{@link Assert#ifEmpty(Object, Supplier)} 참고
|
||||
*/
|
||||
protected static <T> T ifEmpty(T t, Supplier<T> nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/**obj가 빈 값이면 공백문자("")를 반환한다. 빈 값이 아니면 obj를 String으로 캐스팅하여 반환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>obj가 빈 값이면 공백문자("")</li>
|
||||
* <li>빈 값이 아니면 String으로 캐스팅한 obj</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected static final String blankIfEmpty(Object obj) {
|
||||
return ifEmpty((String)obj, () -> "");
|
||||
}
|
||||
|
||||
/**{@link Assert#notEmpty(Object, String)} 참고
|
||||
*/
|
||||
protected static <T> T notEmpty(T t, String s) {
|
||||
return Assert.notEmpty(t, s);
|
||||
}
|
||||
|
||||
/**{@link ApplicationException#get(Throwable)} 참고
|
||||
*/
|
||||
protected static final ApplicationException applicationException(Throwable t) {
|
||||
return ApplicationException.get(t);
|
||||
}
|
||||
|
||||
protected static <T extends List<Map<String, Object>>> T underscoredToCamelCase(T list) {
|
||||
if (list.isEmpty())
|
||||
return list;
|
||||
|
||||
Set<Map.Entry<String, String>> keymap = list.get(0).keySet().stream().collect(
|
||||
Collectors.toMap(k -> k, v -> camelCase(v), (k1, k2) -> k1, LinkedHashMap::new)
|
||||
).entrySet();
|
||||
|
||||
list.forEach(row ->
|
||||
keymap.forEach(entry -> row.put(entry.getValue(), row.remove(entry.getKey())))
|
||||
);
|
||||
return list;
|
||||
}
|
||||
|
||||
private static String camelCase(String str) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
for (String token: str.split("_")) {
|
||||
if (buf.length() < 1)
|
||||
buf.append(token.toLowerCase());
|
||||
else
|
||||
buf.append(token.substring(0, 1).toUpperCase() + token.substring(1).toLowerCase());
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/**파라미터를 담을 DataObject를 생성하여 반환한다.
|
||||
* @return DataObject
|
||||
*/
|
||||
protected DataObject params() {
|
||||
return new DataObject();
|
||||
}
|
||||
|
||||
/**klass와 연계된 Log를 반환한다.
|
||||
* @param klass
|
||||
* @return Log
|
||||
*/
|
||||
protected static Log log(Class<?> klass) {
|
||||
return Log.get(klass);
|
||||
}
|
||||
|
||||
/**현재 클래스와 연계된 Log를 반환한다.
|
||||
* @return Log
|
||||
*/
|
||||
protected Log log() {
|
||||
return isEmpty(logName) ? log(getClass()) : Log.get(logName);
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package cokr.xit.foundation.component;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
|
||||
import org.egovframe.rte.fdl.property.EgovPropertyService;
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
import cokr.xit.foundation.Access;
|
||||
import cokr.xit.foundation.ApplicationException;
|
||||
import cokr.xit.foundation.Assert;
|
||||
import cokr.xit.foundation.Log;
|
||||
import cokr.xit.foundation.UserInfo;
|
||||
|
||||
/**서비스 인터페이스 구현체의 베이스 클래스
|
||||
* <p>업무 서비스는 인터페이스로 클라이언트에 노출된다.<br />
|
||||
* 클라이언트가 업무 인터페이스에 서비스를 요청하면, 서비스의 실행은 서비스 구현체에게 위임된다.
|
||||
* <p>애플리케이션의 서비스 구현체는 이 클래스를 상속받아 정의하고, 이름은 '서비스 인터페이스' + 'ServiceBean'으로 한다.<br />
|
||||
* 그리고 @Service 어노테이션을 명시하고, 카멜 표기법으로 표시한 업무 인터페이스의 이름을 지정한다.
|
||||
* 다음은 그 예이다.
|
||||
* <pre><code> package cokr.xit.example.business.service.bean;
|
||||
* ...
|
||||
* {@code @Service("businessService")}
|
||||
* public class BusinessServiceBean extends AbstractServiceBean implements BusinessService {
|
||||
* ...
|
||||
* }</code></pre>
|
||||
* 정의한 서비스 구현체는 다음과 같이 @Resource 어노테이션으로 이름을 명시해 주입한다.
|
||||
* <pre><code> @Resource(name = "businessService")
|
||||
* BusinessService businessService;</code></pre>
|
||||
* @author mjkhan
|
||||
*/
|
||||
public abstract class AbstractServiceBean extends EgovAbstractServiceImpl {
|
||||
/** 프로퍼티 서비스 */
|
||||
@Resource(name="propertyService")
|
||||
protected EgovPropertyService properties;
|
||||
/** 메시지 소스 */
|
||||
@Resource(name="messageSource")
|
||||
protected MessageSource messages;
|
||||
/** 로그이름 */
|
||||
private String logName;
|
||||
|
||||
/**로그이름을 설정한다.
|
||||
* @param logName 로그이름
|
||||
*/
|
||||
public void setLogName(String logName) {
|
||||
this.logName = logName;
|
||||
}
|
||||
|
||||
/**현재 접근한 사용자를 반환한다.
|
||||
* @return 현재 접근한 사용자
|
||||
*/
|
||||
protected <T extends UserInfo> T currentUser() {
|
||||
return UserInfo.current();
|
||||
}
|
||||
|
||||
/**현재 사용자의 접근정보를 반환한다.
|
||||
* @return 현재 사용자의 접근정보
|
||||
*/
|
||||
protected static Access currentAccess() {
|
||||
return Access.current();
|
||||
}
|
||||
|
||||
/**메시지 리소스 파일에서 key로 정의된 문자열과 args로 메시지를 만들어 반환한다.
|
||||
* @param key 메시지 리소스 파일의 문자열 key
|
||||
* @param args 해당 문자열에 전달할 인자
|
||||
* @return 메시지
|
||||
*/
|
||||
protected String message(String key, String... args) {
|
||||
return messages.getMessage(key, ifEmpty(args, () -> null), currentAccess().getLocale());
|
||||
}
|
||||
|
||||
/**lv와 rv가 같은 지 반환한다.
|
||||
* @param lv 좌측 값
|
||||
* @param rv 우측 값
|
||||
* @return
|
||||
* <ul><li>lv와 rv가 같으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected static final boolean equals(Object lv, Object rv) {
|
||||
return lv == rv ? true : lv != null && lv.equals(rv);
|
||||
}
|
||||
|
||||
/**{@link Assert#isEmpty(Object)} 참고
|
||||
*/
|
||||
protected static final boolean isEmpty(Object obj) {
|
||||
return Assert.isEmpty(obj);
|
||||
}
|
||||
|
||||
/**{@link Assert#ifEmpty(Object, Supplier)} 참고*/
|
||||
protected static final <T> T ifEmpty(T t, Supplier<T> nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/**{@link Assert#ifEmpty(Object, Object)} 참고.
|
||||
*/
|
||||
protected static final <T> T ifEmpty(T t, T nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/**obj가 빈 값이면 공백문자("")를 반환한다. 빈 값이 아니면 obj를 String으로 캐스팅하여 반환한다.
|
||||
* @param obj
|
||||
* @return
|
||||
* <ul><li>obj가 빈 값이면 공백문자("")</li>
|
||||
* <li>빈 값이 아니면 String으로 캐스팅한 obj</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected static final String blankIfEmpty(Object obj) {
|
||||
return ifEmpty((String)obj, () -> "");
|
||||
}
|
||||
|
||||
/**{@link Assert#notEmpty(Object, String)} 참고.
|
||||
*/
|
||||
protected static final <T> T notEmpty(T t, String name) {
|
||||
return Assert.notEmpty(t, name);
|
||||
}
|
||||
|
||||
/**t가 공백이 아닌지 확인하고 t를 반환한다.
|
||||
* t가 공백이면, NullPointerException을 발생시키며, 메시지는 message-common 프로퍼티 파일의 'valueRequired'키에 설정된 값으로 만든다.
|
||||
* @param t 값이나 객체
|
||||
* @param name assertion 실패 시 오류 메시지에 사용할 이름
|
||||
* @return t
|
||||
*/
|
||||
protected final <T> T required(T t, String name) {
|
||||
if (!isEmpty(t)) return t;
|
||||
throw new NullPointerException(message("valueRequired", name));
|
||||
}
|
||||
|
||||
/**{@link Assert#rootCause(Throwable)} 참고
|
||||
*/
|
||||
protected static final Throwable rootCause(Throwable t) {
|
||||
return Assert.rootCause(t);
|
||||
}
|
||||
|
||||
/**{@link ApplicationException#get(Throwable)} 참고
|
||||
*/
|
||||
protected static final ApplicationException applicationException(Throwable t) {
|
||||
return ApplicationException.get(t);
|
||||
}
|
||||
|
||||
/**klass와 연계된 Log를 반환한다.
|
||||
* @param klass
|
||||
* @return Log
|
||||
*/
|
||||
protected static Log log(Class<?> klass) {
|
||||
return Log.get(klass);
|
||||
}
|
||||
|
||||
/**현재 클래스와 연계된 Log를 반환한다.
|
||||
* @return Log
|
||||
*/
|
||||
protected Log log() {
|
||||
return isEmpty(logName) ? log(getClass()) : Log.get(logName);
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
package cokr.xit.foundation.component;
|
||||
|
||||
/**조회 서비스를 요청할 때 필요정보를 전달하는 클래스.
|
||||
* <p>pageNum, fetchSize가
|
||||
* <ul><li>0보다 크면 결과를 페이징하여 반환한다.</li>
|
||||
* <li>그렇지 않으면 전체 결과를 반환한다.</li>
|
||||
* </ul>
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class QueryRequest extends ServiceRequest {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String by;
|
||||
private String term;
|
||||
private String orderBy;
|
||||
private int pageNum;
|
||||
private int fetchSize;
|
||||
private String download;
|
||||
|
||||
/**조회 조건 필드를 반환한다.
|
||||
* @return 조회 조건 필드
|
||||
*/
|
||||
public String getBy() {
|
||||
return ifEmpty(by, () -> null);
|
||||
}
|
||||
|
||||
/**조회 조건 필드를 설정한다.
|
||||
* @param by 조회 조건 필드
|
||||
* @return QueryRequest
|
||||
*/
|
||||
public <T extends QueryRequest> T setBy(String by) {
|
||||
this.by = by;
|
||||
return self();
|
||||
}
|
||||
|
||||
/**조회 조건을 반환한다.
|
||||
* @return 조회 조건
|
||||
*/
|
||||
public String getTerm() {
|
||||
return ifEmpty(term, () -> null);
|
||||
}
|
||||
|
||||
/**조회 조건을 설정한다.
|
||||
* @param term 조회 조건
|
||||
* @return QueryRequest
|
||||
*/
|
||||
public <T extends QueryRequest> T setTerm(String term) {
|
||||
this.term = term;
|
||||
return self();
|
||||
}
|
||||
|
||||
/**정렬 조건을 반환한다.
|
||||
* @return 정렬 조건
|
||||
*/
|
||||
public String getOrderBy() {
|
||||
return orderBy;
|
||||
}
|
||||
|
||||
/**정렬 조건을 설정한다.
|
||||
* @param orderBy 정렬 조건
|
||||
* @return QueryRequest
|
||||
*/
|
||||
public <T extends QueryRequest> T setOrderBy(String orderBy) {
|
||||
this.orderBy = orderBy;
|
||||
return self();
|
||||
}
|
||||
|
||||
/**조회 결과의 시작 페이지를 반환한다.
|
||||
* @return dataStart 조회 결과의 시작 페이지
|
||||
*/
|
||||
public int getPageNum() {
|
||||
return isEmpty(download) ? pageNum : 0;
|
||||
}
|
||||
|
||||
/**조회 결과의 시작 페이지를 설정한다.
|
||||
* @param pageNum 조회 결과의 시작 페이지
|
||||
* @return QueryRequest
|
||||
*/
|
||||
public <T extends QueryRequest> T setPageNum(int pageNum) {
|
||||
this.pageNum = pageNum;
|
||||
return self();
|
||||
}
|
||||
|
||||
/**한 번에 가지고 올 결과 갯수를 반환한다.
|
||||
* @return fetchSize 한 번에 가지고 올 결과 갯수
|
||||
*/
|
||||
public int getFetchSize() {
|
||||
return isEmpty(download) ? fetchSize : 0;
|
||||
}
|
||||
|
||||
/**한 번에 가지고 올 결과 갯수를 설정한다.
|
||||
* @param fetchSize 한 번에 가지고 올 결과 갯수
|
||||
* @return QueryRequest
|
||||
*/
|
||||
public <T extends QueryRequest> T setFetchSize(int fetchSize) {
|
||||
this.fetchSize = fetchSize;
|
||||
return self();
|
||||
}
|
||||
|
||||
/**조회결과를 다운로드할 파일 유형을 반환한다.
|
||||
* @return download 다운로드 파일 유형
|
||||
* <ul><li>엑셀 파일 - "xls"</li>
|
||||
* <li>다운로드하지 않으면 빈 값</li>
|
||||
* </ul>
|
||||
*/
|
||||
public String getDownload() {
|
||||
return download;
|
||||
}
|
||||
|
||||
/**조회결과를 다운로드할 유형을 설정한다.
|
||||
* @param download 다운로드 파일 유형
|
||||
* <ul><li>엑셀 파일 - "xls"</li>
|
||||
* <li>다운로드하지 않으면 빈 값</li>
|
||||
* </ul>
|
||||
* @return QueryRequest
|
||||
*/
|
||||
public <T extends QueryRequest> T setDownload(String download) {
|
||||
this.download = download;
|
||||
return self();
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package cokr.xit.foundation.component;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**서비스 요청을 위한 정보를 갖는 객체의 베이스 클래스.<br />
|
||||
* 서비스를 요청할 때 전달해야할 정보가 많을 경우(4개 이상) 이 클래스를 상속받아 요청을 정의한다.<br />
|
||||
* 전달해야할 정보가 많지 않으면 서비스 요청을 정의할 필요없다.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public abstract class ServiceRequest extends AbstractComponent implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String type;
|
||||
|
||||
/**요청유형을 반환한다. 요청유형은 애플리케이션에서 필요에 따라 정의한다.
|
||||
* @return 요청유형
|
||||
*/
|
||||
protected String getType() {
|
||||
return ifEmpty(type, "");
|
||||
}
|
||||
|
||||
/**요청유형을 설정한다. 요청유형은 애플리케이션에서 필요에 따라 정의한다.
|
||||
* @param type 요청유형
|
||||
* @return 현재 요청
|
||||
*/
|
||||
protected <T extends ServiceRequest> T setType(String type) {
|
||||
this.type = type;
|
||||
return self();
|
||||
}
|
||||
|
||||
/**요청의 상태값에 대한 유효성을 체크한다.<br />
|
||||
* 상태값이 유효하지 않으면 RuntimeException을 발생시킨다.
|
||||
*/
|
||||
public void validate() {}
|
||||
|
||||
/**요청의 상태값이 유효한지 반환한다.
|
||||
* @return 요청의 상태값의 유효 여부
|
||||
* <ul><li>요청의 상태값이 유효하면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isValid() {
|
||||
try {
|
||||
validate();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package cokr.xit.foundation.component;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**서비스 응답으로 반환하는 정보를 갖는 객체의 베이스 클래스.<br />
|
||||
* 서비스 결과로 전달해야할 정보가 여러 개일 경우 이 클래스를 상속받아 응답클래스를 정의한다.<br />
|
||||
* 결과 정보가 하나일 경우는 응답클래스를 정의할 필요없다.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public abstract class ServiceResponse extends AbstractComponent implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
/**서비스 선언 및 구현을 위한 주요 구성요소들을 정의.
|
||||
*/
|
||||
package cokr.xit.foundation.component;
|
@ -0,0 +1,190 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**문자열을 지정하는 charset의 바이트 배열로 처리 시 사용하는 유틸리티`
|
||||
* <p>다음은 시스템 기본 문자셋의 문자열을 euc-kr의 문자열로 변환하는 예다.
|
||||
* <pre><code> String str = "뿌리 깊은 나무는 바람에 아니...";
|
||||
* BSTR euc_kr = new BSTR("euc-kr").set(str);
|
||||
* String converted = euc_kr.string();
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>다음은 위 예에서 변환된 문자열의 처음부터 24바이트 길이만큼 가져오는 예다.
|
||||
* <pre><code> String str24 = euc_kr.string(24);
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>다음은 위 예에서 변환된 문자열의 25바이트 10바이트 길이만큼 가져오는 예다.
|
||||
* <pre><code> String substr = euc_kr.string(25, 10);
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>다음은 위 예에서 변환된 문자열의 36바이트부터 54바이트까지 가져오는 예다.
|
||||
* <pre><code> substr = euc_kr.substring(36, 54);
|
||||
* </code></pre>
|
||||
*
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class BSTR extends AbstractComponent {
|
||||
private static final byte[] EMPTY = new byte[0];
|
||||
private Charset charset;
|
||||
private byte[] bytes;
|
||||
private String str;
|
||||
private StringBuilder buff;
|
||||
|
||||
/**새 BSTR을 생성한다.
|
||||
* @param charset 문자셋
|
||||
*/
|
||||
public BSTR(Charset charset) {
|
||||
this.charset = ifEmpty(charset, Charset::defaultCharset);
|
||||
}
|
||||
|
||||
/**새 BSTR을 생성한다.
|
||||
* @param charset 문자셋 이름
|
||||
*/
|
||||
public BSTR(String charset) {
|
||||
this(charset != null ? Charset.forName(charset) : null);
|
||||
}
|
||||
|
||||
/**처리할 문자열을 설정한다. 문자열은 {@link #BSTR(Charset) 설정한 문자셋}의 바이트 배열로 변환 후 처리한다.
|
||||
* @param obj 문자열
|
||||
* @return BSTR
|
||||
*/
|
||||
public BSTR set(Object obj) {
|
||||
String s = obj != null ? obj.toString() : null;
|
||||
return set(s != null ? s.getBytes(charset) : null);
|
||||
}
|
||||
|
||||
/**처리할 문자열의 바이트 배열을 설정한다.
|
||||
* @param bytes 처리할 문자열의 바이트 배열
|
||||
* @return BSTR
|
||||
*/
|
||||
public BSTR set(byte[] bytes) {
|
||||
this.bytes = bytes != null ? bytes : EMPTY;
|
||||
str = null;
|
||||
return initBuffer();
|
||||
}
|
||||
|
||||
private BSTR initBuffer() {
|
||||
if (buff == null)
|
||||
buff = new StringBuilder();
|
||||
else
|
||||
buff.delete(0, buff.length());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**설정된 문자열의 바이트 길이를 반환한다.
|
||||
* @return 설정된 문자열의 바이트 길이
|
||||
*/
|
||||
public long byteLength() {
|
||||
return bytes.length;
|
||||
}
|
||||
|
||||
/**설정된 문자열이 비어있는지 반환한다.
|
||||
* @return
|
||||
* <ul><li>문자열이 비어있으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return byteLength() < 1;
|
||||
}
|
||||
|
||||
/**설정된 문자셋으로 변환한 문자열을 반환한다.
|
||||
* @return 설정된 문자셋으로 변환한 문자열
|
||||
*/
|
||||
public String string() {
|
||||
return ifEmpty(str, () -> str = new String(bytes, charset));
|
||||
}
|
||||
|
||||
/**지정한 바이트 길이의 문자열을 반환한다.
|
||||
* @param byteLength 바이트 길이
|
||||
* @return 지정한 바이트 길이의 문자열
|
||||
*/
|
||||
public String string(int byteLength) {
|
||||
long diff = byteLength - byteLength();
|
||||
|
||||
if (diff > 0) {
|
||||
initBuffer();
|
||||
buff.append(string());
|
||||
for (long i = 0; i < diff; ++i)
|
||||
buff.append(' ');
|
||||
return buff.toString();
|
||||
}
|
||||
|
||||
if (diff < 0)
|
||||
return substring(0, byteLength);
|
||||
|
||||
return string();
|
||||
}
|
||||
|
||||
/**시작위치부터 길이의 바이트로 문자열을 생성하여 반환한다.
|
||||
* @param beginIndex 시작위치(0부터 시작)
|
||||
* @param byteLength 바이트 길이
|
||||
* @return 시작위치부터 길이의 바이트로 생성한 문자열
|
||||
*/
|
||||
public String string(int beginIndex, int byteLength) {
|
||||
return substring(beginIndex, beginIndex + byteLength);
|
||||
}
|
||||
|
||||
/**시작위치부터 끝위치 까지의 바이트로 문자열을 생성하여 반환한다.
|
||||
* @param beginIndex 시작위치
|
||||
* @param endIndex 끝위치
|
||||
* @return 시작위치부터 끝위치 까지의 바이트로 생성한 문자열
|
||||
*/
|
||||
public String substring(int beginIndex, int endIndex) {
|
||||
if (endIndex > byteLength())
|
||||
throw new IndexOutOfBoundsException(String.format("beginIndex: %d, endIndex:%d", beginIndex, endIndex));
|
||||
|
||||
int size = 0;
|
||||
int length = endIndex - beginIndex;
|
||||
initBuffer();
|
||||
byte[] copy = Arrays.copyOfRange(bytes, beginIndex, endIndex);
|
||||
|
||||
for (char ch: new String(copy, charset).toCharArray()) {
|
||||
int s = String.valueOf(ch).getBytes(charset).length;
|
||||
if ((size + s) > length) break;
|
||||
|
||||
size += s;
|
||||
buff.append(readable(ch) ? ch : ' ');
|
||||
}
|
||||
|
||||
for (int i = 0, diff = length - size; i < diff; ++i)
|
||||
buff.append(' ');
|
||||
|
||||
return buff.toString();
|
||||
}
|
||||
|
||||
private static boolean readable(char ch) {
|
||||
if (Character.isLetterOrDigit(ch))
|
||||
return true;
|
||||
|
||||
int type = Character.getType(ch);
|
||||
return type >= Character.DASH_PUNCTUATION && type <= Character.MODIFIER_SYMBOL;
|
||||
}
|
||||
|
||||
/**지정한 길이의 숫자열을 반환한다.
|
||||
* @param length 숫자열 길이
|
||||
* @return 지정한 길이의 숫자열
|
||||
*/
|
||||
public String number(int length) {
|
||||
String num = string().replace(" ", "0");
|
||||
int diff = length - num.length();
|
||||
|
||||
if (diff > 0) {
|
||||
initBuffer();
|
||||
for (long i = 0; i < diff; ++i)
|
||||
buff.append(num);
|
||||
return buff.toString();
|
||||
}
|
||||
if (diff < 0)
|
||||
return num.substring(0, length);
|
||||
return num;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return string();
|
||||
}
|
||||
}
|
@ -0,0 +1,279 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
|
||||
import cokr.xit.foundation.Assert;
|
||||
|
||||
/**데이터 변환 유틸리티
|
||||
*/
|
||||
public class Convert {
|
||||
private Convert() {}
|
||||
/**주어진 문자열을 주어진 클래스의 객체로 변환한다.
|
||||
* @param klass 클래스
|
||||
* @param s 문자열
|
||||
* @return 클래스의 객체
|
||||
*/
|
||||
public static final Object toObject(Class<?> klass, String s) {
|
||||
if (Assert.isEmpty(s)) return null;
|
||||
if (String.class.equals(Assert.notEmpty(klass, "klass"))) return s;
|
||||
|
||||
s = s.trim();
|
||||
if (isNumber(klass)) {
|
||||
s = s.replace(",", "");
|
||||
if (BigDecimal.class.equals(klass)) return new BigDecimal(s);
|
||||
else if (Double.class.equals(klass)) return Double.valueOf(s);
|
||||
else if (Float.class.equals(klass)) return Float.valueOf(s);
|
||||
else if (BigInteger.class.equals(klass)) return new BigInteger(s);
|
||||
else if (Integer.class.equals(klass)) return Integer.valueOf(s);
|
||||
else if (Long.class.equals(klass)) return Long.valueOf(s);
|
||||
else if (Short.class.equals(klass)) return Short.valueOf(s);
|
||||
else if (Byte.class.equals(klass)) return Byte.valueOf(s);
|
||||
} else {
|
||||
if (Boolean.class.equals(klass)) return Boolean.valueOf(s);
|
||||
else if (Character.class.equals(klass)) return Character.valueOf(s.charAt(0));
|
||||
/*
|
||||
if (Clob.class.isAssignableFrom(klass)) return s;
|
||||
else if (Date.class.equals(klass)) return Date.valueOf(s);
|
||||
else if (Time.class.equals(klass)) return Time.valueOf(s);
|
||||
else if (Timestamp.class.equals(klass)) return Timestamp.valueOf(s);
|
||||
*/
|
||||
}
|
||||
throw inconvertible(s, klass);
|
||||
}
|
||||
|
||||
/**주어진 클래스가 Number 클래스인지 반환한다.
|
||||
* @param klass 클래스
|
||||
* @return
|
||||
* <ul><li>클래스가 Number 클래스면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final boolean isNumber(Class<?> klass) {
|
||||
return Number.class.isAssignableFrom(klass);
|
||||
}
|
||||
|
||||
/**주어진 객체를 문자열로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return 객체의 문자열 표시
|
||||
*/
|
||||
public static final String toString(Object obj) {
|
||||
if (Assert.isEmpty(obj))
|
||||
return "";
|
||||
if (obj instanceof String)
|
||||
return String.class.cast(obj);
|
||||
else
|
||||
return obj.toString();
|
||||
}
|
||||
|
||||
/**주어진 객체를 Number 객체로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>Number 객체를 변환한 Number</li>
|
||||
* <li>객체가 비어있으면 Integer(0)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final Number toNumber(Object obj) {
|
||||
if (Assert.isEmpty(obj))
|
||||
return Integer.valueOf(0);
|
||||
if (obj instanceof Number)
|
||||
return Number.class.cast(obj);
|
||||
if (obj instanceof String) {
|
||||
String s = (String)obj;
|
||||
return Double.valueOf(s.replace(",", "").trim());
|
||||
}
|
||||
throw inconvertible(obj, Number.class);
|
||||
}
|
||||
|
||||
/**주어진 객체를 short값으로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>객체를 변환한 short값</li>
|
||||
* <li>객체가 비어있으면 0</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final short toShort(Object obj) {
|
||||
return toNumber(obj).shortValue();
|
||||
}
|
||||
|
||||
/**주어진 객체를 int값으로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>객체를 변환한 int값</li>
|
||||
* <li>객체가 비어있으면 0</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final int toInt(Object obj) {
|
||||
return toNumber(obj).intValue();
|
||||
}
|
||||
|
||||
/**주어진 객체를 long값으로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>객체를 변환한 long값</li>
|
||||
* <li>객체가 비어있으면 0</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final long toLong(Object obj) {
|
||||
return toNumber(obj).longValue();
|
||||
}
|
||||
|
||||
/**주어진 객체를 long값으로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>객체를 변환한 long값</li>
|
||||
* <li>객체가 비어있으면 0</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final double toDouble(Object obj) {
|
||||
return toNumber(obj).doubleValue();
|
||||
}
|
||||
|
||||
/**주어진 객체를 float값으로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>객체를 변환한 float값</li>
|
||||
* <li>객체가 비어있으면 0</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final float toFloat(Object obj) {
|
||||
return toNumber(obj).floatValue();
|
||||
}
|
||||
|
||||
/**주어진 객체를 byte값으로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>객체를 변환한 byte값</li>
|
||||
* <li>객체가 비어있으면 0</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final byte toByte(Object obj) {
|
||||
return toNumber(obj).byteValue();
|
||||
}
|
||||
|
||||
/**주어진 객체를 boolean값으로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return
|
||||
* <ul><li>객체를 변환한 boolean값</li>
|
||||
* <li>객체가 비어있으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final boolean toBoolean(Object obj) {
|
||||
if (Assert.isEmpty(obj))
|
||||
return false;
|
||||
Boolean bool = obj instanceof Boolean ? Boolean.class.cast(obj) : Boolean.valueOf(obj.toString());
|
||||
return bool.booleanValue();
|
||||
}
|
||||
|
||||
private static final HashMap<Class<?>, Object> ifnull = new HashMap<>();
|
||||
static {
|
||||
ifnull.put(char.class, Character.valueOf(' '));
|
||||
ifnull.put(byte.class, Byte.valueOf("0"));
|
||||
ifnull.put(short.class, Short.valueOf("0"));
|
||||
ifnull.put(int.class, Integer.valueOf(0));
|
||||
ifnull.put(long.class, Long.valueOf(0));
|
||||
ifnull.put(float.class, Float.valueOf(0));
|
||||
ifnull.put(double.class, Double.valueOf(0));
|
||||
ifnull.put(boolean.class, Boolean.FALSE);
|
||||
}
|
||||
/**주어진 wrapper 클래스에 상응하는 기본형의 디폴트값을 반환한다.
|
||||
* @param klass 기본형 데이터의 wrapper 클래스
|
||||
* @return
|
||||
* <ul><li>wrapper 클래스에 상응하는 기본형의 디폴트값</li>
|
||||
* <li>클래스가 기본형의 wrapper가 아니면 null</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final Object primitiveDefault(Class<?> klass) {
|
||||
return klass != null && klass.isPrimitive() ? ifnull.get(klass) : null;
|
||||
}
|
||||
|
||||
/**주어진 문자열의 \r, \n, \t, 큰 따옴표를 이스케이프하여 반환한다.
|
||||
* @param s 문자열
|
||||
* @return \r, \n, \t, 큰 따옴표를 이스케이프한 문자열
|
||||
*/
|
||||
public static final String rntq(String s) {
|
||||
return Assert.isEmpty(s) ? "" :
|
||||
s.replace("\\","\\\\")
|
||||
.replace("\r","\\r")
|
||||
.replace("\n","\\n")
|
||||
.replace("\t","\\t")
|
||||
.replace("\"","\\\"");
|
||||
}
|
||||
|
||||
private static RuntimeException inconvertible(Object obj, Class<?> klass) {
|
||||
return new RuntimeException("Inconvertible: " + obj + " to " + klass);
|
||||
}
|
||||
|
||||
/**{@link Convert}에 대한 인터페이스.
|
||||
* <p>이 인터페이스의 모든 메소드는 디폴트로 Convert 클래스의 메소드를 호출하도록 구현돼 있다.<br />
|
||||
* Assert를 상속받지 않는 클래스에서 쉽게 Convert의 메소드를 사용하도록 하기 위한 것이다.
|
||||
* </p>
|
||||
*/
|
||||
public static interface Support {
|
||||
/**{@link Convert#toObject(Class, String)} 참고.*/
|
||||
default Object toObject(Class<?> klass, String s) {
|
||||
return Convert.toObject(klass, s);
|
||||
}
|
||||
|
||||
/**{@link Convert#isNumber(Class)} 참고.*/
|
||||
default boolean isNumber(Class<?> klass) {
|
||||
return Convert.isNumber(klass);
|
||||
}
|
||||
|
||||
/**{@link Convert#toString(Object)} 참고.*/
|
||||
default String toString(Object obj) {
|
||||
return Convert.toString(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toNumber(Object)} 참고.*/
|
||||
default Number toNumber(Object obj) {
|
||||
return Convert.toNumber(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toShort(Object)} 참고.*/
|
||||
default short toShort(Object obj) {
|
||||
return Convert.toShort(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toInt(Object)} 참고.*/
|
||||
default int toInt(Object obj) {
|
||||
return Convert.toInt(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toLong(Object)} 참고.*/
|
||||
default long toLong(Object obj) {
|
||||
return Convert.toLong(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toDouble(Object)} 참고.*/
|
||||
default double toDouble(Object obj) {
|
||||
return Convert.toDouble(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toFloat(Object)} 참고.*/
|
||||
default float toFloat(Object obj) {
|
||||
return Convert.toFloat(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toByte(Object)} 참고.*/
|
||||
default byte toByte(Object obj) {
|
||||
return Convert.toByte(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#toBoolean(Object)} 참고.*/
|
||||
default boolean toBoolean(Object obj) {
|
||||
return Convert.toBoolean(obj);
|
||||
}
|
||||
|
||||
/**{@link Convert#primitiveDefault(Class)} 참고.*/
|
||||
default Object primitiveDefault(Class<?> klass) {
|
||||
return Convert.primitiveDefault(klass);
|
||||
}
|
||||
|
||||
/**{@link Convert#rntq(String)} 참고.*/
|
||||
default String rntq(String s) {
|
||||
return Convert.rntq(s);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
public class DataFormat extends AbstractComponent {
|
||||
/**'-'로 구분된 법인등록번호를 반환한다.
|
||||
* @param str '-'이 없는 법인등록번호
|
||||
* @return '-'로 구분된 법인등록번호
|
||||
*/
|
||||
public static final String corporateNo(String str) {
|
||||
return !isEmpty(str) ? str.substring(0, 6) + "-" + str.substring(6) : "";
|
||||
}
|
||||
|
||||
/**'-'로 구분된 사업자등록번호를 반환한다.
|
||||
* @param str '-'이 없는 사업자등록번호
|
||||
* @return '-'로 구분된 사업자등록번호
|
||||
*/
|
||||
public static final String businessNo(String str) {
|
||||
return !isEmpty(str) ? str.substring(0, 3) + "-" + str.substring(3, 5) + "-" + str.substring(5) : "";
|
||||
}
|
||||
|
||||
|
||||
/**'-'로 구분된 날짜(yyyy-MM-dd)를 반환한다.
|
||||
* @param str '-'이 없는 날짜
|
||||
* @return '-'로 구분된 날짜
|
||||
*/
|
||||
public static final String yyyy_mm_dd(String str) {
|
||||
if (isEmpty(str)) return "";
|
||||
|
||||
return str.substring(0, 4) + "-" + str.substring(4, 6) + "-" + str.substring(6);
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import cokr.xit.foundation.Assert;
|
||||
|
||||
/**<p>기본 데이터 오브젝트.
|
||||
* </p>
|
||||
* <p>필드값에 접근할 때 사용하는 필드이름은 대소문자를 구분하지 않는다.
|
||||
* </p>
|
||||
*/
|
||||
public class DataObject extends StringMap<Object> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**map의 key와 value로 설정된 DataObject를 생성 반환한다.
|
||||
* @param map Map
|
||||
* @return DataObject
|
||||
*/
|
||||
public static DataObject create(Map<String, ?> map) {
|
||||
if (map == null) return null;
|
||||
if (map instanceof DataObject) return (DataObject)map;
|
||||
|
||||
DataObject result = new DataObject();
|
||||
map.forEach(result::put);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**DataObject를 생성한다.
|
||||
*/
|
||||
public DataObject() {
|
||||
caseSensitiveKey(false);
|
||||
}
|
||||
|
||||
/**lv와 rv가 같은지 반환한다.
|
||||
* @param lv 비교항의 좌측값
|
||||
* @param rv 비교항의 우측값
|
||||
* @return
|
||||
* <ul><li>lv와 rv가 같으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected static boolean equals(Object lv, Object rv) {
|
||||
return lv == rv ? true : lv != null && lv.equals(rv);
|
||||
}
|
||||
|
||||
/**{@link Assert#isEmpty(Object) Assert.isEmpty(...)} 참고.
|
||||
*/
|
||||
protected static boolean isEmpty(Object obj) {
|
||||
return Assert.isEmpty(obj);
|
||||
}
|
||||
|
||||
/**{@link Assert#ifEmpty(Object, Supplier) Assert.ifEmpty(...)} 참고*/
|
||||
protected static <T> T ifEmpty(T t, Supplier<T> nt) {
|
||||
return Assert.ifEmpty(t, nt);
|
||||
}
|
||||
|
||||
/**{@link Assert#notEmpty(Object, String) Assert.notEmpty(...)} 참고.
|
||||
*/
|
||||
protected static <T> T notEmpty(T t, String name) {
|
||||
return Assert.notEmpty(t, name);
|
||||
}
|
||||
|
||||
/**지정하는 이름의 값들이 빈 값인지 확인한다.
|
||||
* @param names 비어있는지 확인할 값들의 이름
|
||||
* @return DataObject 자신
|
||||
*/
|
||||
public DataObject notEmpty(String... names) {
|
||||
for (String name: names)
|
||||
notEmpty(get(name), name);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
/**지정하는 이름의 값을 T로 캐스팅하여 반환한다.
|
||||
* @param name 필드 이름
|
||||
* @return T로 캐스팅한 값
|
||||
* @throws ClassCastException
|
||||
*/
|
||||
public <T> T value(String name) {
|
||||
return (T)get(name);
|
||||
}
|
||||
|
||||
/**지정하는 이름의 값을 String으로 변환하여 반환한다.
|
||||
* @param name 필드 이름
|
||||
* @return String으로 변환한 값
|
||||
*/
|
||||
public String string(String name) {
|
||||
Object obj = get(name);
|
||||
return isEmpty(obj) ? "" : obj instanceof String ? (String)obj : obj.toString().trim();
|
||||
}
|
||||
|
||||
/**지정하는 이름의 값을 Number로 변환하여 반환한다.
|
||||
* @param name 필드 이름
|
||||
* @return
|
||||
* <ul><li>Number로 변환한 값</li>
|
||||
* <li>값이 비어있을 경우 Integer(0)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public Number number(String name) {
|
||||
Object obj = get(name);
|
||||
if (obj == null)
|
||||
return Integer.valueOf(0);
|
||||
if (obj instanceof Number)
|
||||
return Number.class.cast(obj);
|
||||
if (obj instanceof String)
|
||||
return Double.valueOf((String)obj);
|
||||
throw new RuntimeException("The Object named '" + name + "' is not a Number");
|
||||
}
|
||||
|
||||
/**지정한 이름의 필드값을 boolean값으로 변환하여 반환한다.
|
||||
* @param name 필드 이름
|
||||
* @return
|
||||
* <ul><li>boolean 변환한 값</li>
|
||||
* <li>빈 값이면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean bool(String name) {
|
||||
Object obj = get(name);
|
||||
if (obj == null)
|
||||
return false;
|
||||
|
||||
Boolean bool = obj instanceof Boolean ? Boolean.class.cast(obj) : Boolean.valueOf(obj.toString());
|
||||
return bool.booleanValue();
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser.Feature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**JSON 포맷/파싱 유틸리티.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class JSON extends AbstractComponent {
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**JSON 포맷/파싱에 사용하는 ObjectMapper를 반환한다.
|
||||
* @return JSON 포맷/파싱에 사용하는 ObjectMapper
|
||||
*/
|
||||
public ObjectMapper getObjectMapper() {
|
||||
if (objectMapper == null) {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper.configure(Feature.ALLOW_COMMENTS, true);
|
||||
}
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
/**JSON 포맷/파싱에 사용하는 ObjectMapper를 설정한다.
|
||||
* @param objectMapper JSON 포맷/파싱에 사용하는 ObjectMapper
|
||||
* @return 현재 JSON 객체
|
||||
*/
|
||||
public JSON setObjectMapper(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**JSON 문자열을 파싱하여 주어진 클래스의 객체로 변환한다.
|
||||
* @param <T> 클래스 유형
|
||||
* @param json JSON 문자열
|
||||
* @param klass 클래스
|
||||
* @return 문자열을 파싱하여 변환한 객체
|
||||
*/
|
||||
public <T> T parse(String json, Class<T> klass) {
|
||||
try {
|
||||
return getObjectMapper().readValue(json, klass);
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**input의 내용을 파싱하여 주어진 클래스의 객체로 변환한다.
|
||||
* @param <T> 클래스 유형
|
||||
* @param input JSON input
|
||||
* @param klass 클래스
|
||||
* @return input을 파싱하여 변환한 객체
|
||||
*/
|
||||
public <T> T parse(InputStream input, Class<T> klass) {
|
||||
try {
|
||||
return getObjectMapper().readValue(input, klass);
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**주어진 객체를 JSON 문자열로 변환한다.
|
||||
* @param obj 객체
|
||||
* @return 객체를 변환한 JSON 문자열
|
||||
*/
|
||||
public String stringify(Object obj) {
|
||||
return stringify(obj, false);
|
||||
}
|
||||
|
||||
/**주어진 객체를 JSON 문자열로 변환한다.
|
||||
* @param obj 객체
|
||||
* @param indent 띄어쓰기 여부
|
||||
* <ul><li>띄어쓰기를 적용하려면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
* @return 객체를 변환한 JSON 문자열
|
||||
*/
|
||||
public String stringify(Object obj, boolean indent) {
|
||||
try {
|
||||
return getObjectMapper().writeValueAsString(obj);
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import cokr.xit.foundation.Assert;
|
||||
|
||||
/**데이터 작업 유형
|
||||
* @author mjkhan
|
||||
*/
|
||||
public enum ProcType {
|
||||
/** 데이터 생성 */
|
||||
CREATE("C"),
|
||||
/** 데이터 수정 */
|
||||
UPDATE("U"),
|
||||
/** 데이터 삭제 */
|
||||
DELETE("D");
|
||||
|
||||
private final String code;
|
||||
|
||||
private ProcType(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**데이터 작업 유형의 코드를 반환한다.
|
||||
* @return 데이터 작업 유형 코드
|
||||
*/
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**코드에 해당하는 ProcType을 반환한다.
|
||||
* @param code 데이터 작업 유형 코드
|
||||
* @return ProcType
|
||||
*/
|
||||
public static ProcType codeOf(String code) {
|
||||
if (Assert.isEmpty(code))
|
||||
return null;
|
||||
|
||||
for (ProcType procType: values()) {
|
||||
if (procType.code.equals(code))
|
||||
return procType;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("code: " + code);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.Clob;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import cokr.xit.foundation.Assert;
|
||||
|
||||
public class RowValueHandler extends BaseTypeHandler<Object> {
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement pstmt, int i, Object parameter, JdbcType jdbcType) throws SQLException {
|
||||
pstmt.setObject(i, parameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
|
||||
Object obj = resultSet.getObject(columnName);
|
||||
return obj instanceof Clob ? clobToString(obj) : obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
|
||||
Object obj = resultSet.getObject(columnIndex);
|
||||
return obj instanceof Clob ? clobToString(obj) : obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(CallableStatement cstmt, int columnIndex) throws SQLException {
|
||||
return cstmt.getObject(columnIndex);
|
||||
}
|
||||
|
||||
private String clobToString(Object obj) {
|
||||
if (!(obj instanceof Clob))
|
||||
return null;
|
||||
|
||||
try {
|
||||
Clob clob = Clob.class.cast(obj);
|
||||
Reader reader = clob.getCharacterStream();
|
||||
char[] chars = new char[1024];
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
while ((reader.read(chars)) != -1)
|
||||
buffer.append(chars);
|
||||
return buffer.toString();
|
||||
} catch (Exception e) {
|
||||
throw Assert.runtimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**String 키와 E값의 Map으로 편의성 메소드들을 추가하였다.<br />
|
||||
* {@link #caseSensitiveKey(boolean) 키의 대소문자 구분여부}를 지정할 수 있다.<br />
|
||||
* 디폴트는 대소문자를 구분하도록 되어있다..
|
||||
* @param <E> 값의 타입
|
||||
*/
|
||||
public class StringMap<E> extends LinkedHashMap<String, E> {
|
||||
/**objs들을 groupMapper가 반환하는 값을 기준으로 grouping하여 반환한다.
|
||||
* @param objs 객체 목록
|
||||
* @param groupMapper 요소 중 키로 쓰일 값을 반환
|
||||
* @return groupMapper가 반환하는 값을 기준으로 grouping하여 만든 결과 Map
|
||||
*/
|
||||
public static <E> Map<String, List<E>> groupBy(Collection<E> objs, Function<E, String> groupMapper) {
|
||||
return objs != null ?
|
||||
objs.stream().collect(Collectors.groupingBy(groupMapper)) :
|
||||
Collections.emptyMap();
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private boolean caseSensitiveKey = true;
|
||||
|
||||
/**키의 대소문자 구분여부를 반환한다.
|
||||
* @return
|
||||
* <ul><li>대소문자를 구분하면 true</li>
|
||||
* <li>대소문자를 구분하지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean caseSensitiveKey() {
|
||||
return caseSensitiveKey;
|
||||
}
|
||||
|
||||
/**키의 대소문자 구분여부를 설정한다.
|
||||
* @param sensitive
|
||||
* <ul><li>대소문자를 구분하려면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
* @return StringMap 자신
|
||||
*/
|
||||
public <T extends StringMap<E>> T caseSensitiveKey(boolean sensitive) {
|
||||
caseSensitiveKey = sensitive;
|
||||
return self();
|
||||
}
|
||||
|
||||
private String findKey(Object obj) {
|
||||
String s = (String)obj;
|
||||
if (caseSensitiveKey)
|
||||
return s;
|
||||
for (String key: keySet())
|
||||
if (key.equalsIgnoreCase(s))
|
||||
return key;
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return super.containsKey(key) ? true : super.containsKey(findKey(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public E get(Object key) {
|
||||
E t = super.get(key);
|
||||
return t != null ? t : super.get(findKey(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public E getOrDefault(Object key, E defaultValue) {
|
||||
return super.getOrDefault(findKey(key), defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E put(String key, E value) {
|
||||
return super.put(findKey(key), value);
|
||||
}
|
||||
|
||||
/**value를 지정하는 key의 값으로 설정한다.
|
||||
* @param key 키
|
||||
* @param value 키로 연결할 값
|
||||
* @return StringMap 자신
|
||||
*/
|
||||
public <T extends StringMap<E>> T set(String key, E value) {
|
||||
put(key, value);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E remove(Object key) {
|
||||
E t = super.remove(key);
|
||||
return t != null ? t : super.remove(findKey(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public E computeIfAbsent(String key, Function<? super String, ? extends E> mappingFunction) {
|
||||
return super.computeIfAbsent(findKey(key), mappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E computeIfPresent(String key, BiFunction<? super String, ? super E, ? extends E> remappingFunction) {
|
||||
return super.computeIfPresent(findKey(key), remappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E merge(String key, E value, BiFunction<? super E, ? super E, ? extends E> remappingFunction) {
|
||||
return super.merge(findKey(key), value, remappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E putIfAbsent(String key, E value) {
|
||||
return super.putIfAbsent(findKey(key), value);
|
||||
}
|
||||
|
||||
/**지정하는 key에 설정된 값이 없으면 value를 설정한다.
|
||||
* @param key 키
|
||||
* @param value 설정할 값
|
||||
* @return StringMap 자신
|
||||
*/
|
||||
public <T extends StringMap<E>> T setIfAbsent(String key, E value) {
|
||||
putIfAbsent(key, value);
|
||||
return self();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T extends StringMap<E>> T self() {
|
||||
return (T)this;
|
||||
}
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
package cokr.xit.foundation.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**XML 포맷/파싱 유틸리티
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class XML extends AbstractComponent {
|
||||
private String charset;
|
||||
private HashMap<Class<?>, Support> xctxes;
|
||||
|
||||
/**새 XML을(를) 생성한다.
|
||||
* @param charset 문자셋 이름. 지정하지 않으면 시스템 기본 문자셋을 사용한다.
|
||||
*/
|
||||
public XML(String charset) {
|
||||
this.charset = ifEmpty(charset, () -> Charset.defaultCharset().name());
|
||||
}
|
||||
|
||||
private Support jaxb(Class<?> klass) throws Exception {
|
||||
if (xctxes == null)
|
||||
xctxes = new HashMap<>();
|
||||
Support support = xctxes.get(klass);
|
||||
if (support == null) {
|
||||
JAXBContext xctx = JAXBContext.newInstance(klass);
|
||||
xctxes.put(klass, support = new Support(xctx, charset));
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
/**xml 문자열을 파싱하여 주어진 클래스의 객체로 변환한다.
|
||||
* @param <T> 클래스 유형
|
||||
* @param str xml 문자열
|
||||
* @param klass 클래스
|
||||
* @return xml 문자열을 변환하여 만든 객체
|
||||
*/
|
||||
public <T> T parse(String str, Class<T> klass) {
|
||||
try {
|
||||
return klass.cast(jaxb(klass).unmarshal(str));
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**xml input의 내용을 파싱하여 주어진 클래스의 객체로 변환한다.
|
||||
* @param <T> 클래스 유형
|
||||
* @param input xml input
|
||||
* @param klass 클래스
|
||||
* @return xml input의 내용을 변환하여 만든 객체
|
||||
*/
|
||||
public <T> T parse(InputStream input, Class<T> klass) {
|
||||
try {
|
||||
return klass.cast(jaxb(klass).unmarshal(input));
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**xml 파일의 내용을 파싱하여 주어진 클래스의 객체로 변환한다.
|
||||
* @param <T> 클래스 유형
|
||||
* @param file xml 파일
|
||||
* @param klass 클래스
|
||||
* @return xml 파일의 내용을 변환하여 만든 객체
|
||||
*/
|
||||
public <T> T parse(File file, Class<T> klass) {
|
||||
try (FileInputStream input = new FileInputStream(file)) {
|
||||
return parse(input, klass);
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**주어진 객체를 xml 문자열로 변환한다.<br />
|
||||
* 이 때 객체는 JAXB(Java Architecture for XML Binding - javax.xml.*)가 제공하는 다음과 같은 어노테이션이 적용되어 있어야 한다.
|
||||
* <pre><code> @XmlRootElement
|
||||
* @XmlType
|
||||
* @XmlElement
|
||||
* @XmlAttribute
|
||||
* @XmlTransient
|
||||
* </code></pre>
|
||||
* @param obj 객체
|
||||
* @return xml 문자열
|
||||
*/
|
||||
public String stringify(Object obj) {
|
||||
try {
|
||||
return jaxb(obj.getClass()).stringify(obj);
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**주어진 객체를 xml 포맷으로 out에 저장한다.<br />
|
||||
* 이 때 객체는 JAXB(Java Architecture for XML Binding - javax.xml.*)가 제공하는 다음과 같은 어노테이션이 적용되어 있어야 한다.
|
||||
* <pre><code> @XmlRootElement
|
||||
* @XmlType
|
||||
* @XmlElement
|
||||
* @XmlAttribute
|
||||
* @XmlTransient
|
||||
* </code></pre>
|
||||
* @param obj 객체
|
||||
* @param out OutputStream
|
||||
*/
|
||||
public void write(Object obj, OutputStream out) {
|
||||
try {
|
||||
jaxb(obj.getClass()).marshal(obj, out);
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**주어진 객체를 xml 포맷으로 파일에 저장한다.<br />
|
||||
* 이 때 객체는 JAXB(Java Architecture for XML Binding - javax.xml.*)가 제공하는 다음과 같은 어노테이션이 적용되어 있어야 한다.
|
||||
* <pre><code> {@code @XmlRootElement}
|
||||
* {@code @XmlType}
|
||||
* {@code @XmlElement}
|
||||
* {@code @XmlAttribute}
|
||||
* {@code @XmlTransient}</code></pre>
|
||||
* @param obj 객체
|
||||
* @param file 파일
|
||||
*/
|
||||
public void write(Object obj, File file) {
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
write(obj, out);
|
||||
} catch (Exception e) {
|
||||
throw runtimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Support {
|
||||
String charset;
|
||||
JAXBContext xctx;
|
||||
Marshaller marshaller;
|
||||
Unmarshaller unmarshaller;
|
||||
|
||||
Support(JAXBContext xctx, String charset) {
|
||||
this.xctx = xctx;
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
Object unmarshal(String input) throws Exception {
|
||||
if (unmarshaller == null) {
|
||||
unmarshaller = xctx.createUnmarshaller();
|
||||
}
|
||||
try (StringReader reader = new StringReader(input)) {
|
||||
return unmarshaller.unmarshal(reader);
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
Object unmarshal(InputStream input) throws Exception {
|
||||
if (unmarshaller == null) {
|
||||
unmarshaller = xctx.createUnmarshaller();
|
||||
}
|
||||
return unmarshaller.unmarshal(input);
|
||||
}
|
||||
|
||||
Marshaller marshaller() throws Exception {
|
||||
if (marshaller == null) {
|
||||
marshaller = xctx.createMarshaller();
|
||||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
|
||||
marshaller.setProperty(Marshaller.JAXB_ENCODING, charset);
|
||||
}
|
||||
return marshaller;
|
||||
}
|
||||
|
||||
String stringify(Object obj) throws Exception {
|
||||
try (StringWriter writer = new StringWriter()) {
|
||||
marshaller().marshal(obj, writer);
|
||||
return writer.toString();
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
void marshal(Object obj, OutputStream out) throws Exception {
|
||||
marshaller().marshal(obj, out);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package cokr.xit.foundation.data.hierarchy;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**계층구조를 이루는 요소들을 모아놓은 것으로 {@link HierarchyBuilder}로 생성한다.<br />
|
||||
* Hierarchy는
|
||||
* <ul><li>{@link #getElements() 계층구조의 구성요소}</li>
|
||||
* <li>{@link #getIndex() 구성요소들의 인덱스}</li>
|
||||
* <li>{@link #topElements() 최상위 구성요소들}</li>
|
||||
* <li>{@link #getTop() 루트 요소}</li>
|
||||
* <li>{@link #get(String) 특정 구성요소}</li>
|
||||
* </ul>
|
||||
* 등을 제공한다.
|
||||
* @param <T> 구성요소 유형
|
||||
*/
|
||||
public class Hierarchy<T> extends AbstractComponent implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**주어진 계층 구성요소들을 문자열로 반환한다.
|
||||
* @param objs 문자열로 변환할 계층 구성요소
|
||||
* @param getChildren 특정요소의 하위요소들을 반환하는 function
|
||||
* @return 계층 구성요소들의 문자열 표현
|
||||
*/
|
||||
public static <T> String toString(Iterable<T> objs, Function<T, Iterable<T>> getChildren) {
|
||||
return new Stringify<T>()
|
||||
.beginElement((e, level, index) -> {
|
||||
String str = index == 0 && level == 0 ? "" : "\n";
|
||||
if (level > 0)
|
||||
str += Stringify.indent(" ", level) + "+-";
|
||||
return str += e;
|
||||
})
|
||||
.getChildren(getChildren)
|
||||
.get(objs);
|
||||
}
|
||||
|
||||
private Collection<T> elements;
|
||||
private List<T> tops;
|
||||
private Map<String, T> index;
|
||||
|
||||
/**Hierarchy가 비어있는지 반환한다.
|
||||
* @return
|
||||
* <ul><li>Hierarchy가 비어있으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return isEmpty(index) || isEmpty(elements);
|
||||
}
|
||||
|
||||
/**Hierarchy 구성요소들의 인덱스를 반환한다.
|
||||
* @return Hierarchy 구성요소들의 인덱스
|
||||
*/
|
||||
public Map<String, T> getIndex() {
|
||||
return ifEmpty(index, () -> Collections.emptyMap());
|
||||
}
|
||||
|
||||
Hierarchy<T> setIndex(Map<String, T> index) {
|
||||
this.index = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**Hierarchy의 구성요소들을 반환한다.
|
||||
* @return Hierarchy의 구성요소
|
||||
*/
|
||||
public Collection<T> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
Hierarchy<T> setElements(Collection<T> elements) {
|
||||
this.elements = elements;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**Hierarchy의 최상위 요소들을 반환한다.
|
||||
* @return Hierarchy의 최상위 요소들
|
||||
*/
|
||||
public List<T> topElements() {
|
||||
return ifEmpty(tops, () -> Collections.emptyList());
|
||||
}
|
||||
|
||||
/**Hierarchy의 루트요소를 반환한다.
|
||||
* @return Hierarchy의 루트요소
|
||||
*/
|
||||
public T getTop() {
|
||||
return !isEmpty() ? tops.iterator().next() : null;
|
||||
}
|
||||
|
||||
Hierarchy<T> setTops(List<T> tops) {
|
||||
this.tops = tops;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**지정하는 아이디의 구성요소를 반환한다.
|
||||
* @param elementID 구성요소의 아이디
|
||||
* @return 지정하는 아이디의 구성요소
|
||||
*/
|
||||
public T get(String elementID) {
|
||||
return !isEmpty() ? index.get(elementID) : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
package cokr.xit.foundation.data.hierarchy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**Hierarchy를 생성한다.
|
||||
* <p>HierarchyBuilder가 Hierarchy를 생성하려면
|
||||
* <ul><li>계층구조를 이루는 {@link #setElements(Collection) 구성요소들을 설정}하고</li>
|
||||
* <li>{@link #setInstruction(Instruction) Instruction}을 설정해서 계층구조를 만드는 정보를 제공해야 한다.</li>
|
||||
* </ul>
|
||||
* {@link Instruction}은 HierarchyBuilder가 Hierarchy를 만들 때 사용하는 일련의 정보를 제공한다.
|
||||
*
|
||||
* <p>구성요소들과 Instruction이 설정되면 HierarchyBuilder는 {@link #build() 계층구조를 생성}할 수 있다.<br />
|
||||
* HierarchyBuilder는 디폴트로 {@link Hierarchy} 객체를 반환한다.<br />
|
||||
* Hierarchy의 상속 클래스를 받으려면 {@link #build(Supplier)} 메소드를 사용한다.<br />
|
||||
* </p>
|
||||
* <p>다음은 HierarchyBuilder가 객체의 계층구조를 생성하는 예다.
|
||||
* <pre class="shade">{@code Collection<MyObject> elements = ...;
|
||||
* HierarchyBuilder.Instruction<MyObject> instruction = new HierarchyBuilder.Instruction<>()
|
||||
* .getID(e -> e.getId())
|
||||
* .getParentID(e -> e.getParentId())
|
||||
* .addChild((parent, child) -> parent.add(child))
|
||||
* .atTop(e -> e.getParentId() == null || e.getId().equals(e.getParentId()));
|
||||
* HierarchyBuilder<MyObject> builder = new HierarchyBuilder<>()
|
||||
* .setInstruction(instruction);
|
||||
* .setElements(elements);
|
||||
* Hierarchy<MyObject> hierarchy = builder.build();
|
||||
* List<MyObject> tops = hierarchy.topElements();
|
||||
* MyObject root = hierarchy.getTop();}</pre>
|
||||
* 위 코드는 다음처럼 간단히 작성할 수 있다:
|
||||
* <pre class="shade">{@code ...
|
||||
* Hierarchy<MyObject> hierarchy = new HierarchyBuilder<MyObject>()
|
||||
* .getID(e -> e.getId())
|
||||
* .getParentID(e -> e.getParentId())
|
||||
* .addChild((parent, child) -> parent.add(child))
|
||||
* .atTop(e -> e.getParentId() == null || e.getId().equals(e.getParentId()))
|
||||
* .setElements(elements)
|
||||
* .build();
|
||||
* ...}</pre>
|
||||
*
|
||||
*/
|
||||
public class HierarchyBuilder<T> {
|
||||
private Collection<T> elements;
|
||||
private HashMap<String, T> index;
|
||||
private Instruction<T> instruction;
|
||||
|
||||
/**계층구조를 이루는 구성요소들을 설정한다.
|
||||
* @param elements 계층구조를 이루는 구성요소
|
||||
* @return HierarchyBuilder
|
||||
*/
|
||||
public HierarchyBuilder<T> setElements(Collection<T> elements) {
|
||||
if (elements == null)
|
||||
throw new IllegalArgumentException("elements: null");
|
||||
this.elements = elements;
|
||||
createIndex();
|
||||
return this;
|
||||
}
|
||||
|
||||
private Instruction<T> instruction() {
|
||||
return instruction != null ? instruction : (instruction = new Instruction<>());
|
||||
}
|
||||
|
||||
/**계층구조를 생성할 때 사용할 Instruction을 설정한다.
|
||||
* @param instruction 계층구조를 생성할 때 사용할 Instruction
|
||||
* @return HierarchyBuilder
|
||||
*/
|
||||
public HierarchyBuilder<T> setInstruction(Instruction<T> instruction) {
|
||||
this.instruction = instruction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**{@link Instruction#atTop(Predicate)} 참고.
|
||||
* @return HierarchyBuilder
|
||||
*/
|
||||
public HierarchyBuilder<T> atTop(Predicate<T> test) {
|
||||
instruction().atTop(test);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**{@link Instruction#getID(Function)} 참고.
|
||||
* @return HierarchyBuilder
|
||||
*/
|
||||
public HierarchyBuilder<T> getID(Function<T, String> func) {
|
||||
instruction().getID(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**{@link Instruction#getParentID(Function)} 참고.
|
||||
* @return HierarchyBuilder
|
||||
*/
|
||||
public HierarchyBuilder<T> getParentID(Function<T, String> func) {
|
||||
instruction().getParentID(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**{@link Instruction#addChild(BiConsumer)} 참고
|
||||
* @return HierarchyBuilder
|
||||
*/
|
||||
public HierarchyBuilder<T> addChild(BiConsumer<T, T> func) {
|
||||
instruction().addChild(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void createIndex() {
|
||||
index = new HashMap<String, T>();
|
||||
for (T e: elements)
|
||||
index.put(instruction.getID.apply(e), e);
|
||||
}
|
||||
|
||||
/**계층의 구성요소들의 인덱스를 반환한다.
|
||||
* @return 계층의 구성요소들의 인덱스
|
||||
*/
|
||||
public Map<String, T> getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
private void add(T parent, T child) {
|
||||
if (parent == null || child == null || parent == child) return;
|
||||
|
||||
instruction.addChild.accept(parent, child);
|
||||
}
|
||||
|
||||
/**계층구조를 생성하고 루트요소를 반환한다.
|
||||
* @param rootID 루트요소의 ID
|
||||
* @return 루트요소
|
||||
*/
|
||||
public T get(String rootID) {
|
||||
if (elements == null) return null;
|
||||
|
||||
T root = index.get(rootID);
|
||||
if (root == null)
|
||||
throw new RuntimeException("Root element not found: " + rootID);
|
||||
|
||||
for (T element: elements) {
|
||||
if (element == null) continue;
|
||||
String parentID = instruction.getParentID.apply(element);
|
||||
T parent = parentID == null ? null : index.get(parentID);
|
||||
add(parent, element);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
/**계층구조를 생성하고 최상위 요소들을 반환한다.
|
||||
* @return 최상위 요소
|
||||
*/
|
||||
public List<T> get() {
|
||||
if (elements == null) return Collections.emptyList();
|
||||
|
||||
ArrayList<T> tops = new ArrayList<T>();
|
||||
for (T e: elements) {
|
||||
boolean top = instruction.atTop != null && instruction.atTop.test(e);
|
||||
if (top) {
|
||||
if (!tops.contains(e))
|
||||
tops.add(e);
|
||||
} else {
|
||||
String parentID = instruction.getParentID.apply(e);
|
||||
T parent = parentID != null ? index.get(parentID) : null;
|
||||
if (parent == null || parent.equals(e)) {
|
||||
if (!tops.contains(e))
|
||||
tops.add(e);
|
||||
} else {
|
||||
add(parent, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tops;
|
||||
}
|
||||
|
||||
/**Hierarchy를 생성해서 반환한다.
|
||||
* @return Hierarchy
|
||||
*/
|
||||
public Hierarchy<T> build() {
|
||||
return build(Hierarchy::new);
|
||||
}
|
||||
|
||||
/**Hierarchy 상속 클래스의 객체를 생성해서 반환한다.<br />
|
||||
* The Hierarchy is an instance of the class that the factory supplies.
|
||||
* @param factory Hierarchy 객체를 반환하는 함수
|
||||
* @return Hierarchy
|
||||
*/
|
||||
public <H extends Hierarchy<T>> H build(Supplier<H> factory) {
|
||||
List<T> tops = get();
|
||||
H h = factory.get();
|
||||
h.setElements(elements).setIndex(index).setTops(tops);
|
||||
return h;
|
||||
}
|
||||
|
||||
/**HierarchyBuilder를 초기화 한다.
|
||||
*/
|
||||
public void clear() {
|
||||
if (index != null)
|
||||
index.clear();
|
||||
if (instruction != null)
|
||||
instruction.clear();
|
||||
elements = null;
|
||||
}
|
||||
|
||||
/**HierarchyBuilder가 Hierarchy를 생성할 때 사용하는 정보로
|
||||
* <ul><li>{@link #getID(Function) 구성요소의 ID}</li>
|
||||
* <li>{@link #getParentID(Function) 상위요소의 ID}</li>
|
||||
* <li>{@link #addChild(BiConsumer) 구성요소를 상위요소에 추가}하는 방법</li>
|
||||
* <li>{@link #atTop(Predicate) 구성요소가 최상위 요소인지 판단}하는 방법</li>
|
||||
* </ul>
|
||||
* 등을 명시한다.
|
||||
* @param <T> 구성요소 유형
|
||||
*/
|
||||
public static class Instruction<T> {
|
||||
private Function<T, String>
|
||||
getID,
|
||||
getParentID;
|
||||
private BiConsumer<T, T> addChild;
|
||||
private Predicate<T> atTop;
|
||||
|
||||
/**새 Instruction을 생성한다.
|
||||
*/
|
||||
public Instruction() {
|
||||
clear();
|
||||
}
|
||||
|
||||
/**구성요소의 ID를 반환하는 함수를 설정한다.<br />
|
||||
* ID는 구성요소의 실제 ID일 수도, 아닐 수도 있다.<br />
|
||||
* HierarchyBuilder가 구성요소를 식별할 수만 있으면 된다.<br />
|
||||
* @param func 구성요소의 ID를 반환하는 함수
|
||||
* @return Instruction
|
||||
*/
|
||||
public Instruction<T> getID(Function<T, String> func) {
|
||||
this.getID = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**상위 구성요소의 ID를 반환하는 함수를 설정한다.
|
||||
* @param func 상위 구성요소의 ID를 반환하는 함수
|
||||
* @return Instruction
|
||||
*/
|
||||
public Instruction<T> getParentID(Function<T, String> func) {
|
||||
this.getParentID = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**구성요소를 상위요소에 추가하는 함수를 설정한다.
|
||||
* @param consumer 구성요소를 상위요소에 추가하는 함수로, 첫번째 인자가 상위요소, 두번째 인자가 하위요소다.
|
||||
* @return Instruction
|
||||
*/
|
||||
public Instruction<T> addChild(BiConsumer<T, T> consumer) {
|
||||
this.addChild = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**구성요소가 최상위 요소인지 판단하는 함수를 설정한다.<br />
|
||||
* HierarchyBuilder는 이 함수의 결과가 true이거나 상위요소를 찾지못한 요소를 최상위 요소로 간주한다.
|
||||
* @param test 구성요소가 최상위 요소인지 판단하는 함수
|
||||
* @return Instruction
|
||||
*/
|
||||
public Instruction<T> atTop(Predicate<T> test) {
|
||||
this.atTop = test;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**Instruction을 초기화한다.
|
||||
* @return Instruction
|
||||
*/
|
||||
public Instruction<T> clear() {
|
||||
getID = (t) -> "";
|
||||
getParentID = (t) -> "";
|
||||
addChild = (t1, t2) -> {};
|
||||
atTop = null;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
package cokr.xit.foundation.data.hierarchy;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.function.Function;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**Utility to convert objects of a hierarchy to string representation.
|
||||
* <p>For a Stringify to convert objects of a hierarchy, you should
|
||||
* <ul><li>Set an {@link #setInstruction(Instruction) Instruction} for steps to convert the objects</li>
|
||||
* <li>Call {@link #get(Iterable)} with the objects as an argument</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>An {@link Instruction} is a set of steps for a Stringify to perform the conversion.<br />
|
||||
* For convenience, a Stringify has methods that sets the steps to an internal instruction.
|
||||
* </p>
|
||||
*/
|
||||
class Stringify<T> extends AbstractComponent {
|
||||
/**A functional interface that converts an object to a string
|
||||
* @param <T> an object type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ToString<T> {
|
||||
/**Converts t to a string.
|
||||
* @param t an object
|
||||
* @param level 0-based level or depth of t in a hierarchy
|
||||
* @param index 0-based index of t
|
||||
* @return string representation of t
|
||||
*/
|
||||
String get(T t, int level, int index);
|
||||
}
|
||||
|
||||
private Instruction<T> instruction;
|
||||
|
||||
/**Returns indentation with s appended n times.
|
||||
* @param s indentation string
|
||||
* @param n number of times to append indentation string
|
||||
* @return indentation string appended n time
|
||||
*/
|
||||
public static String indent(String s, int n) {
|
||||
String result = "";
|
||||
for (int i = 0; i < n; ++i) {
|
||||
result += s;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Instruction<T> instruction() {
|
||||
return instruction != null ? instruction : (instruction = new Instruction<>());
|
||||
}
|
||||
|
||||
/**Sets an instruction to convert objects to string representation.
|
||||
* @param instruction instruction to convert objects to string representation
|
||||
* @return this Stringify
|
||||
*/
|
||||
public Stringify<T> setInstruction(Instruction<T> instruction) {
|
||||
this.instruction = instruction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**See {@link Instruction#beginElement(ToString)}
|
||||
* @return this Stringify
|
||||
*/
|
||||
public Stringify<T> beginElement(ToString<T> func) {
|
||||
instruction().beginElement(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**See {@link Instruction#endElement(ToString)}
|
||||
* @return this Stringify
|
||||
*/
|
||||
public Stringify<T> endElement(ToString<T> func) {
|
||||
instruction().endElement(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**See {@link Instruction#beginChildren(ToString)}
|
||||
* @return this Stringify
|
||||
*/
|
||||
public Stringify<T> beginChildren(ToString<T> func) {
|
||||
instruction().beginChildren(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**See {@link Instruction#endChildren(ToString)}
|
||||
* @return this Stringify
|
||||
*/
|
||||
public Stringify<T> endChildren(ToString<T> func) {
|
||||
instruction().endChildren(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**See {@link Instruction#getChildren(Function)}
|
||||
* @return this Stringify
|
||||
*/
|
||||
public Stringify<T> getChildren(Function<T, Iterable<T>> func) {
|
||||
instruction().getChildren(func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**Converts elements to string representation.
|
||||
* @param elements objects of a hierarchy
|
||||
* @return string representation of the elements
|
||||
*/
|
||||
public String get(Iterable<T> elements) {
|
||||
if (isEmpty(elements)) return "";
|
||||
|
||||
StringBuilder buff = new StringBuilder();
|
||||
toString(elements, buff, 0);
|
||||
return buff.toString();
|
||||
}
|
||||
|
||||
private void toString(Iterable<T> elements, StringBuilder buff, int level) {
|
||||
Instruction<T> instr = instruction();
|
||||
int index = 0;
|
||||
|
||||
for (T e: elements) {
|
||||
buff.append(instr.beginElement().get(e, level, index));
|
||||
|
||||
Iterable<T> children = instr.getChildren().apply(e);
|
||||
int sublevel = level + 1;
|
||||
if (!isEmpty(children)) {
|
||||
buff.append(instr.beginChildren().get(e, sublevel, index));
|
||||
toString(children, buff, sublevel);
|
||||
buff.append(instr.endChildren().get(e, sublevel, index));
|
||||
}
|
||||
buff.append(instr.endElement().get(e, level, index));
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
/**Is a set of steps to convert objects of a hierarchy to string representation.
|
||||
* <p>With an Instruction, you specify how to
|
||||
* <ul><li>{@link #beginElement(ToString) Mark the beginning of an element} with a string</li>
|
||||
* <li>{@link #endElement(ToString) Mark the end of an element} with a string</li>
|
||||
* <li>{@link #getChildren(Function) Get child elements} of an element</li>
|
||||
* <li>{@link #beginChildren(ToString) Mark the beginning of child elements} with a string</li>
|
||||
* <li>{@link #endChildren(ToString) Mark the end of child elements} with a string</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* @param <T> an object type
|
||||
*/
|
||||
public static class Instruction<T> {
|
||||
private ToString<T>
|
||||
beginElement,
|
||||
endElement,
|
||||
beginChildren,
|
||||
endChildren;
|
||||
private Function<T, Iterable<T>> getChildren;
|
||||
|
||||
/**Creates a new Instruction.
|
||||
*/
|
||||
public Instruction() {
|
||||
clear();
|
||||
}
|
||||
|
||||
ToString<T> beginElement() {
|
||||
return beginElement;
|
||||
}
|
||||
|
||||
/**Sets a function that returns a string to mark the beginning of an element.
|
||||
* @param func function that returns a string to mark the beginning of an element
|
||||
* @return this Instruction
|
||||
*/
|
||||
public Instruction<T> beginElement(ToString<T> func) {
|
||||
this.beginElement = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
ToString<T> endElement() {
|
||||
return endElement;
|
||||
}
|
||||
|
||||
/**Sets a function that returns a string to mark the end of an element.
|
||||
* @param func function that returns a string to mark the end of an element
|
||||
* @return this Instruction
|
||||
*/
|
||||
public Instruction<T> endElement(ToString<T> func) {
|
||||
this.endElement = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
ToString<T> beginChildren() {
|
||||
return beginChildren;
|
||||
}
|
||||
|
||||
/**Sets a function that returns a string to mark the beginning of child elements.
|
||||
* @param func function that returns a string to mark the beginning of child elements
|
||||
* @return this Instruction
|
||||
*/
|
||||
public Instruction<T> beginChildren(ToString<T> func) {
|
||||
this.beginChildren = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
ToString<T> endChildren() {
|
||||
return endChildren;
|
||||
}
|
||||
|
||||
/**Sets a function that returns a string to mark the end of child elements.
|
||||
* @param func function that returns a string to mark the end of child elements
|
||||
* @return this Instruction
|
||||
*/
|
||||
public Instruction<T> endChildren(ToString<T> func) {
|
||||
this.endChildren = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
Function<T, Iterable<T>> getChildren() {
|
||||
return getChildren;
|
||||
}
|
||||
|
||||
/**Sets a function that returns child elements of the current element.
|
||||
* @param getChildren function that returns child elements of the current element
|
||||
* @return this Instruction
|
||||
*/
|
||||
public Instruction<T> getChildren(Function<T, Iterable<T>> getChildren) {
|
||||
this.getChildren = getChildren;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**Clears the Instruction to the initial state.
|
||||
* @return this Instruction
|
||||
*/
|
||||
public Instruction<T> clear() {
|
||||
ToString<T> empty = (e, level, index) -> "";
|
||||
beginElement = empty;
|
||||
endElement = empty;
|
||||
beginChildren = empty;
|
||||
endChildren = empty;
|
||||
getChildren = t -> Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
/**계층구조를 이루는 데이터 지원
|
||||
*/
|
||||
package cokr.xit.foundation.data.hierarchy;
|
@ -0,0 +1,3 @@
|
||||
/**데이터 전달 및 변환을 위한 컴포넌트
|
||||
*/
|
||||
package cokr.xit.foundation.data;
|
@ -0,0 +1,226 @@
|
||||
package cokr.xit.foundation.data.paging;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
/**페이징을 위한 정보를 설정하기 위한 리스트.
|
||||
* <p>BoundedList는 대량의 요소를 한 번에 원하는 수만큼 제공할 때 사용한다.<br />
|
||||
* 예를 들어, 대량의 데이터베이스 조회 결과 데이터를 한 번에 20개씩 제공하고자 할 경우 BoundedList를 사용할 수 있다.</p>
|
||||
* <p>이를 위해 개발자는 필요한 수만큼의 데이터를 추출하고 BoundedList에 이 데이터를 담아야 한다.<br />
|
||||
* 그리고 나서 BoundedList에 다음 정보를 설정한다.
|
||||
* <ul><li>원래 데이터의 {@link #setTotalSize(int) 전체 갯수}</li>
|
||||
* <li>{@link #setFetchSize(int) 한 번에 가지고 온 데이터 갯수}</li>
|
||||
* <li>현재 가지고 온 데이터의 {@link #setStart(int) 시작 인덱스}(0부터 시작)</li>
|
||||
* </ul>
|
||||
* <p>BoundedList가 가지고 온 요소를 접근할 때는 0부터 시작하는 인덱스를 사용한다.</p>
|
||||
* <p>BoundedList는 {@link #hasMore() 더 가져올 수 있는} 요소가 {@link #hasNext() 앞으로} 또는 {@link #hasPrevious() 뒤로} 있는지 반환한다.</p>
|
||||
* @param <E> 요소 타입
|
||||
*/
|
||||
public class BoundedList<E> extends ArrayList<E> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int
|
||||
current,
|
||||
fetchSize,
|
||||
start;
|
||||
private long totalSize;
|
||||
|
||||
/**한 번에 가지고 올 요소의 갯수를 반환한다.
|
||||
* @return 한 번에 가지고 올 요소의 갯수
|
||||
*/
|
||||
public int getFetchSize() {
|
||||
return fetchSize;
|
||||
}
|
||||
/**한 번에 가지고 올 요소의 갯수를 설정한다.
|
||||
* @param size 한 번에 가지고 올 요소의 갯수
|
||||
* @return BoundedList 자신
|
||||
*/
|
||||
public BoundedList<E> setFetchSize(int size) {
|
||||
if (size < 0)
|
||||
throw new IllegalArgumentException("fetchSize < 0");
|
||||
this.fetchSize = size;
|
||||
return this;
|
||||
}
|
||||
/**원래 요소의 전체 갯수를 반환한다.
|
||||
* @return 원래 요소의 전체 갯수
|
||||
*/
|
||||
public long getTotalSize() {
|
||||
if (isEmpty())
|
||||
return 0;
|
||||
if (totalSize > 0)
|
||||
return totalSize;
|
||||
|
||||
if (totalSize < 1) {
|
||||
E first = get(0);
|
||||
if (first instanceof Map) {
|
||||
Map<String, ?> row = (Map<String, ?>)first;
|
||||
totalSize = Pageable.getTotalSize(row);
|
||||
} else if (first instanceof Pageable) {
|
||||
Pageable row = (Pageable)first;
|
||||
totalSize = row.getTotalSize();
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
return totalSize = size();
|
||||
}
|
||||
/**원래 요소의 전체 갯수를 설정한다.
|
||||
* @param totalSize 원래 요소의 전체 갯수
|
||||
*/
|
||||
public BoundedList<E> setTotalSize(long totalSize) {
|
||||
this.totalSize = totalSize;
|
||||
return this;
|
||||
}
|
||||
/**현재 가지고 온 요소의 시작 인덱스를 반환한다.
|
||||
* @return 현재 가지고 온 요소의 시작 인덱스(0부터 시작)
|
||||
*/
|
||||
public int getStart() {return isEmpty() ? -1 : start;}
|
||||
/**현재 가지고 온 요소의 시작 인덱스를 설정한다.
|
||||
* @param start 현재 가지고 온 요소의 시작 인덱스(0부터 시작)
|
||||
*/
|
||||
public BoundedList<E> setStart(int start) {
|
||||
this.start = start;
|
||||
// this.start = totalSize < 1 ? -1 : start;
|
||||
return this;
|
||||
}
|
||||
/**현재 가지고 온 요소의 마지막 인덱스를 반환한다.
|
||||
* @return 현재 가지고 온 요소의 마지막 인덱스(0부터 시작)
|
||||
*/
|
||||
public int getEnd() {
|
||||
return isEmpty() ? -1 : start + size() - 1;
|
||||
}
|
||||
/**현재 선택된 요소의 인덱스를 반환한다.
|
||||
* @return 현재 선택된 요소의 인덱스
|
||||
*/
|
||||
public int current() {
|
||||
return isEmpty() ? -1 : current;
|
||||
}
|
||||
/**지정하는 인덱스의 요소를 현재 요소로 설정한다.
|
||||
* @param index 요소의 인덱스
|
||||
*/
|
||||
public BoundedList<E> setCurrent(int index) {
|
||||
if (index < 0) {
|
||||
current = index;
|
||||
return this;
|
||||
}
|
||||
else if (get(index) != null) current = index;
|
||||
return this;
|
||||
}
|
||||
/**더 가져올 수 있는 요소가 있는지 반환한다.
|
||||
* @return
|
||||
* <ul><li>더 가져올 수 있는 요소가 있으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean hasMore() {
|
||||
return !isEmpty() && size() < getTotalSize();
|
||||
}
|
||||
/**뒤쪽으로 가져올 수 있는 요소가 있는지 반환한다.
|
||||
* @return
|
||||
* <ul><li>가져올 수 있는 요소가 있으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean hasPrevious() {
|
||||
return hasMore() && start > 0;
|
||||
}
|
||||
/**앞쪽으로 가져올 수 있는 요소가 있는지 반환한다.
|
||||
* @return
|
||||
* <ul><li>가져올 수 있는 요소가 있으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
public boolean hasNext() {
|
||||
return hasMore() && getEnd() < getTotalSize() - 1;
|
||||
}
|
||||
/**현재 선택된 요소를 반환한다.
|
||||
* @return 현재 선택된 요소
|
||||
*/
|
||||
public E get() {
|
||||
return get(current);
|
||||
}
|
||||
@Override
|
||||
public boolean add(E e) {
|
||||
if (e == null || contains(e)) return false;
|
||||
|
||||
boolean result = super.add(e);
|
||||
++totalSize;
|
||||
return result;
|
||||
}
|
||||
@Override
|
||||
public void add(int index, E e) {
|
||||
super.add(index, e);
|
||||
++totalSize;
|
||||
}
|
||||
@Override
|
||||
public E remove(int index) {
|
||||
E e = super.remove(index);
|
||||
if (e != null)
|
||||
--totalSize;
|
||||
return e;
|
||||
}
|
||||
@Override
|
||||
public void clear() {
|
||||
super.clear();
|
||||
totalSize = 0;
|
||||
start = -1;
|
||||
}
|
||||
|
||||
/**BoundedList를 초기화 한다.
|
||||
*/
|
||||
public BoundedList<E> init() {
|
||||
totalSize = size();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**페이징 처리를 위한 값들을 계산하기 위한 유틸리티*/
|
||||
public static class Fetch {
|
||||
public static final int
|
||||
ALL = 0,
|
||||
NONE = -1;
|
||||
|
||||
private Fetch() {}
|
||||
/**Returns the number of fetches needed to provide all elements.
|
||||
* @param elementCount number of all elements
|
||||
* @param size size or number of elements in a fetch
|
||||
* @return number of fetches needed to provide all elements
|
||||
*/
|
||||
public static final int count(int elementCount, int size) {
|
||||
if (elementCount == 0 || size == ALL) return 1;
|
||||
return (elementCount / size) + ((elementCount % size) == 0 ? 0 : 1);
|
||||
}
|
||||
/**Returns the end index of the elements starting from the start index.
|
||||
* @param elementCount number of all elements
|
||||
* @param size size or number of elements in a fetch
|
||||
* @param start 0-based start index
|
||||
* @return 0-based end index of the
|
||||
*/
|
||||
public static final int end(int elementCount, int size, int start) {
|
||||
if (size < ALL)
|
||||
throw new IllegalArgumentException("Invalid size: " + size);
|
||||
if (elementCount < 0)
|
||||
throw new IllegalArgumentException("Invalid elementCount: " + elementCount);
|
||||
|
||||
int last = elementCount - 1;
|
||||
if (size == ALL) return last;
|
||||
return Math.min(last, start + size -1);
|
||||
}
|
||||
/**
|
||||
* @param current
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
public static final int page(int current, int count) {
|
||||
return count < 1 ? 0 : current / count;
|
||||
}
|
||||
/**
|
||||
* @param page
|
||||
* @param visibleLinks
|
||||
* @return
|
||||
*/
|
||||
public static final int band(int page, int visibleLinks) {
|
||||
return visibleLinks < 1 ? 0 : page / visibleLinks;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package cokr.xit.foundation.data.paging;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import cokr.xit.foundation.data.Convert;
|
||||
|
||||
/**조회 결과를 페이징 처리하여 받고자 할 경우,
|
||||
* <ul><li>조회 요청 정보 객체나</li>
|
||||
* <li>조회 결과 객체</li>
|
||||
* </ul>
|
||||
* 가 구현해야 하는 인터페이스
|
||||
* @author mjkhan
|
||||
*/
|
||||
public interface Pageable {
|
||||
/**조회 요청 정보 객체가 Map일 경우 요청 페이지 번호를 반환한다.<br />
|
||||
* 조회 요청 정보 맵은 "pageNum" 키에 값을 가지고 있어야 한다.
|
||||
* @param map 조회 요청 정보 객체
|
||||
* @return 요청 페이지 번호
|
||||
*/
|
||||
static int getPageNum(Map<String, ?> map) {
|
||||
return Convert.toInt(map.get("pageNum"));
|
||||
}
|
||||
|
||||
/**조회 요청 정보 객체가 Map일 경우 한 번에 가지고 올 결과값들의 갯수를 반환한다.<br />
|
||||
* 조회 요청 정보 맵은 "fetchSize" 키에 값을 가지고 있어야 한다.
|
||||
* @param map 조회 요청 정보 객체
|
||||
* @return 요청 한 번에 가지고 올 결과값들의 갯수
|
||||
*/
|
||||
static int getFetchSize(Map<String, ?> map) {
|
||||
return Convert.toInt(map.get("fetchSize"));
|
||||
}
|
||||
|
||||
/**조회 결과 객체가 Map일 경우 조회 결과의 전체 갯수를 반환한다.<br />
|
||||
* 조회 결과 객체는 "TOT_CNT" 키에 값을 가지고 있어야 한다.
|
||||
* @param map 조회 결과 객체
|
||||
* @return 조회 결과의 전체 갯수
|
||||
*/
|
||||
static long getTotalSize(Map<String, ?> map) {
|
||||
return Convert.toLong(map.get("TOT_CNT"));
|
||||
}
|
||||
|
||||
/**조회 요청 정보 객체가 구현하는 메소드로, 요청 페이지 번호를 반환한다.
|
||||
* @return 요청 페이지 번호
|
||||
*/
|
||||
default int getPageNum() {return 0;}
|
||||
|
||||
/**조회 요청 정보 객체가 구현하는 메소드로, 요청 페이지 번호를 설정한다.
|
||||
* @param pageNum 요청 페이지 번호
|
||||
*/
|
||||
default void setPageNum(int pageNum) {}
|
||||
|
||||
/**조회 요청 정보 객체가 구현하는 메소드로, 한 번에 가지고 올 결과값들의 갯수를 반환한다.
|
||||
* @return 한 번에 가지고 올 결과값들의 갯수
|
||||
*/
|
||||
default int getFetchSize() {return 0;}
|
||||
|
||||
/**조회 요청 정보 객체가 구현하는 메소드로, 한 번에 가지고 올 결과값들의 갯수를 설정한다.
|
||||
* @param fetchSize 한 번에 가지고 올 결과값들의 갯수
|
||||
*/
|
||||
default void setFetchSize(int fetchSize) {}
|
||||
|
||||
/**조회 결과 객체가 구현하는 메소드로, 조회 결과의 전체 갯수를 반환한다.
|
||||
* @return 조회 결과의 전체 갯수
|
||||
*/
|
||||
default long getTotalSize() {return 0;}
|
||||
|
||||
/**조회 결과 객체가 구현하는 메소드로, 조회 결과의 전체 갯수를 반환한다.
|
||||
* @param totalSize 조회 결과의 전체 갯수
|
||||
*/
|
||||
default void setTotalSize(long totalSize) {}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package cokr.xit.foundation.data.paging;
|
||||
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.resultset.ResultSetHandler;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.apache.ibatis.plugin.Intercepts;
|
||||
import org.apache.ibatis.plugin.Invocation;
|
||||
import org.apache.ibatis.plugin.Signature;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
import cokr.xit.foundation.component.QueryRequest;
|
||||
|
||||
/**조회 결과의 페이징 처리를 지원하는 MyBatis Interceptor
|
||||
* @author mjkhan
|
||||
*/
|
||||
@Intercepts({
|
||||
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
|
||||
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
|
||||
})
|
||||
public class PagingSupport extends AbstractComponent implements Interceptor {
|
||||
private boolean paging;
|
||||
private int pageNum;
|
||||
private int fetchSize;
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
switch (getStep(invocation)) {
|
||||
case "Executor.query": return setPaging(invocation);
|
||||
case "ResultSetHandler.handleResultSets": return setPagingInfo(invocation);
|
||||
default: return invocation.proceed();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Class<?>[] handlers = {Executor.class, ResultSetHandler.class};
|
||||
|
||||
private String getStep(Invocation invocation) {
|
||||
Object obj = invocation.getTarget();
|
||||
String result = "";
|
||||
for (Class<?> klass: handlers) {
|
||||
if (klass.isInstance(obj)) {
|
||||
result = klass.getSimpleName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result += "." + invocation.getMethod().getName();
|
||||
}
|
||||
|
||||
private Object setPaging(Invocation invocation) throws Exception {
|
||||
Object[] args = invocation.getArgs();
|
||||
Object arg = args[1];
|
||||
if (arg instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> params = (Map<String, ?>)arg;
|
||||
fetchSize = Pageable.getFetchSize(params);
|
||||
pageNum = Pageable.getPageNum(params);
|
||||
} else if (arg instanceof QueryRequest) {
|
||||
QueryRequest req = (QueryRequest)arg;
|
||||
fetchSize = req.getFetchSize();
|
||||
pageNum = req.getPageNum();
|
||||
} else if (arg instanceof Pageable) {
|
||||
Pageable pageable = (Pageable)arg;
|
||||
fetchSize = pageable.getFetchSize();
|
||||
pageNum = pageable.getPageNum();
|
||||
}
|
||||
paging = pageNum > 0 && fetchSize > 0;
|
||||
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
private Object setPagingInfo(Invocation invocation) throws Exception {
|
||||
Object obj = invocation.proceed();
|
||||
|
||||
if (paging && (obj instanceof List)) {
|
||||
List<?> list = (List<?>)obj;
|
||||
BoundedList<Object> boundedList = new BoundedList<>();
|
||||
boundedList.setFetchSize(fetchSize);
|
||||
boundedList.addAll(list);
|
||||
if (!boundedList.isEmpty()) {
|
||||
boundedList.setStart((pageNum - 1) * fetchSize);
|
||||
}
|
||||
obj = boundedList;
|
||||
}
|
||||
|
||||
paging = false;
|
||||
pageNum = fetchSize = 0;
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/**<b>조회 결과의 페이징 처리 지원</b>
|
||||
* <p>조회 결과가 대량의 데이터를 반환할 때, 이를 작은 갯수로 여러 번에 나누어 반환하는 것을 페이징이라 한다.<br />
|
||||
* 조회 결과를 페이징 처리하려면 다음과 같은 절차를 거쳐야 한다.
|
||||
* <p><b>{@link PagingSupport} 설정</b>
|
||||
* <p>PagingSupport를 sql/mybatis-config.xml에 다음과 같이 등록한다.
|
||||
* <pre>{@code <plugins>
|
||||
* <plugin interceptor="cokr.xit.foundation.data.paging.PagingSupport" />
|
||||
* </plugins>
|
||||
* }</pre>
|
||||
* <p><b>SELECT문 작성</b><br />
|
||||
* SELECT문을 작성하고 다음과 같이
|
||||
* <ul><li>{@code <include refid="utility.paging-prefix" />}</li>
|
||||
* <li>{@code <include refid="utility-paging-suffix" />}</li>
|
||||
* </ul>
|
||||
* 를 include한다.
|
||||
* <pre>{@code <select id="selectATable" parameterType="..." resultType="...">
|
||||
* <include refid="utility.paging-prefix" />
|
||||
* SELECT *
|
||||
* FROM A_TABLE
|
||||
* WHERE ...
|
||||
* ORDER BY ...
|
||||
* <include refid="utility.paging-suffix" />}</pre>
|
||||
* <ul><li>parameterType - {@code <select .../>}의 parameterType에 올 수 있는 클래스는 다음과 같다.
|
||||
* <ul><li>map</li>
|
||||
* <li>{@link cokr.xit.foundation.component.QueryRequest} 또는 그것을 상속 받는 클래스</li>
|
||||
* <li>{@link cokr.xit.foundation.data.paging.Pageable}을 구현하는 클래스</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>resultType - {@code <select .../>}의 resultType에 올 수 있는 클래스는 다음과 같다.
|
||||
* <ul><li>map</li>
|
||||
* <li>{@link cokr.xit.foundation.data.paging.Pageable}을 구현하는 클래스</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
* <p><b>조회 실행</b><br />
|
||||
* 파라미터가 map일 경우
|
||||
* <pre>{@code DataObject params = new DataObject()
|
||||
* .set("pageNum", 1)
|
||||
* .set("fetchSize", 10);
|
||||
* List<?> list = aTableMapper.selectXXX(params);
|
||||
* }</pre>
|
||||
* 파라미터가 QueryRequest 또는 상속 받은 클래스일 경우
|
||||
* <pre>{@code QueryRequest req = new QueryRequest();
|
||||
* req.setPageNum(1)
|
||||
* req.setFetchSize(10);
|
||||
* List<?> list = aTableMapper.selectXXX(req);
|
||||
* }</pre>
|
||||
* 파라미터가 Pageable의 구현 클래스일 경우
|
||||
* <pre>{@code class PageableObject implements Pageable {
|
||||
* ....
|
||||
* };
|
||||
* PageableObject req = new PageableObject();
|
||||
* req.setPageNum(1)
|
||||
* req.setFetchSize(10);
|
||||
* List<?> list = aTableMapper.selectXXX(req);
|
||||
* }</pre>
|
||||
* <p><b>컨트롤러에서 조회 결과와 페이징 정보 전달</b><br />
|
||||
* {@link cokr.xit.foundation.web.AbstractController}를 상속 받아 정의한 컨트롤러는<br />
|
||||
* <code>setPagingInfo(ModelAndView, List, String)</code> 메소드로 조회 결과를 ModelAndView에 전달한다.
|
||||
* <pre>{@code setPagingInfo(new ModelAndView("viewName"), list, "example");}</pre>
|
||||
*/
|
||||
package cokr.xit.foundation.data.paging;
|
@ -0,0 +1,105 @@
|
||||
/**기반 기술 지원 및 공통 기능, 주요 구성요소 개발 지원
|
||||
* <h3>로그</h3>
|
||||
* <p>로그는 애플리케이션 개발 단계에는 디버깅 시 소스 추적에 사용되며,<br />
|
||||
* 운영단계에서는 로그 자료를 애플리케이션의 튜닝이나 troubleshooting의 근거로 사용한다.
|
||||
* <p>x-base는 slf4j를 포장한 {@link Log}를 로그에 사용한다.<br />
|
||||
* x-base의 {@link AbstractComponent}를 상속받아 만든 클래스에서는<br />
|
||||
* {@link AbstractComponent#log()} 메소드를 호출해서 로그를 남길 수 있다.
|
||||
* <h3>사용자 세션 정보</h3>
|
||||
* x-base는 현재 사용자의 접속과 관련해서 다음 정보를 제공한다.
|
||||
* <ul><li>{@link Access}</li>
|
||||
* <li>{@link UserInfo}</li>
|
||||
* <li>{@link ApplicationContainer}</li>
|
||||
* </ul>
|
||||
* 이중 Access와 UserInfo는 AbstractComponent이나 AbstractServiceBean을 상속받아 만든 클래스에서
|
||||
* <ul><li>{@link AbstractComponent#currentAccess()}</li>
|
||||
* <li>{@link AbstractComponent#currentUser()}</li>
|
||||
* </ul>
|
||||
* 메소드로 받을 수 있다.
|
||||
* <p>ApplicationContainer가 필요한 클래스에서 다음과 같이 받을 수 있다.
|
||||
* <pre>{@code @Resource(name = "applicationContainer")
|
||||
* private ApplicationContainer applicationContainer;
|
||||
* }</pre>
|
||||
* <h3>프로퍼티 사용</h3>
|
||||
* 프로퍼티는 *.properties 파일에 key=value 쌍으로 된 정보를 저장하고<br />
|
||||
* 애플리케이션에서 프로퍼티 파일을 읽어들여 필요한 키의 값을 사용하는 기능이다.
|
||||
* <p>x-base는 전자정부 프레임워크의 EgovPropertyService를 사용해 프로퍼티를 읽어들인다.<br />
|
||||
* EgovPropertyService는 spring/context-common.xml 파일에 다음과 같이 설정되어있다.
|
||||
* <pre>{@code <bean name="propertyService" class="org.egovframe.rte.fdl.property.impl.EgovPropertyServiceImpl" destroy-method="destroy">
|
||||
* <property name="properties"><!-- 프로퍼티 추가 -->
|
||||
* <map>
|
||||
* <entry key="tempDir" value="D:/temp"/>
|
||||
*
|
||||
* <entry key="pageUnit" value="10"/>
|
||||
* <entry key="pageSize" value="10"/>
|
||||
* </map>
|
||||
* </property>
|
||||
* <property name="extFileName"><!-- 프로퍼티 파일 추가 시 설정 -->
|
||||
* <set>
|
||||
* <map>
|
||||
* <entry key="encoding" value="UTF-8"/>
|
||||
* <entry key="filename" value="classpath*:properties/your-file.properties"/>
|
||||
* </map>
|
||||
* <map>
|
||||
* <entry key="encoding" value="UTF-8"/>
|
||||
* <entry key="filename" value="classpath*:properties/your-file2.properties"/>
|
||||
* </map>
|
||||
* </set>
|
||||
* </property>
|
||||
* </bean>
|
||||
* }</pre>
|
||||
* 프로퍼티를 설정할 때는 프로퍼티의 키가 중복되지 않도록 한다.
|
||||
* <p>x-base의 AbstractComponent이나 AbstractServiceBean을 상속받아 만든 클래스에서는<br />
|
||||
* {@link AbstractComponent#properties} 필드로 프로퍼티를 사용할 수 있다.
|
||||
* <h3>트랜잭션 처리</h3>
|
||||
* <ul><li>트랜잭션은 완료 또는 취소할 수 있는 작업의 최소단위다.</li>
|
||||
* <li>애플리케이션 개발자는 트랜잭션의 적용에 개입하지 않아야 한다.</li>
|
||||
* <li>애플리케이션에서 트랜잭션의 대상은 서비스 인터페이스의 각각의 메소드다.</li>
|
||||
* <li>서비스 인터페이스 메소드를 대상으로 하는 트랜잭션의 설정은<br />
|
||||
* spring/context-datasource.xml에 다음과 같이 한다.</li>
|
||||
* </ul>
|
||||
* <pre>{@code <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
|
||||
* <property name="dataSource" ref="dataSource"/>
|
||||
* </bean>
|
||||
*
|
||||
* <tx:advice id="txAdvice" transaction-manager="txManager">
|
||||
* <tx:attributes>
|
||||
* <tx:method name="*" rollback-for="Exception"/>
|
||||
* </tx:attributes>
|
||||
* </tx:advice>
|
||||
*
|
||||
* <aop:config>
|
||||
* <aop:pointcut id="serviceMethod" expression="execution(* cokr.xit..service.bean..*ServiceBean.*(..))" />
|
||||
* <aop:pointcut id="requiredTx" expression="execution(* cokr.xit..service.bean..*ServiceBean.*(..))"/>
|
||||
* <aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
|
||||
* </aop:config>
|
||||
* }</pre>
|
||||
* <h3>Lombok 라이브러리를 이용한 엔티티 정의</h3>
|
||||
* <ul><li>엔티티는 자신의 상태를 파일이나 데이터베이스 테이블에 저장하는 객체를 말한다.</li>
|
||||
* <li>엔티티 클래스의 필드들에 대한 getter와 setter 메소드는
|
||||
* <ul><li>가급적 직접 작성을 우선한다.</li>
|
||||
* <li>Lombok 라이브러리로 생성하는 것도 허용한다.</li>
|
||||
* <li>Lombok 라이브러리로 생성할 경우 가급적 @Getter와 @Setter만 사용하고 다른 어노테이션의 사용은 자제한다.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
* <h3>조회 결과의 엑셀 다운로드</h3>
|
||||
* 추가 예정
|
||||
* <h3>예외 처리</h3>
|
||||
* 애플리케이션 동작 중 발생한 오류는 {@link ApplicationException}으로 처리한다.<br />
|
||||
* ApplicationException을 발생시키려면
|
||||
* <pre>{@code if (result == null)
|
||||
* throw ApplicationException
|
||||
* .get(null)
|
||||
* .setCode("NPE")
|
||||
* .setMessage("결과값이 없습니다.")
|
||||
* .info("reason", "알 수 없음"); // 추가 정보 설정
|
||||
* }</pre>
|
||||
* 다른 오류를 ApplicationException으로 바꾸어 발생시키려면
|
||||
* <pre>{@code try {
|
||||
* ...
|
||||
* } catch(Exception e) {
|
||||
* throw ApplicationException.get(e);
|
||||
* }}</pre>
|
||||
*/
|
||||
package cokr.xit.foundation;
|
@ -0,0 +1,43 @@
|
||||
package cokr.xit.foundation.test;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import cokr.xit.foundation.component.AbstractDao;
|
||||
|
||||
/**JUnit 테스트 프로그램에서 SQL문을 실행할 때 사용하는 DAO.<br />
|
||||
* 주로 테스트 데이터를 지우는데 사용한다.<br />
|
||||
* TestDao의 매퍼 파일은 test.xml이다.
|
||||
* @author mjkhan
|
||||
*/
|
||||
@Repository("testDao")
|
||||
public class TestDao extends AbstractDao {
|
||||
/**주어진 INSERT문을 실행한다.
|
||||
* @param sql INSERT SQL
|
||||
* @return 저장된 데이터 수
|
||||
*/
|
||||
public int execInsert(String sql) {
|
||||
return insert("test.insert", params().set("sql", sql));
|
||||
}
|
||||
|
||||
/**주어진 UPDATE문을 실행한다.
|
||||
* @param sql UPDATE SQL
|
||||
* @return 저장된 데이터 수
|
||||
*/
|
||||
public int execUpdate(String sql) {
|
||||
return insert("test.update", params().set("sql", sql));
|
||||
}
|
||||
|
||||
/**주어진 DELETE문을 실행한다.
|
||||
* @param sql DELETE SQL
|
||||
* @return 저장된 데이터 수
|
||||
*/
|
||||
public int execDelete(String sql) {
|
||||
return insert("test.delete", params().set("sql", sql));
|
||||
}
|
||||
|
||||
/**실행된 SQL문을 커밋한다.
|
||||
*/
|
||||
public void commit() {
|
||||
update("test.commit");
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package cokr.xit.foundation.test;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
import cokr.xit.foundation.UserInfo;
|
||||
import cokr.xit.foundation.data.DataObject;
|
||||
|
||||
/**스프링 프레임워크에서 동작하는 클래스들의 단위 테스트 작성을 위한 베이스 클래스.<br />
|
||||
* TestSupport는
|
||||
* <ul><li>JUnit5를 사용하며</li>
|
||||
* <li>설정파일들은 클래스패스의 spring 디렉토리(spring/context-*.xml)에 위치해야 하며</li>
|
||||
* <li>"test" 프로필을 설정한다.</li>
|
||||
* <li>AbstractComponent의 기능을 사용할 수 있다.</li>
|
||||
* </ul>
|
||||
* <p>단위 테스트 클래스는 src/test/java 디렉토리 아래에 작성하되 테스트 대상이 되는 클래스와 같은 패키지에 위치하도록 한다.
|
||||
* <p>예를 들어 cokr.xit.example.business.service.BusinessService라는 인터페이스의 단위 테스트 클래스는<br />
|
||||
* src/test/java 디렉토리 아래에 cokr.xit.example.business.service 패키지를 만들고<br />
|
||||
* 그 아래에 BusinessServiceTest라는 이름으로 생성한다.
|
||||
* @author mjkhan
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration("classpath:spring/context-*.xml")
|
||||
@ActiveProfiles("test")
|
||||
public class TestSupport extends AbstractComponent {
|
||||
@Resource(name="testDao")
|
||||
protected TestDao testDao;
|
||||
|
||||
protected DataObject dataObject() {
|
||||
return new DataObject();
|
||||
}
|
||||
|
||||
/**user를 현재 사용자로 설정한다.
|
||||
* @param user 사용자 객체
|
||||
*/
|
||||
protected void currentUser(UserInfo user) {
|
||||
UserInfo.Provider.get().setUserInfo(user);
|
||||
}
|
||||
|
||||
/**주어진 인자들을 각각 SQL 문자 리터럴로 변환하고, 컴마(,)로 연결하여 반환한다.<br />
|
||||
* SQL의 IN (...) 문장에 조건을 명시하는 것을 돕기 위한 것이다.
|
||||
* @param params 인자
|
||||
* @return 컴마(,)로 연결된 SQL 문자 리터럴
|
||||
*/
|
||||
protected String join(Object... params) {
|
||||
return Stream.of(params).map(obj -> "'" + obj + "'").collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
/**JUnit5를 통한 단위 테스트 지원
|
||||
*/
|
||||
package cokr.xit.foundation.test;
|
@ -0,0 +1,74 @@
|
||||
package cokr.xit.foundation.util;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**문자열 인코더.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class CharsEncoder extends AbstractComponent {
|
||||
private String algorithm;
|
||||
private boolean plainText;
|
||||
|
||||
/**문자열 인크립션에 사용할 알고리즘을 반환한다. 디폴트는 SHA-256.
|
||||
* @return 문자열 인크립션에 사용할 알고리즘
|
||||
*/
|
||||
public String getAlgorithm() {
|
||||
return ifEmpty(algorithm, "SHA-256");
|
||||
}
|
||||
|
||||
/**문자열 인크립션에 사용할 알고리즘을 설정한다. 디폴트는 SHA-256.
|
||||
* @param algorithm 문자열 인크립션에 사용할 알고리즘
|
||||
*/
|
||||
public void setAlgorithm(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
/**인코딩 여부를 설정한다.
|
||||
* @param plainText 인코딩 여부
|
||||
* <ul><li>문자열을 인코딩하지 않으려면 true</li>
|
||||
* <li>문자열을 인코딩하려면 false(디폴트)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public void setPlainText(boolean plainText) {
|
||||
this.plainText = plainText;
|
||||
}
|
||||
|
||||
/**문자열을 설정된 알고리즘으로 인크립션 후 BASE64로 인코딩하여 반환한다.
|
||||
* @param rawChars 문자열
|
||||
* @return 인코딩한 문자열
|
||||
*/
|
||||
public String encode(CharSequence rawChars) {
|
||||
if (isEmpty(rawChars)) return "";
|
||||
|
||||
String str = rawChars.toString();
|
||||
if (plainText) return str;
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(getAlgorithm());
|
||||
md.reset();
|
||||
md.update(str.getBytes());
|
||||
|
||||
byte[] hashValue = md.digest(str.getBytes());
|
||||
return new String(Base64.encodeBase64(hashValue));
|
||||
} catch (Exception e) {
|
||||
throw applicationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**주어진 문자열을 인코딩한 결과가 인코딩한 문자열과 일치하는지 반환한다.
|
||||
* @param rawChars 인코딩하지 않은 일반 문자열
|
||||
* @param encodedChars 인코딩한 문자열
|
||||
* @return
|
||||
* <ul><li>두 문자열의 인코딩 결과가 같으면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected boolean matches(CharSequence rawChars, String encodedChars) {
|
||||
String encoded = encode(rawChars);
|
||||
return encoded.equals(encodedChars);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cokr.xit.foundation.util;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**난수 발생기
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class RandomNumber {
|
||||
/**지정한 자릿수의 난수를 반환한다.
|
||||
* @param digit 자릿수
|
||||
* @return 지정한 자릿수의 난수
|
||||
*/
|
||||
public static final String get(int digit) {
|
||||
if (digit < 1)
|
||||
throw new IllegalArgumentException("digit < 1");
|
||||
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
StringBuilder buff = new StringBuilder();
|
||||
for (int i = 0; i < digit; ++i) {
|
||||
buff.append(Integer.toString(random.nextInt(10)));
|
||||
}
|
||||
|
||||
return buff.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
/**인코딩 및 난수 발생 유틸리티
|
||||
*/
|
||||
package cokr.xit.foundation.util;
|
@ -0,0 +1,202 @@
|
||||
package cokr.xit.foundation.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
import cokr.xit.foundation.Access;
|
||||
import cokr.xit.foundation.component.QueryRequest;
|
||||
import cokr.xit.foundation.data.paging.BoundedList;
|
||||
|
||||
/**스프링 MVC에서 동작할 컨트롤러의 베이스 클래스.<br />
|
||||
* 애플리케이션의 컨트롤러 클래스는 이 클래스를 상속받아 정의한다.
|
||||
* <p>AbstractController를 상속받아 정의하는 컨트롤러는
|
||||
* <ul><li>AbstractComponent의 기능</li>
|
||||
* <li>{@link #ajaxRequest() 요청이 AJAX에 의한 것인지} 확인</li>
|
||||
* <li>{@link #jsonResponse() 응답을 JSON으로 해야 하는지} 확인</li>
|
||||
* <li>{@link #toJson(Object) JSON 문자열}이나 {@link #fromJson(String, Class) 객체}로의 변환</li>
|
||||
* <li>{@link #setCollectionInfo(ModelAndView, List, String) List의 ModelAndView 설정}</li>
|
||||
* <li>{@link #download(InputStream, String, String, long, String, HttpServletResponse) 파일 다운로드}</li>
|
||||
* </ul>
|
||||
* 등의 기능을 물려 받는다.
|
||||
* <p>그리고 '업무 용어' + Controller와 같이 이름을 붙이고, @Controller 어노테이션을 명시한다.
|
||||
* <p>컨트롤러의 클래스와 메소드에 명시하는 @RequestMapping, @PostMapping 어노테이션에는 이름과 url을 설정한다.<br/>
|
||||
* <pre><code> @RequestMapping(name = "홈", value = "index.do")
|
||||
* public String index() {return "index";}</code></pre>
|
||||
* <p>컨트롤러의 메소드가
|
||||
* <ul><li>읽기 기능이면 @RequestMapping</li>
|
||||
* <li>쓰기 기능이면 @PostMapping</li>
|
||||
* </ul>
|
||||
* 을 명시한다.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public abstract class AbstractController extends AbstractComponent {
|
||||
@Resource(name="objectMapper")
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**현재 요청이 AJAX로 전달된 것인지 반환한다.
|
||||
* @return
|
||||
* <ul><li>AJAX 요청이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected boolean ajaxRequest() {
|
||||
return Access.current().isAjaxRequest();
|
||||
}
|
||||
|
||||
/**현재 요청의 응답을 JSON으로 응답하는지 반환한다.
|
||||
* @return
|
||||
* <ul><li>JSON 응답이면 true</li>
|
||||
* <li>그렇지 않으면 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected boolean jsonResponse() {
|
||||
return Access.current().isJsonResponse();
|
||||
}
|
||||
|
||||
/**obj를 JSON 문자열로 변환하여 반환한다.
|
||||
* @param obj 객체
|
||||
* @return JSON 문자열
|
||||
*/
|
||||
protected String toJson(Object obj) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (Exception e) {
|
||||
throw applicationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**JSON 문자열을 읽어 klass의 객체를 반환한다.
|
||||
* @param <T> 객체 타입
|
||||
* @param content JSON 문자열
|
||||
* @param klass 클래스
|
||||
* @return klass의 객체
|
||||
*/
|
||||
protected <T> T fromJson(String content, Class<T> klass) {
|
||||
try {
|
||||
return objectMapper.readValue(content, klass);
|
||||
} catch (Exception e) {
|
||||
throw applicationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**ModelAndView에 list와 관련정보를 설정한다.<br />
|
||||
* 예를 들어 <code>setCollectionInfo(mav, list, "obj")</code>와 같이 호출하면
|
||||
* mav에 다음과 같이 설정한다.
|
||||
* <pre><code>{
|
||||
* "infoPrefix": "obj",
|
||||
* "objList": list,
|
||||
* "objStart": list.getStart(),
|
||||
* "objFetch": list.getFetchSize(),
|
||||
* "objTotal": list.getTotalSize(),
|
||||
* }</code></pre>
|
||||
* @param mav ModelAndView
|
||||
* @param collection Collection
|
||||
* @param prefix 접두어
|
||||
* @return ModelAndView
|
||||
*/
|
||||
protected ModelAndView setCollectionInfo(ModelAndView mav, Collection<?> collection, String prefix) {
|
||||
return setCollectionInfo(mav, collection, "infoPrefix", prefix);
|
||||
}
|
||||
|
||||
/**ModelAndView에 list와 관련정보를 설정한다.<br />
|
||||
* 예를 들어 <code>setCollectionInfo(mav, list, "yourPrefix", "obj")</code>와 같이 호출하면
|
||||
* mav에 다음과 같이 설정한다.
|
||||
* <pre><code>{
|
||||
* "yourPrefix": "obj",
|
||||
* "objList": list,
|
||||
* "objStart": list.getStart(),
|
||||
* "objFetch": list.getFetchSize(),
|
||||
* "objTotal": list.getTotalSize(),
|
||||
* }</code></pre>
|
||||
* @param mav ModelAndView
|
||||
* @param collection Collection
|
||||
* @param prefixKey 접두어를 저장할 키이름
|
||||
* @param prefix 접두어
|
||||
* @return ModelAndView
|
||||
*/
|
||||
protected ModelAndView setCollectionInfo(ModelAndView mav, Collection<?> collection, String prefixKey, String prefix) {
|
||||
mav.addObject(prefixKey, prefix)
|
||||
.addObject(prefix + "List", collection)
|
||||
.addObject(prefix + "Start", 0)
|
||||
.addObject(prefix + "Fetch", collection.size())
|
||||
.addObject(prefix + "Total", collection.size());
|
||||
if (!(collection instanceof BoundedList))
|
||||
return mav;
|
||||
|
||||
BoundedList<?> bounded = (BoundedList<?>)collection;
|
||||
return mav.addObject(prefix + "Start", bounded.getStart())
|
||||
.addObject(prefix + "Fetch", bounded.getFetchSize())
|
||||
.addObject(prefix + "Total", bounded.getTotalSize());
|
||||
}
|
||||
|
||||
protected <T extends QueryRequest> T setFetchSize(T req) {
|
||||
if (req.getFetchSize() > 0
|
||||
|| !isEmpty(req.getDownload())) return req;
|
||||
|
||||
return req.setFetchSize(properties.getInt("pageSize"));
|
||||
}
|
||||
|
||||
/**InputStream을 지정한 유형과 이름으로 다운로드 한다.
|
||||
* @param input 다운로드할 파일의 InputStream
|
||||
* @param contentType 컨텐트 유형. 지정하지 않으면 application/octet-stream
|
||||
* @param filename 다운로드됐을 때 사용자가 보게되는 파일 이름
|
||||
* @param length 다운로드할 파일의 길이
|
||||
* @param disposition 'attachment', 또는 'inline'. 지정하지 않으면 attachment
|
||||
* @param hresp HttpServletResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
protected void download(InputStream input, String contentType, String filename, long length, String disposition, HttpServletResponse hresp) throws Exception {
|
||||
if (input == null) {
|
||||
hresp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
hresp.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
hresp.setCharacterEncoding("UTF-8");
|
||||
hresp.setContentType(ifEmpty(contentType, "application/octet-stream"));
|
||||
hresp.setHeader("Content-Disposition", ifEmpty(disposition, "attachment") + "; filename=\"" + URLEncoder.encode(filename, "UTF-8") +"\"");
|
||||
hresp.setContentLengthLong(length);
|
||||
FileCopyUtils.copy(input, hresp.getOutputStream());
|
||||
}
|
||||
|
||||
/**file을 지정한 유형과 이름으로 다운로드한다.
|
||||
* @param file 다운로드할 파일
|
||||
* @param contentType 컨텐트 유형. 지정하지 않으면 application/octet-stream
|
||||
* @param filename 다운로드됐을 때 사용자가 보게되는 파일 이름. 지정하지 않으면 file의 이름을 사용한다.
|
||||
* @param disposition 'attachment', 또는 'inline'. 지정하지 않으면 attachment
|
||||
* @param hresp HttpServletResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
protected void download(File file, String contentType, String filename, String disposition, HttpServletResponse hresp) throws Exception {
|
||||
if (file == null) {
|
||||
download(null, null, null, 0, null, hresp);
|
||||
return;
|
||||
}
|
||||
try (FileInputStream in = new FileInputStream(file)) {
|
||||
download(in, contentType, ifEmpty(filename, file.getName()), file.length(), disposition, hresp);
|
||||
}
|
||||
}
|
||||
|
||||
/**임시 디렉토리 경로에 파일 경로를 추가하여 반환한다.<br />
|
||||
* 임시 디렉토리 경로는 프로퍼티 파일에 tempDir 키로 설정한다.<br />
|
||||
* tempDir 프로퍼티가 설정되어 있지 않으면 시스템 프로퍼티(java.io.tmpdir)의 값을 사용한다.
|
||||
* @param path 파일 경로
|
||||
* @return 임시 디렉토리 경로 + 파일 경로
|
||||
*/
|
||||
protected String tempPath(String path) {
|
||||
String dir = ifEmpty(properties.getString("tempDir"), System.getProperty("java.io.tmpdir")).replace("\\", "/");
|
||||
return dir.endsWith("/") || path.startsWith("/") ? dir + path : dir + "/" + path;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package cokr.xit.foundation.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import cokr.xit.foundation.Access;
|
||||
|
||||
/**사용자의 {@link Access 접근정보}를 추출하는 필터.<br />
|
||||
* 추출한 접근정보는
|
||||
* <ul><li>현재 스레드에 바인딩하고</li>
|
||||
* <li>서블릿 요청에 "currentAccess"라는 이름의 애트리뷰트로 저장한다.</li>
|
||||
* </ul>
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class AccessFilter implements Filter {
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest sreq, ServletResponse sresp, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest hreq = (HttpServletRequest)sreq;
|
||||
String action = hreq.getRequestURI().replace(hreq.getContextPath(), "");
|
||||
HttpSession session = hreq.getSession(false);
|
||||
|
||||
Access access = new Access()
|
||||
.setAction(action)
|
||||
.setNewSession(session == null || session.isNew())
|
||||
.setMobile(hreq.getHeader("User-Agent").contains("Mobi"))
|
||||
.setSessionID(session != null ? session.getId() : null)
|
||||
.setAjaxRequest(hreq.getHeader("X-Requested-With"))
|
||||
.setJsonResponse(hreq.getHeader("accept"))
|
||||
.setCurrent();
|
||||
|
||||
hreq.setAttribute("currentAccess", access);
|
||||
|
||||
chain.doFilter(sreq, sresp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package cokr.xit.foundation.web;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import cokr.xit.foundation.Access;
|
||||
import cokr.xit.foundation.ApplicationContainer;
|
||||
import cokr.xit.foundation.UserInfo;
|
||||
import cokr.xit.foundation.component.ServiceRequest;
|
||||
|
||||
/**클라이언트가 시스템에 접근했을 때 메뉴정보, 클라이언트의 접근정보를 HttpServletRequest의 애트리뷰트로 설정하여,
|
||||
* 시스템의 다른 컴포넌트(예:JSP)에서 사용할 수 있도록 한다.
|
||||
* <ul><li>"applicationContainer" - {@link ApplicationContainer}</li>
|
||||
* <li>"currentUser" - 접근한 ({@link UserInfo 클라이언트})</li>
|
||||
* <li>"currentAccess" - 클라이언트의 {@link Access 접근정보}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>AccessInitializer가 동작하려면 인터셉터로 설정해야 한다.</p>
|
||||
*/
|
||||
public class AccessInitializer extends RequestInterceptor {
|
||||
@Resource(name="applicationContainer")
|
||||
protected ApplicationContainer applicationContainer;
|
||||
@Resource(name="localeResolver")
|
||||
protected LocaleResolver localeResolver;
|
||||
|
||||
private String version;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest hreq, HttpServletResponse hresp, Object handler) throws Exception {
|
||||
log().debug("Initializing access information for the current request...");
|
||||
hreq.setAttribute("applicationContainer", applicationContainer);
|
||||
|
||||
setVersion(hreq);
|
||||
initUserAccess(hreq);
|
||||
return super.preHandle(hreq, hresp, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest hreq, HttpServletResponse hresp, Object handler, ModelAndView mav) throws Exception {
|
||||
Map<String, Object> model = mav.getModel();
|
||||
model.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() instanceof ServiceRequest)
|
||||
.map(entry -> entry.getKey())
|
||||
.collect(Collectors.toList())
|
||||
.forEach(model::remove);
|
||||
}
|
||||
|
||||
/**애플리케이션의 시작시간으로 "ver=yyMMddHHmm"이라는 문자열을 만들어 서블릿 요청에 "ver"라는 이름의 애트리뷰트로 저장한다.
|
||||
* @param hreq 서블릿 요청
|
||||
*/
|
||||
protected void setVersion(HttpServletRequest hreq) {
|
||||
if (isEmpty(version))
|
||||
version = "ver=" + new SimpleDateFormat("yyMMddHHmm").format(applicationContainer.getStartupDate());
|
||||
hreq.setAttribute("ver", version);
|
||||
}
|
||||
|
||||
/**현재 접근한 사용자의 정보를 추출하여 서블릿 요청에 "currentUser"라는 이름의 애트리뷰트로 저장한다.
|
||||
* @param hreq
|
||||
*/
|
||||
protected void initUserAccess(HttpServletRequest hreq) {
|
||||
UserInfo currentUser = UserInfo.current();
|
||||
hreq.setAttribute("currentUser", currentUser);
|
||||
hreq.setAttribute("production", applicationContainer.isProfileActive("production"));
|
||||
log().debug("{} accessing the system.", currentUser);
|
||||
|
||||
Locale locale = localeResolver.resolveLocale(hreq);
|
||||
|
||||
Access.current()
|
||||
.setIpAddress(getClientAddress(hreq))
|
||||
.setLocale(locale);
|
||||
}
|
||||
|
||||
private static final String UNKNOWN = "unknown";
|
||||
private static final String[] HEADERS = {"x-forwarded-for", "X-FORWARDED-FOR", "WL-Proxy-Client-IP", "HTTP_X_FORWARDED_FOR"};
|
||||
|
||||
private String getClientAddress(HttpServletRequest hreq) {
|
||||
for (String header: HEADERS) {
|
||||
String addr = hreq.getHeader(header);
|
||||
if (!isEmpty(addr) && !UNKNOWN.equalsIgnoreCase(addr))
|
||||
return addr;
|
||||
}
|
||||
|
||||
return Access.getClientAddress(hreq.getRemoteAddr(), applicationContainer.getHostAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest hreq, HttpServletResponse hresp, Object handler, Exception ex) throws Exception {
|
||||
super.afterCompletion(hreq, hresp, handler, ex);
|
||||
Access.release();
|
||||
log().debug("Access information cleared for the current request.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConcurrentHandlingStarted(HttpServletRequest hreq, HttpServletResponse hresp, Object handler) throws Exception {
|
||||
super.afterConcurrentHandlingStarted(hreq, hresp, handler);
|
||||
Access.release();
|
||||
log().debug("Access information cleared for the current request.");
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package cokr.xit.foundation.web;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import cokr.xit.foundation.Access;
|
||||
import cokr.xit.foundation.ApplicationException;
|
||||
|
||||
/**예외 처리를 위한 정보를 갖는 ModelAndView를 생성하여 반환한다.
|
||||
* @author mjkhan
|
||||
*/
|
||||
@Controller
|
||||
@ControllerAdvice
|
||||
public class ExceptionController extends AbstractController {
|
||||
/**사용자가 실행을 요청한 uri를 반환한다.
|
||||
* @param hreq 서블릿 요청
|
||||
* @return 사용자가 실행을 요청한 uri
|
||||
*/
|
||||
protected String getAction(HttpServletRequest hreq) {
|
||||
return (String)hreq.getAttribute("javax.servlet.forward.request_uri");
|
||||
}
|
||||
|
||||
/**pageNotFound 오류의 처리를 위한 정보를 갖는 ModelAndView를 반환한다.
|
||||
* @param hreq 서블릿 요청
|
||||
* @return ModelAndView
|
||||
*/
|
||||
@RequestMapping("/error/pageNotFound.do")
|
||||
public ModelAndView pageNotFound(HttpServletRequest hreq) {
|
||||
return new ModelAndView(ajaxRequest() ? "jsonView" : "error/errorPage")
|
||||
.addObject("path", getAction(hreq))
|
||||
.addObject("failed", true)
|
||||
.addObject("status", 404)
|
||||
.addObject("message", message("pageNotFound"));
|
||||
}
|
||||
|
||||
/**세션 만료 시 처리를 위한 정보를 갖는 ModelAndView를 반환한다.
|
||||
* @param hreq 서블릿 요청
|
||||
* @return ModelAndView
|
||||
*/
|
||||
@RequestMapping("/error/sessionExpired.do")
|
||||
public ModelAndView sessionExpired(HttpServletRequest hreq) {
|
||||
return new ModelAndView(ajaxRequest() ? "jsonView" : "error/errorPage")
|
||||
.addObject("path", getAction(hreq))
|
||||
.addObject("failed", true)
|
||||
.addObject("status", "sessionExpired")
|
||||
.addObject("message", message("sessionExpired"))
|
||||
.addObject("home", true);
|
||||
}
|
||||
|
||||
/**접근 거부 오류의 처리를 위한 정보를 갖는 ModelAndView를 반환한다.
|
||||
* @param hreq 서블릿 요청
|
||||
* @return ModelAndView
|
||||
*/
|
||||
@RequestMapping("/error/accessDenied.do")
|
||||
public ModelAndView accessDenied(HttpServletRequest hreq) {
|
||||
return new ModelAndView(ajaxRequest() ? "jsonView" : "error/errorPage")
|
||||
.addObject("path", Access.current().getAction())
|
||||
.addObject("failed", true)
|
||||
.addObject("status", "accessDenied")
|
||||
.addObject("message", message("accessDenied"));
|
||||
}
|
||||
|
||||
/**ApplicationException 처리를 위한 정보를 갖는 ModelAndView를 반환한다.
|
||||
* @param e ApplicationException
|
||||
* @return ModelAndView
|
||||
*/
|
||||
@ExceptionHandler(ApplicationException.class)
|
||||
public ModelAndView onException(ApplicationException e) {
|
||||
return onException((Throwable)e)
|
||||
.addObject("errorCode", e.getCode());
|
||||
}
|
||||
|
||||
/**Throwable의 처리를 위한 정보를 갖는 ModelAndView를 반환한다.
|
||||
* @param e Throwable
|
||||
* @return ModelAndView
|
||||
*/
|
||||
@ExceptionHandler(Throwable.class)
|
||||
public ModelAndView onException(Throwable e) {
|
||||
Throwable cause = rootCause(e);
|
||||
|
||||
String action = currentAccess().getAction(),
|
||||
description = cause.getClass().getSimpleName() + " from " + currentAccess(),
|
||||
stacktrace = getStackTrace(cause);
|
||||
log().error(description);
|
||||
log().error(stacktrace);
|
||||
|
||||
return new ModelAndView(ajaxRequest() || jsonResponse() ? "jsonView" : "error/errorPage")
|
||||
.addObject("status", 500)
|
||||
.addObject("failed", true)
|
||||
.addObject("description", description)
|
||||
.addObject("path", action)
|
||||
.addObject("message", cause.getMessage())
|
||||
.addObject("stacktrace", stacktrace);
|
||||
}
|
||||
|
||||
private static String getStackTrace(Throwable throwable) {
|
||||
StringWriter sw = new StringWriter();
|
||||
throwable.printStackTrace(new PrintWriter(sw, true));
|
||||
return sw.getBuffer().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package cokr.xit.foundation.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
import cokr.xit.foundation.data.DataObject;
|
||||
import cokr.xit.foundation.data.hierarchy.Hierarchy;
|
||||
import cokr.xit.foundation.data.hierarchy.HierarchyBuilder;
|
||||
|
||||
/**컨트롤러에 명시된 {@code @RequestMapping} 어노테이션을 읽어들인다.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class RequestHandlerReader extends AbstractComponent {
|
||||
/**컨트롤러에 명시된 {@code @RequestMapping} 어노테이션을 읽어들여, {"name", "url", "parentID"} 정보를 추출하여 목록으로 반환한다.
|
||||
* @param requestHandlerMappings 요청 매핑
|
||||
* @return {"name", "url", "parentID"} 정보 목록
|
||||
*/
|
||||
public List<DataObject> read(RequestMappingHandlerMapping requestHandlerMappings) {
|
||||
Map<RequestMappingInfo, HandlerMethod> handlers = requestHandlerMappings.getHandlerMethods();
|
||||
ArrayList<DataObject> nameURLs = new ArrayList<>();
|
||||
|
||||
handlers.forEach((info, methods) -> {
|
||||
String name = blankIfEmpty(info.getName());
|
||||
if (!isEmpty(name)) {
|
||||
int pos = name.lastIndexOf("#");
|
||||
if (pos > -1)
|
||||
name = name.substring(pos + 1);
|
||||
}
|
||||
|
||||
Object bean = methods.getBean();
|
||||
for (String url: info.getPatternsCondition().getPatterns()) {
|
||||
DataObject row = menuItem(name, url);
|
||||
nameURLs.add(row.set("bean", bean));
|
||||
}
|
||||
});
|
||||
|
||||
Set<String> parentIDs = nameURLs.stream()
|
||||
.filter(row -> !isEmpty(row.get("parentID")))
|
||||
.map(row -> row.string("parentID"))
|
||||
.collect(Collectors.toSet());
|
||||
parentIDs.forEach(parentID -> {
|
||||
nameURLs.add(menuItem(null, parentID));
|
||||
});
|
||||
|
||||
Hierarchy<DataObject> tree = new HierarchyBuilder<DataObject>()
|
||||
.getID(row -> row.string("url"))
|
||||
.getParentID(row -> row.string("parentID"))
|
||||
.atTop(row -> "/".equals(row.string("parentID")))
|
||||
.addChild((parent, child) -> {
|
||||
ArrayList<DataObject> children = (ArrayList<DataObject>)parent.get("children");
|
||||
if (children == null)
|
||||
parent.put("children", children = new ArrayList<>());
|
||||
children.add(child);
|
||||
})
|
||||
.setElements(nameURLs)
|
||||
.build();
|
||||
return tree.topElements();
|
||||
}
|
||||
|
||||
private static DataObject menuItem(String name, String path) {
|
||||
int pos = Math.max(path.lastIndexOf("/"), 0);
|
||||
String parentID = ifEmpty(path.substring(0, pos), "/");
|
||||
|
||||
return new DataObject()
|
||||
.set("name", ifEmpty(name, path))
|
||||
.set("url", path)
|
||||
.set("parentID", parentID);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package cokr.xit.foundation.web;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.web.servlet.AsyncHandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import cokr.xit.foundation.AbstractComponent;
|
||||
|
||||
/**인터셉터 인터페이스의 구현을 돕기 위한 베이스 클래스.
|
||||
* @author mjkhan
|
||||
*/
|
||||
public class RequestInterceptor extends AbstractComponent implements AsyncHandlerInterceptor {
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest hreq, HttpServletResponse hresp, Object handler) throws Exception {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest hreq, HttpServletResponse hresp, Object handler, ModelAndView mav) throws Exception {}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest hreq, HttpServletResponse hresp, Object handler, Exception ex) throws Exception {}
|
||||
|
||||
@Override
|
||||
public void afterConcurrentHandlingStarted(HttpServletRequest hreq, HttpServletResponse hresp, Object handler) throws Exception {}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
/**베이스 컨트롤러, 웹 계층에서의 접근 정보 추출 및 예외 처리 지원
|
||||
*/
|
||||
package cokr.xit.foundation.web;
|
Loading…
Reference in New Issue