Drag & Drop의 구현

스윙에서의 Drag & Drop 을 구현해 보자. 과정이 꽤 복잡하고 기능상에 약간의 제약은 있으나 한번 구현해 볼 만한 예제라고 생각을 한다. 소스코드는 자바소프트에서 발췌했음을 알아두길 바란다. 중간중간에 풍선도움말을 두겠으니 단풍잎에 마우스를 갖다대면 도움말이 뜨니 참고하길 바란다.

Drag & Drop step
Drag & Drop 을 구현하는 것은 비교적 쉽다. 그렇지만 가장 난해한 것은 모든 과정에 대해서 이해하는 것이다. 다음의 순서를 먼저 익혀두길 바란다.     [ DnD Source Download]

1. Drag source 에 대한 참조를 가진다. - DragSource.getDefaultSource() 나 new DragSource() 를 통해서.
2. Drag gesture recognizer 를 생성한다. - DragSource.createDafaultGestureRecognizer()
3. Drop target 를 생성한다. - 하나의 컴포넌트와 Drop target listener 를 명세한다.
4. Transferable 로 옮겨질 수 있는 데이타를 wrap 한다.
5. Drag 를 초기화한다. - DragSource.startDrag()
6. DropTargetLisenter, DragSource 인터페이스를 구현함으로써 Drop 를 핸들링한다.

Drag & Drop (이하 D&D) 는 하나의 datasource 와 여러개의 droptarget 에 - 흔히 콤포넌트와 연관되어져 있다 - 와 연관되어 이뤄진다. 다음에 나열된 클래스와 인터페이스는 D&D 에서 사용되는 것을 정리한 것이다. 대부분의 것들은 java.awt.dnd 패키지에 있고, 오직 하나 Transferable 은 java.awt.datatransfer 패키지에 있다.

DragGestureRecognizer : 컴포넌트에서 drag 를 수행할려고 할때 이벤트를 발사한다.
DragSource : Drag 와 DragGestureRecognizer 를 초기화한다.
DropTarget : 연관된 컴포넌트 상에서 Drop 이 일어난다.
Transferable ; D&D 를 통해서 전송되어지는 데이타를 위한 Wrapper 이다.
DragGestureListener : DragGestureRecognizer 에 의해 통보를 받고 drag 를 초기화한다.
DragSourceListener : DragSource 이벤트에 반응한다.
DropTargetListener : Drop 포함한 target 이벤트를 핸들링한다.

D&D 는 하나의 기미(Gesture) 즉, 마우스를 내려서 드래깅을 시도할때 초기화된다. 그러면 DragGestureRecognizer 는 이런 기미를 포착하고 이벤트를 발생하는 것이다.
DragSource 는 startDrag() 메소드로 초기화작업을 수행하고, createDragGestureRecognizer() 나 createDafaultDragGestureRecognizer() 를 이용하여 DragGestureRecognizer 를 생성하게 되는 것이다.
DropTarget 은 하나의 컴포넌트와 Listener 객체와 연관되어 있어서 drop target event 가 발생할때 listener 객체가 통보를 받게 된다.
DragGestureListener 는 recognizer 에 의해 drag gesture 를 통보받는데 전형적인 반응예는 DragSource.startDrag() 메소들르 호출하게 되었을 경우이다.
DragSourceListener 는 drag 가 초기화된 후 dragsource 에서 일어나게 된다.

다소 나열적인 설명이었지만 어느 정도 감을 잡기 위해서 중복 설명했다. D&D 하는 과정과 메소드호출, 클래스와 인터페이스의 API 를 꼼꼼하게 살펴보면 이해할 수 있으리라 생각한다. 가장 기본적인 D&D 를 하기 위해서는 반드시 DragSource, DropTarget, DragGestureRecognizer, Transferable 이 반드시 생성이 되어야 하고, Listener 가 구현되어야 한다는 것을 명심하길 바란다. 그러나 실제 구현은 몇가지 객체를 생성하고 데이타를 Wrapping 하고 핸들링하는 것을 빼면 나머지 동작은 프로그래머 재량에 상관없이 자체적으로 구현이 된다.
Adding D&D to Swing Component
여기서도 두가지 방법이 있을 수가 있는데 한가지 방법으로는 Swing Component 를 상속해 객체를 만들어 그것을 DragSource, DropTarget 으로 만들어 사용하는 방법과 제 3의 콤포넌트를 생성해 두가지를 수행하는 방법이 있다.
첫번째 방법으로 수행을 하면 반드시 객체만이 D&D 특성을 가질수가 있고, 표준 Component 는 그런 특성을 공유할 수 없음을 명심하길 바란다. 가령 JList 를 상속한 ListDragSource 객체를 만들어서 D&D 를 추가해 사용한다면 ListDragSource 만이 그런 동작을 할 수 있다는 것을 의미한다.

아래에 있는 그림은 이 프로그램을 실행시켰을때 볼 수 있는 화면이다. 왼쪽은 DragSource 로 JTree 를 이용해 구현했고, 오른쪽은 DropTarget 은 JTextPane 을 이용해 구현했음을 보길 바란다. 이것은 .txt 와 .java 로만 한정되어 있기 때문에 다른 확장자를 가진 파일을 D&D 했을 경우에는 에러창을 발견하게 될 것이다. 그리고 .txt 와 .java 파일은 그 내용을 JTextPane 에서 볼 수 있을 것이다.


오른쪽의 파일을 D&D 수행할 수가 있다


확장자가 .txt 와 .java 가 아닐 경우에 볼 수 있는 경고창이다.


위의 프로그램은 얼마든지 응용해서 확장할 수 있다고 생각한다. 다른 파일도 볼 수 있게끔 할 수 있고, image 도 가능하리라 생각한다.
다음 장에서는 소스코드 분석에 들어가도록 하겠다. 먼저 해당 API 를 꼼꼼히 살펴본 다음에 코드분석에 임하길 바란다.


 
  

