2010年9月16日 星期四

[C#]-利用clipboard轉換office內容物為圖片

最近接了一個case,
在office上選取物件利用ctrl+c複製物件到clipboard內,
並在picturebox控制項上利用ctrl+v將物件從clipboard取出,
並以圖片形式顯示在picturebox上,
最後將picturebox上的image存成圖片檔。

首先先記錄一下如何判斷鍵盤的組合鍵,如下:
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    switch (e.KeyData)
    {
        case (Keys.Control | Keys.V):
            // do something 
            break;
    }
}

接下來的重點是從clipboard內取出物件時必須知道它支援那些格式,
我們可以利用下面的方式知道該物件所支援的格式有那些:
foreach (string format in Clipboard.GetDataObject().GetFormats())
{
    // do something
}
也有一些簡便的方式,例如Clipboard.ContainsText()或Clipboard.ContainsImage()等可以判斷。

再來我們準備一些method來從clipboard中取出office中大部份常見的格式。
Bitmap格式的物件,我們都能用下面的方式取得image:
public void CatchBitmap()
{
    this.pictureBox1.Image = (Bitmap)Clipboard.GetImage();
}

這裡遇到比較麻煩的困難點是EnhancedMetafile格式,
必須要用win32裡的API去取得系統的clipboard來使用,方式如下:
public const uint CF_METAFILEPICT = 3;
public const uint CF_ENHMETAFILE = 14;

[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern bool OpenClipboard(IntPtr hWndNewOwner);

[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern bool CloseClipboard();

[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern IntPtr GetClipboardData(uint format);

[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern bool IsClipboardFormatAvailable(uint format);

public void CatchEMF()
{
    if (OpenClipboard(this.Handle)) {
        if (IsClipboardFormatAvailable(CF_ENHMETAFILE))
        {
            IntPtr ptr = GetClipboardData(CF_ENHMETAFILE);
            if (!ptr.Equals(new IntPtr(0)))
            {
                Metafile metafile = new Metafile(ptr, true);
                //Set the Image Property of PictureBox 
                this.pictureBox1.Image = metafile;
            }
        }
        CloseClipboard();
    }
}

再來如果copy放進clipboard內的是純文字物件的話,
就必須要用Graphics來drawing string才能產生圖檔,如下:
public void DrawString()
{
    String sImageText = Clipboard.GetText();
    Bitmap objBmpImage = new Bitmap(1, 1);

    int intWidth = 0;
    int intHeight = 0;

    // Create the Font object for the image text drawing.
    Font objFont = 
        new Font("Arial", 20, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel);

    // Create a graphics object to measure the text's width and height.
    Graphics objGraphics = Graphics.FromImage(objBmpImage);

    // This is where the bitmap size is determined.
    intWidth = (int)objGraphics.MeasureString(sImageText, objFont).Width;
    intHeight = (int)objGraphics.MeasureString(sImageText, objFont).Height;

    // Create the bmpImage again with the correct size for the text and font.
    objBmpImage = new Bitmap(objBmpImage, new Size(intWidth, intHeight));

    // Add the colors to the new bitmap.
    objGraphics = Graphics.FromImage(objBmpImage);

    // Set Background color
    objGraphics.Clear(Color.White);
    objGraphics.SmoothingMode = SmoothingMode.AntiAlias;
    objGraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
    objGraphics.DrawString(sImageText, objFont, 
        new SolidBrush(Color.FromArgb(102, 102, 102)), 0, 0);
    objGraphics.Flush();
    this.pictureBox1.Image = objBmpImage;
}

接下來下面要講解的功能是如何把在excel內選取的cells自動轉換成對應的chart圖,
基本上複製的cells放進clipboard本身就支援Bitmap格式,
但直接取用會直接像剪貼那樣,如果要轉換成對應的chart圖則必須再行判斷,
這裡是利用Microsoft.Office.Interop.Excel來轉換,範例碼如下:
public void Convert()
{
    IDataObject iData = Clipboard.GetDataObject();
    String cellArea = (String)iData.GetData(DataFormats.Text);
    String[] rows = cellArea.Split(new Char[] { '\n' });

    Excel.Application xlApp;
    Excel.Workbook xlWorkBook;
    Excel.Worksheet xlWorkSheet;
    object misValue = System.Reflection.Missing.Value;

    xlApp = new Excel.ApplicationClass();
    xlWorkBook = xlApp.Workbooks.Add(misValue);
    xlWorkSheet = (Excel.Worksheet)xlWorkBook.Worksheets.get_Item(1);

    int rowLength = rows.Length;
    int rowCount = 1;
    int cellCount = 1;
    int maxCellCount = 1;
    
    foreach (String line in rows)
    {
        if (rowCount == rowLength)
        {
            break;
        }
        String[] cells = line.Split(null);
        int cellLength = cells.Length;
        foreach (String cell in cells)
        {
            if (cellCount == cellLength)
            {
                break;
            }
            if (cellCount > maxCellCount)
            {
                maxCellCount++;
            }
            xlWorkSheet.Cells[rowCount, cellCount] = cell;
            cellCount++;
        }
        rowCount++;
        cellCount = 1;
    }

    Excel.Range chartRange;

    Excel.ChartObjects xlCharts = (Excel.ChartObjects)xlWorkSheet.ChartObjects(Type.Missing);
    Excel.ChartObject myChart = (Excel.ChartObject)xlCharts.Add(10, 80, 300, 250);
    Excel.Chart chartPage = myChart.Chart;

    rowCount--;
    maxCellCount--;
    String endCorner = map.ElementAt(maxCellCount).Value;
    endCorner = endCorner + rowCount.ToString();
    
    // endCorner的格式例如:c7,就是cells右下角的範圍邊界點
    chartRange = xlWorkSheet.get_Range("A1", endCorner);
    chartPage.SetSourceData(chartRange, misValue);
    // 設定繪製的chart圖型式
    chartPage.ChartType = Excel.XlChartType.xlColumnClustered;
    chartPage.CopyPicture(Excel.XlPictureAppearance.xlScreen,
                          Excel.XlCopyPictureFormat.xlBitmap,
                          Excel.XlPictureAppearance.xlScreen);
    xlWorkBook.Close(false, false, misValue);
}
產生的chart圖會利用copy放進clipboard內。

使用畫面如下,紅色框部份就是被選取的cells,當在picturebox上按下ctrl+v時,
就會利用office元件繪製chart圖:

最後就是整理一些條件判斷式,來處理當遇到什樣格式時要執行那一個method比較恰當:
// 一般的圖檔
if (this.formatList.Contains("Bitmap") && !this.formatList.Contains("Text"))
{
    CatchBitmap();
}
// excel的cells欄位集合
else if (this.formatList.Contains("Bitmap") && this.formatList.Contains("Text"))
{
    // if want to create chart
    Convert();
                
    // offic 2003
    CatchBitmap();
                   
    // offic 2007
    CatchEMF();
}
// 純粹選取文字
else if (!this.formatList.Contains("Bitmap") && this.formatList.Contains("Text"))
{
    DrawString();
}
// excel2003的chart,只有emf格式,最後再處理
else if (this.formatList.Contains("EnhancedMetafile"))
{
    CatchEMF();
}

2010年9月3日 星期五

[C#]-利用Microsoft.Office.Interop.Outlook讀取outlook msg檔

若要開發 Microsoft Office Outlook 的增益集,您可以與 Outlook 物件模型提供的物件進行互動。此 Outlook 物件模型會提供表示使用者介面中各種項目的類別。例如,Microsoft.Office.Interop.Outlook.Application 類別是表示整個應用程式、Microsoft.Office.Interop.Outlook.MAPIFolder 類別是表示含有電子郵件訊息或其他項目的資料夾,而 Microsoft.Office.Interop.Outlook.MailItem 類別則是表示電子郵件訊息。

Outlook 會提供許多您可以與之互動的類別。下列各節將簡述某些最上層類別,並描述它們彼此的互動關係。這些類別包括:

  • Microsoft.Office.Interop.Outlook.Application
  • Microsoft.Office.Interop.Outlook.Explorer
  • Microsoft.Office.Interop.Outlook.Inspector
  • Microsoft.Office.Interop.Outlook.MAPIFolder
  • Microsoft.Office.Interop.Outlook.MailItem
  • Microsoft.Office.Interop.Outlook.AppointmentItem
  • Microsoft.Office.Interop.Outlook.TaskItem
  • Microsoft.Office.Interop.Outlook.ContactItem

下面是範例程式:
首先在專案參考內先匯入Microsoft.Office.Interop.Outlook
接著是取出msg檔內各欄位的方式
Microsoft.Office.Interop.Outlook.Application app = null;
Microsoft.Office.Interop.Outlook.MailItem item = null;

DialogResult msgFileSelectResult = this.openFileDialog.ShowDialog();
if (msgFileSelectResult == DialogResult.OK)
{
     string msgfile = this.openFileDialog.FileName
     app = new Microsoft.Office.Interop.Outlook.Application();
     item = app.Session.OpenSharedItem(msgfile) as Microsoft.Office.Interop.Outlook.MailItem;
     this.label1.Text = item.Subject;
     this.label2.Text = item.SenderName;
     this.label3.Text = item.To;
     this.label4.Text = item.Body;           
}

2010年9月2日 星期四

[PHP]-使用PDO操作sqlite

PHP5以後除了使用mysql_connect這種舊的方法連接資料庫之外,
加入了PDO的新功能可以用來連接資料庫,好處在於在效能上改善很多,
而且未來在將系統移植道不同的資料庫時也比較方便。

因為在PHP 5以前的php4/php3都是一堆的數據庫擴展來跟各個數據庫的連接和處理,
什麼 php_mysql.dll、php_pgsql.dll、php_mssql.dll、php_sqlite.dll等等擴展
來連接MySQL、 PostgreSQL、MS SQL Server、SQLite,同樣的,
我們必須借助 ADOdb、PEAR::DB、PHPlib::DB之類的數據庫抽象類來幫助我們,
無比煩瑣和低效。

以下是各種不同資料庫中的dsn(數據源)設定方法:

MySQL:
mysql:host={IP HOST}; dbname={DB NAME}

MSSQL︰
mssql:host={IP HOST};dbname={DB NAME}

Orcale:
oci:host={IP HOST};dbname={DB NAME}

SQLite:
sqlite:{DB PATH + DB NAME}

若是要設定預設的編碼,例如要設為使用UTF8,可在dsn中設定charset=UTF8即可。

簡介SQLite
1. ACID事務
2. 零配置 – 無需安裝和管理配置
3. 儲存在單一磁盤文件中的一個完整的數據庫
4. 數據庫文件可以在不同字節順序的機器間自由的共享
5. 支持數據庫大小至2TB
6. 足夠小, 大致3萬行C代碼, 250K
7. 比一些流行的數據庫在大部分普通數據庫操作要快
8. 簡單, 輕鬆的API
9. 包含TCL綁定, 同時通過Wrapper支持其他語言的綁定
10. 良好註釋的源代碼, 並且有著90%以上的測試覆蓋率
11. 獨立: 沒有額外依賴
12. Source完全的Open, 你可以用於任何用途, 包括出售它
要注意的是,如果在linux上使用sqlite,資料檔和資料檔所在目錄的權限一定要開,
這裡特別要強調的是,本身所在目錄的權限也要。

雖然SQLite允許忽略數據類型,下面仍列出常見的數據類型:
CREATE TABLE ex(
a VARCHAR(10),
b NVARCHAR(15),
c TEXT,
d INTEGER,
e FLOAT,
f BOOLEAN,
g CLOB,
h BLOB,
i TIMESTAMP,
j NUMERIC(10,5),
k VARYING CHARACTER (24),
l NATIONAL VARYING CHARACTER(16)
);

下面列舉一些利用PDO操作SQLite的範例:
create table
$dbh = new PDO('sqlite:example.db');
if (!$dbh) die ($error);

$stm = "CREATE TABLE User(id INTEGER PRIMARY KEY, name, birthday1, birthday2, ".
            "birthday0, gender, marry, education, job, tel1, tel2, ext, pw, ".
            "city, district, address, email)";
$dbh->exec($stm);
可以在欄位後面直接加INTEGER PRIMARY KEY指定為primary key,這個欄位值
同時也具有auto increment特性。
也可以利用下列語法指定索引鍵:
CREATE INDEX id_index on table_name(id)

select data from table
$dbh = new PDO('sqlite:example.db');
$sth = $dbh->prepare("SELECT * FROM User");
$sth->execute();
$result_set = $sth->fetchAll();
result_set的格式會如下:
Array
([0] => Array(
[id] => 1
[0] => 1
[name] => heiyeluren
[1] => heiyeluren
[gender] => 男
[2] => 男
[time] => 2006-10-28 23:14:23
[3] => 2006-10-28 23:14:23
)

prepare statement 型式
$dbh = new PDO('sqlite:example.db');
$sth = $dbh->prepare("SELECT * FROM User WHERE email = ?");
$sth->execute(array($_SESSION['user']));
$result = $sth->fetch();

攔截錯誤訊息
$dbh = new PDO('sqlite:example.db');
$rs = $dbh->query("SELECT * FROM User");
if ($dbh->errorCode() != 00000){
 echo $dbh->errorInfo();
 exit;
}
PDO和PDOStatement對象有errorCode() 和 errorInfo() 方法,
如果沒有任何錯誤, errorCode() 返回的是: 00000 ,
否則就會返回一些錯誤代碼。errorInfo() 返回的一個數組,
包括PHP定義的錯誤代碼和MySQL的錯誤代碼和錯誤信息,數組結構如下:
Array
(
[0] => 42S22
[1] => 1054
[2] => Unknown column aaa in field list
)

PDO的一些method參數說明
$dbh = new PDO('sqlite:example.db');
$dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER);
$rs = $dbh->query("SELECT * FROM User");
$rs->setFetchMode(PDO::FETCH_ASSOC);
$result_set = $rs->fetchAll();
setAttribute() 方法是設置部分屬性
PDO::CASE_LOWER -- 強制列名是小寫
PDO::CASE_NATURAL -- 列名按照原始的方式
PDO::CASE_UPPER -- 強制列名為大寫

setFetchMode方法來設置獲取結果集的返回值的類型
PDO::FETCH_ASSOC -- 關聯數組形式
PDO::FETCH_NUM -- 數字索引數組形式
PDO::FETCH_BOTH -- 兩者數組形式都有,這是缺省的
PDO::FETCH_OBJ -- 按照對象的形式,類似於以前的 mysql_fetch_object()

2010年8月17日 星期二

[Struts2]-Hibernate Integration

在談論Hibernate前我們要先了解JPA(Java Persistence API)

Java的物件導向模型與關聯資料庫模型之間有相當程度的不匹配,而一些物件與資料庫資料同步、更新,也就是所謂的Java永續儲存(Persistence)問題,在過去,Object/Relational Mapping(ORM)有 JBoss Hibernate、Oracle TopLink等解決方案,而JPA為吸收這些方案的經驗,所製訂出的Java永續儲存標準。

使用JPA,底層可以使用不同廠商的ORM實作,而介面則是JPA的標準,若您使用NetBeans+Glassfish,則預設的底層實作為TopLink,JBoss的工具其底層實作則為Hibernate。

要在Struts2使用Hibernate Plugin,必須有下列libs:
antlr-2.7.6.jar
commons-collections-3.1.jar
commons-fileupload-1.2.1.jar
commons-io-1.3.2.jar
commons-lang-2.3.jar
commons-logging-1.1.jar
dom4j-1.6.1.jar
ejb3-persistence.jar
freemarker-2.3.13.jar
hibernate3.jar
hibernate-annotations.jar
hibernate-commons-annotations.jar
hibernate-validator.jar
hsqldb.jar
javassist-3.9.0.GA.jar
jta-1.1.jar
junit-3.8.1.jar
log4j-1.2.15.jar
ognl-2.6.11.jar
slf4j-api-1.5.8.jar
slf4j-log4j12-1.5.8.jar
struts2-convention-plugin-2.1.6.jar
struts2-core-2.1.6.jar
struts2-fullhibernatecore-plugin-1.4-GA.jar
xwork-2.1.2.jar
另外在struts2.xml的package中,要繼承hibernate-default如下:
<struts>
<package name="user" extends="hibernate-default">
<action name="saveOrUpdateUser" method="saveOrUpdate" class="web.UserAction">
<result name="success" type="redirect">listUser</result>
</action>
<action name="listUser" method="list" class="web.UserAction">
<result name="success">/register.jsp</result>
</action>
<action name="editUser" method="edit" class="web.UserAction">
<result name="success">/register.jsp</result>
</action>
<action name="deleteUser" method="delete" class="web.UserAction">
<result name="success" type="redirect">listUser</result>
</action>
</package>
</struts>
上面的struts.xml也透露出了struts2的另一個應用,DispatchAction functionality
action可以指定到class內的method,而不一定是預設的execute method。

接下來我們設定hibernate.cfg.xml來做O/R Mapping,
hibernate.cfg.xml主要是設定資料庫連線資訊,如url、driver、帳密等,
並註明這個連線資訊mapping到那些object。
<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
        <property name="hibernate.connection.username">root</property>
        <property name="connection.password">syscom</property>
        <property name="connection.pool_size">1</property>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
        <property name="show_sql">true</property>
        <property name="hbm2ddl.auto">create</property>
        <mapping class="domain.User"/>
    </session-factory>
</hibernate-configuration>
這裡的mapping是直接mapping到一個User類別。

直接mapping到物件的話要使用annotation。使用Hibernate Annotations,
不需要使用HBM映射檔案,而是直接在POJO上使用Annotation設定對映關係,
如下的User.java:
package domain;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity //說明這是一個映射物件
@Table(name = "USER") //mapping到那一個table
public class User implements Serializable {

  private Long id;
  private String name;
  private String gender;
  private String country;
  private String aboutYou;
  private Boolean mailingList;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "USER_ID") //非必要,在欄位名稱與屬性名稱不同時使用
  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  @Column(name = "USER_NAME")
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  @Column(name = "USER_GENDER")
  public String getGender() {
    return gender;
  }

  public void setGender(String gender) {
    this.gender = gender;
  }

  @Column(name = "USER_COUNTRY")
  public String getCountry() {
    return country;
  }

  public void setCountry(String country) {
    this.country = country;
  }

  @Column(name = "USER_ABOUT_YOU")
  public String getAboutYou() {
    return aboutYou;
  }

  public void setAboutYou(String aboutYou) {
    this.aboutYou = aboutYou;
  }

  @Column(name = "USER_MAILING_LIST")
  public Boolean getMailingList() {
    return mailingList;
  }

  public void setMailingList(Boolean mailingList) {
    this.mailingList = mailingList;
  }
}
所有的Annotation都是在javax.persistence這個package之下,這是標準的 JPA Annotations,
Hibernate實作了這些Annotations,其中id是個特殊的屬性,Hibernate會使用它來作為主鍵識別,你可以定義主鍵產生的方式,這邊設定為自動產生主鍵,依賴於資料庫自動增生主鍵的機制,也就是相當於在XML中設定native。

使用Hibernate另一個酷炫的好處是我們不用事先建立資料表,只要hibernate.cfg.xml設定檔
內的hbm2ddl.autocreate參數值設為create,基於JPA的概念,
Hibernate底層會幫我們完成這一切。自動建立的資料表如下:
這裡要特別注意的是,如果hbm2ddl.autocreate參數值設為create,則每次系統重啟時資料表
會被清空,如果要保留原本資料表內的資料,參數值則必須設為update。

接著我們回頭來看前面的struts.xml,
可以看出這是一個新增、刪除、修改使用者資料的範例,
接下來我們將這個範例檔補齊。
首先編寫前端的render頁面register.jsp:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@taglib uri="/struts-tags" prefix="s"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Registration Page</title>
<s:head />
<style type="text/css">
@import url(style.css);
</style>
</head>
<body>
<s:form action="saveOrUpdateUser">
<s:push value="user">
<s:hidden name="id" />
<s:textfield name="name" label="User Name" />
<s:radio name="gender" label="Gender" list="{'Male','Female'}" />
<s:select name="country" list="{'India','USA','UK'}" headerKey=""
headerValue="Select" label="Select a country" />
<s:textarea name="aboutYou" label="About You" />
<s:checkbox name="mailingList"
label="Would you like to join our mailing list?" />
<s:submit />
</s:push>
</s:form>

<s:if test="userList.size() > 0">
<div class="content">
<table class="userTable" cellpadding="5px">
<tr class="even">
<th>Name</th>
<th>Gender</th>
<th>Country</th>
<th>About You</th>
<th>Mailing List</th>
<th>Edit</th>
<th>Delete</th>
</tr>
<s:iterator value="userList" status="userStatus">
<tr
class="<s:if test="#userStatus.odd == true ">odd</s:if><s:else>even</s:else>">
<td><s:property value="name" /></td>
<td><s:property value="gender" /></td>
<td><s:property value="country" /></td>
<td><s:property value="aboutYou" /></td>
<td><s:property value="mailingList" /></td>
<td><s:url id="editURL" action="editUser">
<s:param name="id" value="%{id}"></s:param>
</s:url> <s:a href="%{editURL}">Edit</s:a></td>
<td><s:url id="deleteURL" action="deleteUser">
<s:param name="id" value="%{id}"></s:param>
</s:url> <s:a href="%{deleteURL}">Delete</s:a></td>
</tr>
</s:iterator>
</table>
</div>
</s:if>
</body>
</html>
接下來是後端的action物件,UserAction.java:
package web;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;
import dao.UserDAO;
import dao.UserDAOImpl;
import domain.User;

public class UserAction extends ActionSupport implements ModelDriven {

  private User user = new User();
  private List userList = new ArrayList();
  private UserDAO userDAO = new UserDAOImpl();

  @Override
  public User getModel() {
    return user;
  }

  public String saveOrUpdate() {
    userDAO.saveOrUpdateUser(user);
    return SUCCESS;
  }

  public String list() {
    userList = userDAO.listUser();
    return SUCCESS;
  }

  public String delete() {
    HttpServletRequest request =
      (HttpServletRequest) ActionContext.getContext().get(ServletActionContext.HTTP_REQUEST);
    userDAO.deleteUser(Long.parseLong(request.getParameter("id")));
    return SUCCESS;
  }

  public String edit() {
    HttpServletRequest request =
      (HttpServletRequest) ActionContext.getContext().get(ServletActionContext.HTTP_REQUEST);
    user = userDAO.listUserById(Long.parseLong(request.getParameter("id")));
    return SUCCESS;
  }

  public User getUser() {
    return user;
  }

  public void setUser(User user) {
    this.user = user;
  }

  public List getUserList() {
    return userList;
  }

  public void setUserList(List userList) {
    this.userList = userList;
  }
}
在action的method內是用一個userDao類別來封裝物件與db之間的transaction。
同時在這個action我們可以看到struts2的另一個特殊用法,ModelDriven Action
繼承ModelDriven interface後,action invoke過程就會涉入modelDriven interceptor
此時framework就可以自動傳送前端表單資料到所覆寫的getModel() method所回傳的物件。
UserDao.java
package dao;

import java.util.List;
import domain.User;

public interface UserDAO {
  public void saveOrUpdateUser(User user);
  public List listUser();
  public User listUserById(Long userId);
  public void deleteUser(Long userId);
}
UserDaoImpl.java
package dao;

import java.util.List;

import org.hibernate.Session;
import org.hibernate.Transaction;

import com.googlecode.s2hibernate.struts2.plugin.annotations.SessionTarget;
import com.googlecode.s2hibernate.struts2.plugin.annotations.TransactionTarget;
import domain.User;

public class UserDAOImpl implements UserDAO {

  @SessionTarget
  Session session;
  @TransactionTarget
  Transaction transaction;

  @Override
  public void saveOrUpdateUser(User user) {
    try {
      session.saveOrUpdate(user);
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    }
  }

  @Override
  public void deleteUser(Long userId) {
    try {
      User user = (User) session.get(User.class, userId);
      session.delete(user);
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public List listUser() {
    List courses = null;
    try {
      courses = session.createQuery("from User").list();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return courses;
  }

  @Override
  public User listUserById(Long userId) {
    User user = null;
    try {
      user = (User) session.get(User.class, userId);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return user;
  }
}

UserDAOImpl是本篇另一個重點,它說明如何利用annotation來取得物件與db之間的
操控權session、transaction。
利用@SessionTarget、@TransactionTarget這兩個annotation可以從hibernate.cfg.xml設定檔中,取得相對應的session、transaction控制物件。

除了利用annotation來取得session外,我們也能用傳統的方式,編寫一個session factory,
這裡要注意的是,POJO類別(User.java)內如果是使用annotation來設定對映關係的話,
就必須要利用AnnotationConfiguration類別來取得SessionFactory。

下面我們編寫一個HibernateUtil class來專門取得SessionFactory。
package dao;

import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.SessionFactory;

public class HibernateUtil {
  private static final SessionFactory sessionFactory;

  static {
    try {
      sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
    } catch (Throwable ex) {
      System.err.println("Initial SessionFactory creation failed." + ex);
      throw new ExceptionInInitializerError(ex);
    }
  }

  public static SessionFactory getSessionFactory() {
    return sessionFactory;
  }
}
接著我們就能從SessionFactory內取得session及transaction操控物件。

下面就是整個範例的操作畫面:

2010年8月16日 星期一

[Struts2]-OGNL存取ActionContext內非Value Stack內之物件

參考前一篇[Struts2]-實作Interceptor
result頁面可以利用OGNL語法從ActionContext的ValueStack中取得欄位物件
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<html>
<head>
<title>Hello World</title>
</head>
<body>
<h2><s:property value="message"/></h2>
</body>
</html>
此時property tag中的value屬性就是利用OGNL語法從hello action中取得message field。
因為當action處理完後欄位資料後會將欄位物件置於ActionContext的ValueStack中,
此時的前端頁面就是利用OGNL從ValueStack中取出相對應的欄位值。

在ActionContext中,除了ValueStack內的物件外還有其它的scope物件可以存取,
如下圖:


 名称作用例子
parameters包含當前HTTP請求參數的Map#parameters.id[0]作用相當於request.getParameter("id")
request包含當前HttpServletRequest的屬性(attribute)的Map#request.userName相當於request.getAttribute("object name")
session包含當前HttpSession的屬性(attribute)的Map#session.userName相當於session.getAttribute("object name")
application包含當前應用的ServletContext的屬性(attribute)的Map#application.userName相當於application.getAttribute("object name")
attr用於按request > session > application順序訪問其屬性(attribute)#attr.userName相當於按顺序在以上三個範圍(scope)内讀取object name屬性,直到找到為止

接下來我們實際編寫一個action(OgnlAction.java)來存取這些scope物件。
package example;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsStatics;

public class OgnlAction extends ActionSupport {

  private ActionContext actionContext;
  private Map application;
  private Map session;
  private Map parameters;
  private HttpServletRequest request;
  private HttpServletResponse response;

  @Override
  public String execute() {
    actionContext = ActionContext.getContext();
    application = actionContext.getApplication();
    session = actionContext.getSession();
    parameters = actionContext.getParameters();

    request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
    //private HttpServletRequest request = ServletActionContext.getRequest();
    response = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE);

    application.put("message", "message from application");
    session.put("message", "message from session");
    parameters.put("message", "message from parameters");
    request.setAttribute("message", "message from request");
    return SUCCESS;
  }
}

我們可以利用ActionContext類別來取得這些scope物件,
另外也能使用ServletActionContext這個輔助類別來取得
HttpServletRequest、HttpServletResponse 。
這裡要注意的是,ActionContext物件必須要在execute method內取得,
如果在method外初始的話,此時內部的application等scope物件都還是null。

接下來編寫一個Ognl.jsp來當做前端頁面。
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts OGNL Demo</title>
</head>
<body>
<h3>訪問ActionContext物件</h3>
<p>parameters: <s:property value="#parameters.message" /></p>
<p>request.message: <s:property value="#request.message" /></p>
<p>session.message: <s:property value="#session.message" /></p>
<p>application.message: <s:property value="#application.message" /></p>
<p>attr.message: <s:property value="#attr.message" /></p>
<hr/>
</body>
</html>
這裡可以發現,OGNL預設就是取得ValueStack內的物件,如果要取得其它的scope物件,
前面則要加上#符號。

最後是struts.xml的設定
<struts>
<package name="example" namespace="/example" extends="struts-default">
<action name="Ognl" class="example.OgnlAction">
<result name="success">/example/Ognl.jsp</result>
</action>
</package>
</struts>
輸入連結後/example/Ognl.actiont可以看到下面的結果:
除了parameters外其餘的結果都如我們預料,
筆者猜測也許parameters是取得當前http request的參值,
無法事後更動。

當我們直接在請求頁面的連結後加入參數時,就正常了。

2010年8月15日 星期日

[Struts2]-實作Interceptor

首先先來了解struts2 framework的運作機制
要部署struts2 framework我們必須在web.xml中加入這段
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
當 FilterDispatcher 根據 action name 所對應到 struts.xml 中的 Action class 之後,
Struts2 核心會先呼叫 Interceptor Stack 中的所有 interceptors。
接著再呼叫 Action class 中的 execute() method,完成後再呼叫 Result 幫助我們排版,
這過程中都會使用到 OGNL 到 ValueStack 中幫助我們取得必要的資料。

Interceptor Stack
這裡我們先將重點擺在Interceptor。Interceptor Stack 是在整個流程中第一個被呼叫的,
也是最後一個被呼叫的。Interceptor Stack 中包含一集合的 interceptors,
而 interceptors 的排列方式是以 stack 的方式排列。

下面我們先寫一個簡單無牽扯自訂interceptor的範例。
首先我們先新增一個Login.jsp頁面。
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>login</title>
</head>
<body>
<s:form action="/example/Hello" method="post">
<s:textfield name="user_name" label="User Name:"/>
<s:submit/>
</s:form>
</body>
</html>
這個頁面會呼叫hello action,所以我們接下來編寫一個HelloWorld.java來處理這個呼叫。
package example;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import javax.servlet.http.HttpServletRequest;
import org.apache.struts2.StrutsStatics;

public class HelloWorld extends ActionSupport {
  private String user_name;
  private String message;

  @Override
  public String execute() throws Exception {
    message = "Hello:" + user_name;
    return SUCCESS;
  }

  public String getUser_name() {
    return user_name;
  }

  public void setUser_name(String user_name) {
    this.user_name = user_name;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }
}
hello action被呼叫時會自動執行excute這個method,當處理完後會藉由return "String"告訴
framework要將結果導向那個result頁面。

接下來我們再編寫一個HellowWorld.jsp來作為輸出頁面。
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<html>
<head>
<title>Hello World</title>
</head>
<body>
<h2><s:property value="message"/></h2>
</body>
</html>

至於如何將Login.jsp與HelloWorld.java做連結以及利用result標韱來控制輸出頁面呢?
則是在struts.xml中設定。
<struts>
<package name="example" namespace="/example" extends="struts-default">
<action name="Hello" class="example.HelloWorld">
<result name="success">/example/HelloWorld.jsp</result>
<result name="login">/example/LOGIN.jsp</result>
</action>
</package>
</struts>
上述例子表示,
當action return "success"時會導向HelloWorld.jsp,
當return "login"時會導向Login.jsp。
這裡有兩個需要注意的地方:
1.在jsp頁面中呼叫action時,必須加入其package的namespace為prefix,
    如action="/example/Hello";
2.result的name屬性值是有區分大小寫的。


下面為這個範例的執行結果:
輸入名稱後
會出現歡迎頁面

接下來進入這篇的重點,我們要在這個流程中加入自訂的interceptor,
編寫一個AuthorizationInterceptor.java,
自訂的interceptor須繼承AbstractInterceptor介面。
package example;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

public class AuthorizationInterceptor extends AbstractInterceptor {
  @Override
  public String intercept(ActionInvocation ai) throws Exception {
    return ai.invoke();
  }
}
當interceptor發生作用時會呼叫intercept這個method。
而利用ActionInvocation的getInvocationContext()method,
我們可以得到儲存全文流程資料的ActionContext物件,這時就可以自由發揮了。

回頭看第一張framework流程圖,interceptor可以invoke原本request呼叫的action,
也可以直接return result頁面,
在這個例子中我們在interceptor中不做任何事,直接invoke action。

要讓自訂的interceptor發揮作用還需要在struts.xml中加以指明。
<struts>
<package name="example" namespace="/example" extends="struts-default">
<interceptors>
<interceptor name ="auth" class ="example.AuthorizationInterceptor"/>
</interceptors>
<action name="Hello" class="example.HelloWorld">
<interceptor-ref name ="auth"/>
<result name="success">/example/HelloWorld.jsp</result>
<result name="login">/example/LOGIN.jsp</result>
</action>
</package>
</struts>
表示呼叫hello action時必須先經過auth這個interceptor。

我們再一次的執行這個範例,確發現了意外的結果。
Login.jsp所輸入的使用者名稱無法帶到所呼叫的hello action,導致HelloWorld.jsp
頁面輸出時user_name欄位值為null。
這個原因是因為當我們自訂的interceptor呼叫invoke method時,整個flow會回到
framework的開頭階段,此時hello action並非由Login.jsp直接呼叫,
導致Login.jsp中的textfield欄位自動調用HellowWorld.java中的setUser_name()的機制失效。

此時hello action(HelloWorld.java)就必須自行由request中取出Login.jsp傳來的parameter。
將HellowWorld.java的execute method內容改寫如下:
public String execute() throws Exception {
ActionContext actionContext = ActionContext.getContext();
HttpServletRequest request= 
(HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
message = "Hello:" + request.getParameter("user_name");
return SUCCESS;
}
接著再一次執行修改過後的範例,就又可以看到正確的輸出結果了。

2010年8月5日 星期四

[AJAX]-動態清單

這裡要記錄如何用Ajax來達到動態清單的功能
Ajax 是 Asynchronous JavaScript  and XML 的簡稱,這指出了 Ajax 的核心觀念
(Asynchronous)與所使用到的主要兩個技術(JavaScript、XML)。
要達到 Asynchronous 利用的就是 JavaScript 中的XMLHttpRequest 物件。

XMLHttpRequest:
在 JavaScript 中利用new XMLHttpRequest()語法建立。
包含下列幾個標準屬性-

onreadystatechange 
參考至callback函式,readyState每次改變時,都會呼叫onreadystatechange所參考的函式。

readyState 
會有0到4的數值,分別表示不同的請求狀態:
 0 = 未初始化的連線(uninitialized),還沒呼叫open()
 1 = 載入中(loading),呼叫open(),還沒呼叫send()
 2 = 已載入(loaded),呼叫send(),請求header/status準備好
 3 = 互動中(interactive),正在與伺服器互動中
 4 = 請求完成(completed),完成請求

responseText 
伺服器傳來的請求回應文字,會設定給這個屬性。

responseXML
伺服器傳來的請求回應如果是XML,會成為DOM設定給這個屬性。

status 
伺服器回應的狀態碼,例如200是OK,404為Not Found…

statusText 
伺服器回應的狀態文字。
接下來我們實際寫個web頁面來發送XMLHttpRequest
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>動態載入清單</title>
<script type="text/javascript" src=dynamicList.js></script>
</head>
<body>
語言…
<br/>
<select id="langs" onchange="refreshList();">
<option value="func">功能說明</option>
<option value="java">Java</option>
<option value="c#">C#</option>
</select>
<br/><br/>
相關技術…
<br/>
<select id="techs" size="6" style="width:300px;"/>
</body>
</html>
主要功能是選擇 id 為 langs 的 select html 元件時,會利用 Ajax 去後端 server 取得
相關資料,非同步的更動 id 為 techs 的 select html元件。
另外主要功能的JavaScript是利用
<script type="text/javascript" src=dynamicList.js></script>來引入。
var xmlHttp;

window.onload = refreshList;

function createXMLHttpRequest() {
  if(window.XMLHttpRequest) {
    xmlHttp = new XMLHttpRequest();
  }
  else if(window.ActiveXObject) {
    xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
}

function prepareXML() {
  var xml = "<langs>";
  var lang = document.getElementById("langs").value;
  xml = xml + "<lang>" + lang + "<\/lang>";
  xml = xml + "<\/langs>";
  return xml;
}

function refreshList() {
  var xml = prepareXML();
  var url = "refreshservlet";

  createXMLHttpRequest();
  xmlHttp.onreadystatechange = handleStateChange;
  xmlHttp.open("POST", url);
  xmlHttp.setRequestHeader("Content-Type", "text/xml");
  xmlHttp.send(xml);  
}

function handleStateChange() {
  if(xmlHttp.readyState == 4) {
    if(xmlHttp.status == 200) {
      clearList();
      updateList();
    }
  }
}

// 清除上一次的顯示結果
function clearList() { 
  var techs = document.getElementById("techs");
  while(techs.childNodes.length > 0) {
    techs.removeChild(techs.childNodes[0]);
  }
}

// 以回應更新資料
function updateList() {
  var results = xmlHttp.responseXML.getElementsByTagName("tech");
  var techs = document.getElementById("techs");

  var option = null;
  for(var i = 0; i < results.length; i++) {
    option = document.createElement("option");
    option.appendChild(document.createTextNode(results[i].firstChild.nodeValue));
    techs.appendChild(option);
  }
}
由上面我們可以看到將XMLHttpRequest的onreadystatechange屬性
註冊到handleStateChange method,並利用post機制傳送XML格式的資料。
而所謂的XML格式資料其實就是符合XML規格的字串資料。
因為非同步的資料皆由XMLHttpRequest所控制,所以server回傳資料
也是由 XMLHttpRequest 的 responseXML 屬性取得。

接下來我們用一個servlet來接收並回傳由client端發送來的資料。
public class RefreshServlet extends HttpServlet {
  private static Map inMemoryDB = new HashMap();

  public void init() throws ServletException {
    inMemoryDB.put("func", new String[] { "非同步", "動態清單" });
    inMemoryDB.put("java", 
    new String[] { "JSP", "J2EE", "Spring", "Hibernate" });
    inMemoryDB.put("c#", new String[] { "ASP.Net", "ADO.Net" });
  }

  protected void doPost(HttpServletRequest request, 
    HttpServletResponse response) throws ServletException, IOException {
    String xml = readXMLFromRequestBody(request);
    Document xmlDoc = null;

    try {
      DocumentBuilder builder = 
      DocumentBuilderFactory.newInstance().newDocumentBuilder();
      xmlDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
    } catch (ParserConfigurationException e) {
      System.out.println(e);
    } catch (SAXException e) {
      System.out.println(e);
    }

    String responseXML = prepareXMLResponse(xmlDoc);

    response.setContentType("text/xml;charset=utf-8");
    response.getWriter().print(responseXML);
    response.getWriter().close();
  }

  private String readXMLFromRequestBody(HttpServletRequest request) {
    StringBuffer xml = new StringBuffer();

    try {
      BufferedReader reader = request.getReader();
      String line = null;
      while ((line = reader.readLine()) != null) {
        xml.append(line);
      }
    } catch (Exception e) {
      System.out.println("XML讀取有誤…" + e.toString());
    }
    return xml.toString();
  }

  private String prepareXMLResponse(Document xmlDoc) {
    NodeList langNodes = xmlDoc.getElementsByTagName("langs");
    String lang = langNodes.item(0).getFirstChild().getTextContent();

    StringBuffer xml = new StringBuffer();
    xml.append("<techs>");
    String[] techs = (String[])inMemoryDB.get(lang);
    for (int i = 0; i < techs.length; i++) {
      xml.append("<tech>");
      xml.append(techs[i]);
      xml.append("</tech>");
    }
    xml.append("</techs>");
    return xml.toString();
  }
}

因為XML也是字串資料,所以我們先用reader讀入
BufferedReader reader = request.getReader();
String line = null; 
while ((line = reader.readLine()) != null) {
     xml.append(line);
 }
接著利用 DocumentBuilder 來解析每個tag的資料
DocumentBuilder builder =
     DocumentBuilderFactory.newInstance().newDocumentBuilder(); 

xmlDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));

最後也是利用 StringBuffer 將查詢到的資料組成XML格式的字串資料回傳。