이번 장에서는 소스분석과 함께 설명을 하겠다. 단풍잎 부분에 API 에 대한 설명을 첨가하겠다.

Source Analysis
DragTree.java 소스이다. 이전 장에서 본 JTree 부분의 DragSource 를 정의한 부분이다.
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.io.*;

class DragTree extends JTree implements DragGestureListener, DragSourceListener {

     public DragTree() {
       DragSource dragSource = DragSource.getDefaultDragSource();
         // static 메소드인 위 메소드로 DragSource 에 대한 참조를 얻는다.
            그리고, 위 참조객체를 이용해 DragGestureRecognizer 를 생성하게 되는 것이다.
  
       dragSource.createDefaultDragGestureRecognizer(
	 this, // component where drag originates
	 DnDConstants.ACTION_COPY_OR_MOVE, // actions
	 this); // drag gesture recognizer  
  public DragGestureRecognizer createDefaultDragGestureRecognizer(
                      Component c, int actions, DragGestureListener dgl)

       c - The Component target for the recognizer
       actions - The permitted source actions
       dgl - The DragGestureListener to notify
setModel(createTreeModel()); // Tree 에 관한 코딩부분으로 innerclass 로 구현돼 있다. addTreeExpansionListener(new TreeExpansionListener(){ public void treeCollapsed(TreeExpansionEvent e) {} public void treeExpanded(TreeExpansionEvent e) { TreePath path = e.getPath(); if(path != null) { FileNode node = (FileNode) path.getLastPathComponent(); if( ! node.isExplored()) { DefaultTreeModel model = (DefaultTreeModel)getModel(); node.explore(); model.nodeStructureChanged(node); } } } }); } DragGestureListener 인터페이스에서 구현해야 하는 메소드이다. public void dragGestureRecognized(DragGestureEvent e) { String s = getFilename(); // 확장자를 .txt 와 .TXT, .java 로 한정지은 부분이다. 다른 파일까지 읽고 싶으면 이 부분을 수정하면 된다. if(s.endsWith(".txt") || s.endsWith(".TXT") || s.endsWith(".java")) { e.startDrag(DragSource.DefaultCopyDrop, // cursor new StringSelection(s), // transferable
           public void startDrag(Cursor dragCursor,
                      Transferable transferable,
                      DragSourceListener dsl)  throws InvalidDnDOperationException
           여기서는 Transferable 로 StringSelection 클래스가 사용되었다.
           이 클래스는 간단한 java String 을 text 포맷으로 수송하는데 필요한 기능을 
           구현하는 클래스이다.
// startDrag() 메소드로 Tree 는 drag 를 초기화하고, draggesture 에 반응하게 된다. } else { // invokeLater 호출에 대해서 생각해 봐야 한다. 이 메소드는 application thread 가 GUI 를 갱신해야 될 필요가 있을때 사용되어지며 당면해 있는 AWT 이벤트가 수행된 이후에 수행되게 한다. 이 메소드의 사용으로 경고창이 뜨는 시간을 조절할 수가 있게 한다. SwingUtilities.invokeLater(new Runnable() { public void run() { JOptionPane.showMessageDialog( SwingUtilities.getRootPane(DragTree.this), "Only \".txt\" and \".java\" files " + "can be dragged", "Not Draggable", JOptionPane.ERROR_MESSAGE); } }); } } // 이 부분은 DragSourceListener 메소드 구현부분이다. public void dragDropEnd(DragSourceDropEvent e) {} public void dragEnter(DragSourceDragEvent e) {} public void dragExit(DragSourceEvent e) {} public void dragOver(DragSourceDragEvent e) {} public void dropActionChanged(DragSourceDragEvent e) {} public String getFilename() { TreePath path = getLeadSelectionPath(); FileNode node = (FileNode)path.getLastPathComponent(); return ((File)node.getUserObject()).getAbsolutePath(); } private DefaultTreeModel createTreeModel() { File root = new File("C:" + File.separator); FileNode rootNode = new FileNode(root); rootNode.explore(); return new DefaultTreeModel(rootNode); } } // 이 부분은 왼쪽 창의 JTree 인터페이스 설정부분으로 자세한 설명은 안하겠다. class FileNode extends DefaultMutableTreeNode { private boolean explored = false; public FileNode(File file) { setUserObject(file); } public boolean getAllowsChildren() { return isDirectory(); } public boolean isLeaf() { return !isDirectory(); } public File getFile() { return (File)getUserObject(); } public boolean isExplored() { return explored; } public boolean isDirectory() { File file = getFile(); return file.isDirectory(); } public String toString() { File file = (File)getUserObject(); String filename = file.toString(); int index = filename.lastIndexOf(File.separator); return (index != -1 && index != filename.length()-1) ? filename.substring(index+1) : filename; } public void explore() { if(!isDirectory()) return; if(!isExplored()) { File file = getFile(); File[] children = file.listFiles(); for(int i=0; i < children.length; ++i) add(new FileNode(children[i])); explored = true; } } }
이상으로 DragTree.java 소스를 살펴보았다. 설명이 조금 부족한 감이 있으니 자기나름대로 살펴보길 바란다. 다음에는 이 프로그램의 메인코드인 Test.java 에 대해서 살펴보겠다.


 
  
Source Analysis
Test.java 소스이다. main() 와 기본 인터페이스가 설정돼 있다. 아래의 코드를 살펴보면 알겠지만 Test 생성자는 drop target 을 생성해낸다. 즉 TextPane 을 drop target component 로 생성하는 것이다.
실제 drop 이 일어났을때 Test.drop() 메소드가 호출이 된다. drop 과 연관된 transferable 은 String 으로써 데이타를 제공해주며 drop 을 받아들인 후에 readFile() 메소드가 호출이 돼 file 의 내용을 TextPane 으로 로딩을 하게 된다. Drop 이 완료된 후 e.dropComplete(true) 가 호출이 돼 실제 drop 이 완료되게 된다.
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.io.*;

public class Test extends JFrame implements DropTargetListener {
	private JTextPane textPane = new JTextPane();

	public Test() {
                   super("Drag and Drop With Swing");
                   new DropTarget(textPane,DnDConstants.ACTION_COPY_OR_MOVE,this);
	   JSplitPane splitPane =
                      new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,createTreePanel(),createTextPanel());
	   splitPane.setDividerLocation(250);	
	   splitPane.setOneTouchExpandable(true);
	   getContentPane().add(splitPane, BorderLayout.CENTER);
	}
	public static void main(String args[]) {
	  Test test = new Test();
	  test.setBounds(300,300,850,350);
	  test.setVisible(true);
	  test.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
	  test.addWindowListener(new WindowAdapter() {
	  public void windowClosed(WindowEvent e) {
                        System.exit(0);
	    }
	  });
	}
	private JPanel createTreePanel() {
	  JPanel treePanel = new JPanel();
	  DragTree tree = new DragTree();
	  treePanel.setLayout(new BorderLayout());
	  treePanel.add(new JScrollPane(tree), BorderLayout.CENTER); 
	  treePanel.setBorder(BorderFactory.createTitledBorder(
		"Drag source for filenames"));
        	return treePanel;
	}
	private JPanel createTextPanel() {
	  JPanel textPanel = new JPanel();
	  textPanel.setLayout(new BorderLayout());
	  textPanel.add(new JScrollPane(textPane),BorderLayout.CENTER);
	  textPanel.setMinimumSize(new Dimension(375,0));
	  textPanel.setBorder(BorderFactory.createTitledBorder(
	    "Drop target for filenames"));

	  return textPanel;
	}
	private void readFile(final String filename) {
	  EditorKit kit = textPane.getEditorKit();
	  Document document = textPane.getDocument();

          try {
	    document.remove(0,document.getLength());
	    kit.read(new FileReader(filename), document, 0); 
	  }
	  catch(Exception ex) {
	    ex.printStackTrace();
	  }
	}
        
	public void drop(DropTargetDropEvent e) {
	  try {
	   DataFlavor stringFlavor = DataFlavor.stringFlavor;
	   Transferable tr = e.getTransferable();
 public class DataFlavor extends Object implements Externalizable, Cloneable
 
 이 클래스는 D&D 수행할 때 클립보드에 들어가는 데이타의 형태를 제공해주는 클래스이다.
 여기서 사용된 StringFlavor 는 java Unicode String class 을 나타낸다.
 MimeType = "application/x-java-serialized-object" 을 나타내게 된다.
if(e.isDataFlavorSupported(stringFlavor)) { String filename = (String)tr.getTransferData(stringFlavor); e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); // 인자로 넘어온 액션으로 drop 을 받아들인다. readFile(filename); textPane.setCaretPosition(0); // Text 의 첫줄이 TextPane 의 첫줄에 오게끔 설정한다. e.dropComplete(true); } else { e.rejectDrop(); // Drop 을 거절한다. } } catch(IOException ioe) { ioe.printStackTrace(); } catch(UnsupportedFlavorException ufe) { ufe.printStackTrace(); } } // DropTargetListener 메소드를 구현한 부분이다. public void dragEnter(DropTargetDragEvent e) { } public void dragExit(DropTargetEvent e) { } public void dragOver(DropTargetDragEvent e) { } public void dropActionChanged(DropTargetDragEvent e) { } }
출처:http://www.javastudy.co.kr/docs/b612/swing/draganddrop.html

저작자 표시 비영리
Posted by 티엘로

JTextField 클래스를 이용하면 간단하게 텍스트 입력 필드를 화면에 뿌려줄 수 있다. 그런데 JTextField 자체에는 입력 글자수를 제한하는 메써드가 존재하지 않는다. 즉, 10자 까지만 입력하게 하고 싶어도 기본 API로는 구현이 불가능하다. 입력 할 글자수를 제한하려면 다음과 같이 PlainDocument 클래스를 상속받은 클래스를 정의하여 구현할 수 있다.

 

public class JTextFieldLimit extends PlainDocument
{
      private int limit;                             // 제한할 길이
      public JTextFieldLimit(int limit)        // 생성자 : 제한할 길이를 인자로 받음
      {
            super();
            this.limit = limit;
      }

       // 텍스트 필드를 채우는 메써드 : 오버라이드
      public void insertString(int offset, String str, AttributeSet attr) throws BadLocationException 
      {
            if (str == null)
                  return;


            if (getLength() + str.length() <= limit) 
                  super.insertString(offset, str, attr);
       }
}

사용법은 다음과 같다.

textfield.setDocument(new JTextFieldLimit(10));

Posted by 티엘로

애플리케이션에서 사용할 데이터 모델 만들기

모델-뷰-컨트롤(MVC) 아키텍처에 익숙하다면 지금까지 만든 것이 애플리케이션의 뷰에 해당한다는 것을 알아차렸을 것이다. 이 부분이야 말로 Jigloo가 진가를 발휘하는 부분이다. 이제는 모델을 만들어야 한다. 다시 말하면 시스템에서 사용할 데이터를 가져오고 사용할 자바 코드를 만들어야 함을 의미한다. Jigloo는 여기서도 역시 빛을 발한다. Jigloo는 단순한 이클립스 플러그인으로 Jigloo를 사용할 때도 직접 이클립스의 강력한 기능들을 힘들이지 않고 바로 사용할 수 있다. 이클립스는 자바 코드 작성과 데이터를 다루는 데 매우 훌륭한 도구이기 때문에 Jigloo 역시 이런 작업을 하기에 적합하다.

스키마 만들기

언급했다시피, XML 형식으로 데이터를 저장하고 JAXB를 사용하여 XML을 읽고 쓰는 작업을 할 것이다. 따라서 스키마를 작성할 필요가 있다.


Listing 1. 워크플로우 XML 스키마
                    
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema" 
  targetNamespace="org:developerworks:workflow" 
  xmlns:dw="org:developerworks:workflow">

     <element name="workflow" type="dw:workflow"/>
     <complexType name="workflow">
          <sequence>
               <element name="user" type="dw:user" minOccurs="0"
maxOccurs="unbounded"/>
               <element name="po" type="dw:purchaseOrder"
minOccurs="0" maxOccurs="unbounded"/>
          </sequence>
     </complexType>
     
     <complexType name="user">
          <sequence>
               <element name="username" type="string"/>
               <element name="role" type="dw:role"/>
          </sequence>
          <attribute name="id" type="integer" use="required"/>
     </complexType>
     <simpleType name="role">
          <restriction base="string">
               <enumeration value="worker"/>
               <enumeration value="manager"/>
          </restriction>
     </simpleType>
     
     <complexType name="purchaseOrder">
          <sequence>
               <element name="priority" type="dw:priority"/>
               <element name="dateRequested" type="date"/>
               <element name="dateNeeded" type="date" minOccurs="0"/>
               <element name="itemName" type="string"/>
               <element name="itemDescription" type="string" minOccurs="0"/>
               <element name="quantityRequested" type="integer"/>
               <element name="url" type="anyURI" minOccurs="0"/>
               <element name="price" type="decimal"/>
               <element name="status" type="dw:orderStatus"/>
               <element name="submittedBy" type="integer"/>
               <element name="processedBy" type="integer" minOccurs="0"/>
          </sequence>
          <attribute name="id" type="integer" use="required"/>
     </complexType>
     
     <simpleType name="priority">
          <restriction base="string">
               <enumeration value="normal"/>
               <enumeration value="high"/>
          </restriction>
     </simpleType>
     
     <simpleType name="orderStatus">
          <restriction base="string">
               <enumeration value="pending"/>
               <enumeration value="approved"/>
               <enumeration value="rejected"/>
          </restriction>
     </simpleType>
</schema>

스키마에 해당하는 XML 파일에 바인딩할 자바 클래스를 만들기 위해 JAXB를 사용할 수 있다. 자바 6을 사용한다면 JAXB가 내장되어 있다. 자바 5를 사용한다면 썬에서 JAXB를 다운로드해야 한다. 여러분은 명령행 도구인 xjc를 사용하길 원할 수도 있다. 예를 들어 xjc workflow.xsd와 같이 사용할 수 있다. 이 명령어를 사용하여 workflow.xsd를 스키마 컴파일러가 파싱하고 클래스를 만들어낼 것이다. 그런 다음 클래스 파일들을 프로젝트에 복사하여 사용할 수 있다. 프로젝트에 XML 디렉터리를 만들고 그 안에 스키마 파일도 복사한다. 이제 예제로 사용할 XML 파일을 만들자.


Listing 2. 초기 XML 데이터
                    
<?xml version="1.0" encoding="UTF-8"?>
<dw:workflow xmlns:dw="org:developerworks:workflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="org:developerworks:workflow
workflow.xsd ">
  <user id="0">
    <username>homer</username>
    <role>worker</role>
  </user>
  <user id="1">
    <username>bart</username>
    <role>manager</role>
  </user>
  <po id="0">
    <priority>normal</priority>
    <dateRequested>2001-01-01</dateRequested>
    <dateNeeded>2001-01-01</dateNeeded>
    <itemName>stapler</itemName>
    <itemDescription>A great stapler</itemDescription>
    <quantityRequested>2</quantityRequested>
    <url>http://www.thinkgeek.com/homeoffice/gear/61b7/</url>
    <price>21.99</price>
    <status>pending</status>
    <submittedBy>0</submittedBy>
  </po>
</dw:workflow>

모든 요소들을 추가한 뒤 Package Explorer는 그림 26처럼 보일 것이다(필요하다면 JAXB jar 파일들을 자바 빌드 패스에 추가해야 한다).


그림 26. JAXB 클래스와 XML 파일이 추가된 Package explorer
 
사용자 삽입 이미지





위로


데이터 접근

Package Explorer에 몇 가지 파일들이 추가된 것을 확인할 수 있을 것이다. 바로 WorkflowDao와 XmlWorkflow다. WorkflowDao는 데이터를 사용할 때 필요로 하는 작업들을 정의한 인터페이스다(Listing 3).


Listing 3. WorkflowDao 인터페이스
                    
package org.developerworks.workflow;

import java.util.List;

public interface WorkflowDao {
     public List<User> getUsers();
     public List<PurchaseOrder> getAllOrders();
     public List<PurchaseOrder> getAllPendingOrders();
     public List<PurchaseOrder> getOrdersForUser(int userId);
     public void saveOrder(PurchaseOrder order);
     public void setOrderStatus(int orderId, OrderStatus status);
}

고전적인 데이터 접근 객체(Data Access Object) 패턴을 사용하고 있다. 간단하게 인터페이스를 정의하고 이 인터페이스에 대한 애플리케이션 코드를 작성한다. XML을 사용하여 JAXB 기반 구현을 할 것이다. 하지만 이 디자인을 사용하여 손쉽게 데이터베이스 기반 구현 같은 기타 구현으로 변경할 수 있다. 이 인터페이스의 구현체가 XmlWorkFlow다.


Listing 4. XmlWorkflow 인터페이스 구현
                    
package org.developerworks.workflow;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class XmlWorkflow implements WorkflowDao {
     private static final String DATA_FILE = "data.xml";
     private static XmlWorkflow instance;
     
     private Workflow workflow;
     
     private XmlWorkflow() {
          try {
               JAXBContext ctx = this.getContext();
               Unmarshaller unm = ctx.createUnmarshaller();
               File dataFile = this.getDataFile();
               InputStream inputStream;
               if (dataFile.exists() && dataFile.length() > 0){
                    inputStream = new FileInputStream(dataFile);
               } else {
                    inputStream = 
Thread.currentThread().getContextClassLoader().getResourceAsStream("xml/"+DATA_FILE);
               }
               JAXBElement element = (JAXBElement) unm.unmarshal(inputStream);
               this.workflow = (Workflow) element.getValue();
          } catch (JAXBException e) {
               e.printStackTrace();
               throw new RuntimeException("Failed to read data file",e);
          } catch (FileNotFoundException e) {
               e.printStackTrace();
               throw new RuntimeException("Could not open data file", e);
          }
     }
     
     public static XmlWorkflow getInstance(){
          if (instance == null){
               instance = new XmlWorkflow();
          }
          return instance;
     }

     public List<PurchaseOrder> getAllOrders() {
          return this.workflow.getPo();
     }

     public List<PurchaseOrder> getAllPendingOrders() {
          List<PurchaseOrder> allOrders = this.getAllOrders();
          List<PurchaseOrder> pending = new ArrayList<PurchaseOrder>();
          for (PurchaseOrder order : allOrders){
               if (order.getStatus().equals(OrderStatus.PENDING)){
                    pending.add(order);
               }
          }
          return pending;
     }

     public List<PurchaseOrder> getOrdersForUser(int userId) {
          List<PurchaseOrder> allOrders = this.getAllOrders();
          List<PurchaseOrder> userOrders = new ArrayList<PurchaseOrder>();
          for (PurchaseOrder order : allOrders){
               if (order.getSubmittedBy().intValue() == userId){
                    userOrders.add(order);
               }
          }
          return userOrders;
     }

     public List<User> getUsers() {
          return this.workflow.getUser();
     }

     public void saveOrder(PurchaseOrder order) {
          int index = 0;
          for (PurchaseOrder po : this.workflow.getPo()){
               if (po.getId().intValue() == order.getId().intValue()){
                    this.workflow.getPo().set(index, order);
                    this.saveData();
                    return;
               }
               index++;
          }
          // add new order
          order.setId(new BigInteger(Integer.toString(this.workflow.getPo().size())));
          this.workflow.getPo().add(order);
          this.saveData();
     }
     
     public void setOrderStatus(int orderId, OrderStatus status) {
          for (PurchaseOrder po : this.workflow.getPo()){
               if (po.getId().intValue() == orderId){
                    po.setStatus(status);
                    this.saveData();
                    return;
               }
          }
     }

     private void saveData(){
          File dataFile = this.getDataFile();
          try {
               JAXBContext ctx = this.getContext();
               Marshaller marshaller = ctx.createMarshaller();
               FileOutputStream stream = new FileOutputStream(dataFile);
               marshaller.marshal(this.workflow, stream);
          } catch (JAXBException e) {
               e.printStackTrace();
               throw new RuntimeException("Exception serializing data file",e);
          } catch (FileNotFoundException e) {
               e.printStackTrace();
               throw new RuntimeException("Exception opening data file");
          }
     }
     
     private File getDataFile() {
          String tempDir = System.getProperty("java.io.tmpdir");
          File dataFile = new File(tempDir + File.separatorChar + DATA_FILE);
          return dataFile;
     }

     private JAXBContext getContext() throws JAXBException {
          JAXBContext ctx = JAXBContext.newInstance("org.developerworks.workflow");
          return ctx;
     }
     
     public static void main(String[] args){
          XmlWorkflow dao = XmlWorkflow.getInstance();
          List<User> users = dao.getUsers();
          assert(users.size() == 2);
          for (User user : users){
               System.out.println("User: " + user.getUsername() + " ID:" + user.getId());
          }
          List<PurchaseOrder> orders = dao.getAllOrders();
          assert(orders.size() == 1);
          for (PurchaseOrder order : orders){
               System.out.println("Order:" + order.getItemName() + "
ID:" + order.getId() + " Status:" + order.getStatus());
          }
          PurchaseOrder order = orders.get(0);
          order.setStatus(OrderStatus.APPROVED);
          order.setProcessedBy(new BigInteger("1"));
          dao.saveOrder(order);
     }
}

위에서 만들었던 예제 파일을 초기에 읽어 들이는 것을 확인할 수 있지만 시스템 임시 폴더에 data.xml로 변경사항을 저장한다. 데이터를 저장하기에 가장 안전한 장소는 아니지만 예제 애플리케이션에 사용하기에는 적절하다. 이 클래스 파일 안에 간단한 main 메서드가 있는 것 또한 볼 수 있다. JAXB가 동작하는 것을 간단하게 단위 테스트하기 위한 용도로 만들었다. 만약에 자바 5를 사용한다면 JAXB jar 파일을 프로젝트의 클래스패스에 추가해야 한다. 계속해서 작업하려면 그 파일들을 복사하여 프로젝트에 추가하거나 프로젝트 외부에 그 파일들이 위치한 장소를 참조할 수 있도록 해야 한다.




위로


애플리케이션 초기화하기

애플리케이션과 인터랙트하기 전에 모든 요소를 초기화해야 한다. 먼저 애플리케이션에서 사용할 모델 객체를 몇 개 선언해야 한다. Listing 5에 있는 코드를 추가하여 WorkflowMain 멤버 변수를 추가한다.


Listing 5. 모델 객체 선언
                    
     // Data Model Objects
     private java.util.List<User> users;
     private User user;
     
     // Service Object
     private WorkflowDao dao = XmlWorkflow.getInstance();

코드에 접근하려면 Workflow.java 파일을 마우스 오른쪽 클릭을 하고 Open With > Java Editor를 선택한다.

애플리케이션의 initGUI() 메서드 코드를 수정한다. 사용자 명단을 초기화하기 위해 private 메서드를 만들겠다.


Listing 6. 사용자 메서드 만들기
                    
     private void initUserList(){
          this.users = dao.getUsers();
          for (User u : users){
               this.userListCombo.add(u.getUsername());
          }
     }

userListCombo를 정의한 후 initGUI()에서 이 메서드를 호출한다.


Listing 7. 사용자 메서드 호출
                    
                {
                    userListCombo = new Combo(this, SWT.NONE);
                    userListCombo.setText("Users");
                    userListCombo.setBounds(28, 35, 105, 21);
                    this.initUserList();
               }




위로


이벤트를 사용하여 뷰와 모델을 엮기

뷰와 모델을 만들었다. 이제 그 둘을 엮어 보자. 컨트롤러가 필요하다. SWT(와 스윙)는 모든 UI 프레임워크에서 사용하는 간단한 기술을 사용한다. 바로 이벤트 주도 시스템이라는 것이다. 이벤트를 사용하여 언제 모델에 있는 작업을 호출하고 뷰를 수정할지 알려줄 수 있다.

이제 다시 비주얼 디자이너로 돌아가자. 모델과 엮기를 원하는 첫 번째 UI 이벤트는 사용자 콤보 리스트에서 사용자를 선택했을 때다. 콤보 컨트롤을 선택하고 GUI 속성 뷰에서 Event 탭으로 변경하면 그림 27에 보이는 것과 같은 화면을 확인할 수 있을 것이다.


그림 27. 콤보 컨트롤 이벤트에 접근하기
 
사용자 삽입 이미지











































몇몇 리스너들을 콤보 컨트롤에서 볼 수 있다. SelectionListener를 선택한다. 이 리스너는 콤보 컨트롤에서 무언가를 선택할 때마다 SelectionEvent를 발생시킨다. 이 이벤트를 다룰 것을 익명 메서드로 그것을 인라인에서 다룰 수도 있고이 이벤트를 다룰 메서드를 정의할 수도 있다. 후자를 선택하겠다. 이렇게 하면 userListComboWidgetSelected라는 메서드가 생성된다. 이벤트를 다루기 위한 코드는 Listing 8에 나와있다.


Listing 8. 사용자 콤보 리스트 선택 코드
                    
     private void userListComboWidgetSelected(SelectionEvent evt) {
          int index = this.userListCombo.getSelectionIndex();
          if (index >= 0){
               this.user = this.users.get(index);
               System.out.println("User selected="+this.user.getUsername());
               purchaseOrderTable.removeAll();
               java.util.List<PurchaseOrder> orders;
               boolean isManager = this.user.getRole().equals(Role.MANAGER);
               if (isManager){
                    orders = dao.getAllPendingOrders();
               } else {
                    orders = dao.getOrdersForUser(this.user.getId().intValue());
               }
               this.approveButton.setVisible(isManager);
               this.rejectButton.setVisible(isManager);
               for (PurchaseOrder order : orders){
                    displayPurchaseOrder(order);
               }
          }
     }

여기서 살펴봐야 할 것들이 많다. 먼저 사용자가 관리자인지 확인한다. 관리자가 아니면 사용자의 모든 구매 주문 목록을 보여준다. 관리자가 맞다면 대기중인 주문 목록만을 보여준다. 다음으로 관리자가 아니면 승인/취소 버튼이 보이지 않도록 한다. 마지막으로 purchaseOrderTable에 있는 모든 주문 데이터들을 데이터 접근 객체를 사용해 가져와 보여준다.

이제 Approve 버튼에 이벤트를 추가한다. 비주얼 디자이너에 보이는 Approve 버튼을 선택하고 GUI 속성 창에 있는 이벤트 탭으로 이동한다. 버튼을 선택했을 때 실행되어야 하기 때문에 그림 28에 보이는 것처럼 여기서도 selection event를 사용할 것이다.


그림 28. Approve 버튼 선택 이벤트 설정
 
사용자 삽입 이미지










































그리고 나서 이 이벤트를 다룰 코드를 추가한다.


Listing 9. Approve 버튼 선택 이벤트 코드
                    
     private void approveButtonWidgetSelected(SelectionEvent evt) {
          TableItem[] selected = this.purchaseOrderTable.getSelection();
          if (selected != null){
               for (TableItem item : selected){
	this.dao.setOrderStatus(Integer.parseInt(item.getText(4)), OrderStatus.APPROVED);
                    item.setText(3, OrderStatus.APPROVED.toString());
               }
          }
     }

reject 버튼도 이와 매우 비슷하게 할 수 있다. 선택 이벤트 리스너를 추가하고 비슷한 코드를 실행한다. 유일한 차이가 있다면 주문 상태를 APPROVED가 아닌 REJECTED로 바꾼다는 것이다.


Listing 10. Reject 버튼 코드
                    
     private void rejectButtonWidgetSelected(SelectionEvent evt) {
          TableItem[] selected = this.purchaseOrderTable.getSelection();
          if (selected != null){
               for (TableItem item : selected){
	this.dao.setOrderStatus(Integer.parseInt(item.getText(4)), OrderStatus.REJECTED);
                    item.setText(3, OrderStatus.REJECTED.toString());
               }
          }
     }

이제 남은 건 Submit 버튼이다. 이 버튼을 사용하여 새로운 구매 주문을 추가할 수 있어야 한다. 이 버튼 역시 다른 버튼들과 유사하다. 선택 이벤트에 대한 이벤트 핸들러를 그림 29처럼 추가한다.


그림 29. PO 버튼 선택 리스너 추가
 
사용자 삽입 이미지













































이벤트를 다룰 코드를 추가한다.


Listing 11. PO 버튼 이벤트 핸들러 코드 추가
                    
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.GregorianCalendar;

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

...

     private void addButtonWidgetSelected(SelectionEvent evt) {
          try {
               this.addPurchaseOrder();
          } catch (Exception e) {
               throw new RuntimeException("Exception adding purchase order",e);
          }
          this.formItemText.clearSelection();
          this.formPriceText.clearSelection();
          this.formQuantityText.clearSelection();
     }
     
     private void addPurchaseOrder() throws Exception{
          String item = this.formItemText.getText();
          String priceString = this.formPriceText.getText();
          String quantityString = this.formQuantityText.getText();
          BigDecimal price = new BigDecimal(priceString);
          BigInteger quantity = new BigInteger(quantityString);
          PurchaseOrder po = new PurchaseOrder();
          int num = this.dao.getAllOrders().size();
          String numString = Integer.toString(num);
          BigInteger newId = new BigInteger(numString);
          po.setId(newId);
          po.setItemName(item);
          po.setPrice(price);
          po.setQuantityRequested(quantity);
          po.setPriority(Priority.NORMAL);
          po.setStatus(OrderStatus.PENDING);
          po.setSubmittedBy(this.user.getId());
          GregorianCalendar cal = (GregorianCalendar) GregorianCalendar.getInstance();
          DatatypeFactory factory = DatatypeFactory.newInstance();
          XMLGregorianCalendar now = factory.newXMLGregorianCalendar(cal);
          po.setDateRequested(now);
          this.dao.saveOrder(po);
          this.displayPurchaseOrder(po);
     }

	private void displayPurchaseOrder(PurchaseOrder order) {
		String[] row = new String[] 
		         {order.getItemName(), order.getPrice().toString(), 
				order.getQuantityRequested().toString(),
order.getStatus().toString(), order.getId().toString()};
		TableItem tableItem = new TableItem(purchaseOrderTable,0);
		  tableItem.setText(row);
		  this.purchaseOrderTable.showItem(tableItem);
	}




위로


GUI 테스트

GUI를 테스트해야 할 시간이다. 그림 30처럼 클래스를 오른쪽 클릭을 한 후 Run As > SWT Application을 선택한다.


그림 30. 애플리케이션 실행
사용자 삽입 이미지


 



















이렇게 하면 애플리케이션이 실행된다.


그림 31. 워크플로우 애플리케이션
 
사용자 삽입 이미지

















기본 데이터로 대기중인 구매 주문을 넣어뒀기 때문에 사용자 bart를 선택한 뒤 구매 주문을 승인할 수 있다.


그림 32. 주문 승인
사용자 삽입 이미지
















사용자를 homer로 변경한 뒤 구매 주문의 상태를 확인해 보자.


그림 33. 사용자가 자기 PO 보기
사용자 삽입 이미지
















새로운 주문을 추가할 수도 있다.


그림 34. PO 추가
사용자 삽입 이미지















Add PO
를 클릭하여 테이블에 구매 주문을 추가할 수 있다.


그림 35. 새 PO 추가
사용자 삽입 이미지















다시 bart로 변경하여 승인 또는 취소할 수 있다.


그림 36. 업데이트된 관리자 화면
사용자 삽입 이미지
Posted by 티엘로

개요

이번 절에서는 Jigloo 플러그인을 설치하고 설정하기 전에 이것으로 무엇을 할 수 있는지 살펴보겠다.

Jigloo란 무엇인가?

는 비주얼 자바 GUI 빌더다. Jigloo는 CloudGarden에서 만든 이클립스(와 WebSphere® Studio) 플러그인이다. 상업적인 용도가 아니라면 공짜로 쓸 수 있다. CloudGarden에서 라이선스를 받아 상업적인 용도로 사용할 수 있다.

Jigloo로 무엇을 할 수 있나?

Jigloo는 스윙과 SWT GUI를 개발할 수 있는 고전적인 위지위그(WYSIWYG) 편집기다. 만약 자바로 데스크톱 애플리케이션을 개발하고 싶다면 Jigloo가 최고의 대안이 될 것이다. 하지만 그것뿐만이 아니다.

Jigloo는 라운드 트리핑(round-tripping)을 지원한다. 이 말은 코드 수정을 통해서만 GUI에 변화를 줄 수 있는 것이 아니라 GUI에 직접 변화를 주어 코드가 수정되는 것을 확인할 수 있다는 뜻이다. 기존에 만들어진 GUI를 변경하고 싶을 때도 Jigloo는 훌륭한 대안이 될 수 있다. 이미 존재하고 있는 애플리케이션을 분석하고 화면을 통해 수정할 수 있도록 도와준다. 그리고 Jigloo를 NetBeans나 JBuilder와 같은 IDE와도 함께 쓸 수 있다.

Jigloo는 스윙/SWT 호환을 염두에 두고 개발되었다. 각각의 기술이 제공하는 다양한 레이아웃 옵션을 지원한다. 새로운 UI를 만드는 것뿐만 아니라 Jigloo를 사용하여 스윙과 SWT를 서로 변환할 수도 있다. SWT_AWT 브리지(bridge)를 사용하여 스윙 컴포넌트를 SWT 애플리케이션에 포함시킬 수도 있다. 하지만 이번 튜토리얼에서 우리는 SWT GUI를 사용하는 워크플로우 애플리케이션을 개발하겠다.

Jigloo 설치

Jigloo는 이클립스 플러그인이기 때문에 설치가 매우 쉽다. 만약 몇 년 간 이클립스를 사용해오고 있다면 플러그인을 다운로드해 이클립스 설치 디렉터리에 압축을 푸는 방법으로 플러그인을 설치해봤을 것이다. 하지만 최신 버전 이클립스를 사용하면 더 쉽게 설치할 수 있다. 이클립스 업데이트 관리자 기능이 바로 그것이다. 이 기능을 사용하려면 Help > Software Updates > Find and Install을 선택하면 된다. 그러면 Install/Update 대화창이 열린다. 이 때 “Search for new features to install” 옵션을 선택한다. 그리고 나서 Next를 클릭하면 Update Site 대화창으로 이동한다.

만약 다른 이클립스 플러그인들이 이미 설치되어 있다면 “Sites to include in search”에 이미 등록되어 있는 다른 사이트들이 보일 것이다. 이 때 목록에 보이는 모든 사이트들을 선택하지 않는다. 아래 보이는 New Update Site 대화창을 볼 수 있도록 New Remote Site를 클릭한다.


그림 1. 새 업데이트 사이트
새 업데이트 사이트

이 때 중요한 것은 URL 필드에 http://cloudgarden1.com/update-site를 입력하는 것이다. Name 필드에는 어떤 것이든 원하는 값을 입력할 수 있지만 Jigloo Update Site처럼 무슨 사이트인지 설명할 수 있는 내용이 좋을 것이다. OK를 클릭하면 Update Site 대화창으로 다시 이동하지만 이번에는 방금 입력한 업데이트 사이트가 목록에 포함되어 보일 것이다. Finish를 클릭하면 Search Results 대화창으로 이동한다. Search Results 대화창에서 Jigloo를 선택하고 Next를 클릭한다. 그럼 Feature License로 이동할 것이다.

이미 언급했던 것처럼 Jigloo는 무료로 사용할 수 있지만 상업적인 용도가 아닐 경우에만 그렇다. 만약 상용으로 사용할 것이라면 CloudGarden에서 전문가용 라이선스를 받아야 한다. 라이선스를 읽은 후 “I accept the terms in the license agreement”를 선택하여 라이선스에 동의한다. 그리고 나서 간단하게 Next를 클릭하면 Installation details 대화창으로 이동한다. Finish를 클릭하면 Feature Verification 대화창이 나온다. Install이나 Install All을 선택하면 된다. 그럼 이제 설치를 시작하게 된다. 이클립스는 CloudGarden에서 플러그인을 다운로드하고 설치할 것이다. 설치가 끝나면 이클립스를 재시작해야 설치 과정이 끝난다.

축하한다! 이제 Jigloo 설치가 끝났다. 다른 이클립스 플러그인들과 마찬가지로 전혀 어렵지 않았다. 이제 Jigloo를 시작할 준비가 끝났다. 간단한 설정부터 시작하자.




위로


Jigloo 설정하기

이클립스를 사용하여 새 자바 프로젝트를 생성한다. File > New > Project에서 Java Project를 클릭한다. Next를 클릭하여 New Project 창으로 이동한다.

이 튜토리얼에서 프로젝트의 이름은 “workflow”로 하겠다. 물론 여러분이 원하는 이름을 줄 수도 있다. 이름을 준 뒤 Finish를 클릭한다.

이미 언급했다시피, Jigloo를 사용해 스윙 또는 SWT GUI를 만들 수 있다. 이 튜토리얼에서 우리는 SWT GUI를 만들 것이다. 이 경우 약간의 추가 설정이 필요하다. SWT JAR 파일을 프로젝트의 클래스패스에 추가해야 한다. 이 작업을 하기 위해 프로젝트를 클릭하고 메뉴에서 File > Properties를 선택한다. 그럼 Project Properties 화면이 나타나고 여기서 왼쪽 네비게이션의 Java Build Path를 선택하여 Java Build Path 창으로 이동한다.

Libraries 탭을 클릭한 후 Add External JARs 버튼을 클릭한다. 그러면 file explorer 창이 뜬다. 아래 보이는 그림처럼 이클립스를 설치한 디렉터리인 $ECLIPSE_HOME의 하위 디렉터리인 $ECLIPSE_HOME/plug-ins로 이동한다.


그림 2. 이클립스 플러그인 디렉터리
이클립스 플러그인 디렉터리

여기서 org.eclipse.swt.X.X.X.jar를 볼 수 있을 것이다. X.X.X는 사용하는 플랫폼 종류와 설치한 이클립스 버전에 따라 다를 수 있다. Open을 클릭하고 Java Build Path 화면으로 이동하여 OK를 클릭한다.

기본적인 자바 프로젝트를 만들고 SWT 라이브러리를 클래스패스에 추가했다. 이제 Jigloo를 사용하여 워크플로우 애플리케이션 디자인과 개발을 시작할 준비가 끝났다.

워크플로우 애플리케이션

예제는 매우 간단한 워크플로우 애플리케이션이다. 두 종류의 사용자가 있는데 작업자와 관리자다. 작업자는 구매 요청을 입력할 때 이 애플리케이션을 사용한다. 작업자들은 구매 주문에 필요한 정보를 입력할 것이다. 그리고 입력한 모든 주문의 상태를 볼 수도 있어야 한다. 각각의 구매 주문은 세 가지(대기, 승인, 취소) 상태 중 한 상태여야 한다. 관리자에는 이 애플리케이션을 약간 다르게 사용한다. 관리자는 구매 주문을 대기 상태와 함께 볼 수 있어야 한다. 관리자는 구매 주문을 승인 또는 취소할 수 있어야 한다.

간단한 애플리케이션이므로 로그인이나 로그아웃은 신경 쓰지 않겠다. 대신 시스템의 모든 사용자 명단을 보여주고 그 중에 로그인할 사용자를 선택할 수 있도록 하겠다. 진짜 워크플로우 시스템에서는 동시성(concurrency)이 관건이다. 이것도 역시 예제 애플리케이션을 여러 시스템에서 실행하지 않을 것이기 때문에 고려하지 않겠다. 실제 워크플로우 시스템은 워크플로우 데이터를 관계형 데이터베이스 같은 공유 저장소에 영속성을 유지한다. 하지만 여기서는 최대한 단순하게 하기 위해 데이터를 XML 파일을 사용하여 영속성을 유지한다. JAXB를 사용하여 XML 형태로 데이터를 저장하고 읽을 것이다.

자, 이제 GUI를 만들기 위해 실제 코드를 작성해 보자

Posted by 티엘로