2012年10月13日 星期六

如何在Grails 上實現多對多的關係(四) : 怎麼實作

寫了這麼多廢話,該說如何實作了。我們先從使用者開始。


class LoginUsers{
   static hasMany = [roleLink : RoleUsers]
   String userid
   static constraints = {
      userid(unique:true)  
   }

 List roles(){
    return roleLink.collect{it.role}
 }
 List addRole(Roles r){
    RoleUsers.link( this , r )
    return roles()
 }
 List addRoles(List roleids){
    for(rid in roleids){
       RoleUsers.link( this , Roles.get(rid))
    }
    return roles()
 }
 List removeRole(Roles r){
    RoleUsers.unlink(this,r)
    return roles()
 }
 void removeAllRoles(){
    RoleUsers.unlinkAllRolesFromUser(this)
 }
}
稍微解釋一下。還記得測試的程式嗎, aska.roles() 就可以得到角色,背後怎麼做呢?使用者的類別中我們這樣定義過…

   static hasMany = [roleLink : RoleUsers]

不用宣告一個 Set roleLink 在上面,GORM 自動幫我們加入了。所以呼叫 aska.roleLink() 就會馬上得到屬於aska的List<RoleUsers>

但我說過 RoleUsers 不算是一個真正的「物件」,我們並不關心它( 雖然它要做很多事),我們要的是群組。所以這一部份,我們要再加上collect後,封裝在 roles() 的方法裡丟出Roles。 

如果你還不知道collect的話,請看前面的介紹,這裡很簡潔的把 RoleUsers裡屬於 aska 的群組蒐集起來丟出去。

class LoginUsers {
    static hasMany = [roleLink : RoleUsers]
}
class RoleUsers {
    LoginUsers user
    Roles role
    static constraints = {  
    }
    static belongsTo = [user:LoginUsers , role:Roles]
}

這樣夠清楚吧。有這樣的宣告後,aska只要呼叫 roleLink.collect{it.role} 一行就完事了,非常給力。

可想而知 Roles的部份也是 userLink.collect{it.user} 來得到一個群組下的使用者們。這邊我就不再解釋一次了。

aska.addRole ,因為我們要新增一個 RoleUsers的物件,所以責任上我們就給RoleUsers去做。先定義方法叫做link。加多數的群組,就是呼叫link 多次。removeRole就先定義叫 unlink。 Roles的類別,可想而知也定義了類似的方法,不再解釋

class Roles {
        String name
        static constraints = {
             name(unique:true)
        }
        static hasMany = [ userLink:RoleUsers  ]
 List users(){
    return userLink.collect{it.user}
 }
 
 List addUser(LoginUsers u){
    RoleUsers.link( u , this)
    return users()  
 }
 List addUsers(List uids){
    for(uid in uids){
       RoleUsers.link(LoginUsers.get(uid) , this)
    }
    return users() 
 } 
 List removeUser(LoginUsers u){
    RoleUsers.unlink( u , this)
    return users()
 }
 void removeAllUsers(){
    RoleUsers.unlinkAllUsersFromRole(this)
 }
}

重頭戲在 RoleUsers 裡。

class RoleUsers {
 LoginUsers user
 Roles role
 static constraints = {
  
 }
 static belongsTo = [user:LoginUsers , role:Roles]
 
 static RoleUsers link(LoginUsers u , Roles r){
   def lk = RoleUsers.findByUserAndRole(u,r)
   if(!lk){
   //if not in db then we start to link
     lk = new RoleUsers()
     u.addToRoleLink(lk)
     r.addToUserLink(lk)
     lk.save(flush:true)   
   }
   return lk
 }
 static void  unlink(LoginUsers u , Roles r ){
   def lk = RoleUsers.findByUserAndRole(u,r)
   if(lk){
     u.removeFromRoleLink(lk)
     r.removeFromUserLink(lk)
     lk.delete(flush:true)
  }
 }
 static void unlinkAllRolesFromUser(LoginUsers u){
  def lks = RoleUsers.findAllByUser(u);
  if(lks){
   for(lk in lks){
      lk.user.removeFromRoleLink(lk)
      lk.role.removeFromUserLink(lk)
      lk.delete(flush:true)
   }
  }
 }
 static void unlinkAllUsersFromRole(Roles r){
  def lks = RoleUsers.findAllByRole(r);
  if(lks){
    for(lk in lks){
      lk.role.removeFromUserLink(lk)
      lk.user.removeFromRoleLink(lk)
      lk.delete(flush:true)
   }
  }    
 }
}

我假設你已經會基本的GORM了,所以只說重點。

在做鏈結與非鏈結時,記得先「解除關係」。

link 這個方法中透過


  • LoginUsers 的 hasMany 裡定義的變數名稱為roleLink,
  • 並且RoleUsers屬於 LoginUsers
  • 於是Grails就會在LoginUsers中,自動幫你合成方法 addToRoleLink



addToRoleLink 如此就變成是內建的方法了,不用再實作了。

記得要從「一」方去加「多」方,因為有主權的一方擁有責任。
aska.addToRoleLink(link),把自己加到鏈結當中。Roles也要做一樣的事。最後再save 那個 link (也就是RoleUsers) 則大功告成。

相反的,非鏈結要做的步驟也是一樣。把二邊的關聯去除後,再把 link 刪除。若你忘了先把關聯解除,就會得到exception。

再來是一個使用者想要把所有的群組刪掉。首先得到屬於他的 List<RoleUsers>後,一樣是要先解除關聯。要從「一」方來呼叫removeFrom,所以還得要先連回「一」方再分別解除,這也就是 lk.role.remove... 與 lk.user.remove....的意義。

程式是否少到令你難以置信?這中間沒有任何的 xml 任何的 annotation。類別的關聯性也不用再做文件說明,非常的明白易懂。定義好 RoleUsers裡的方法實作後,再來就沒有什麼事可以做了。呃,對,你也不用再管DB裡的表格了,Grails會自動幫你處理。

快速的做一些ui 的假想

有一個使用者的列表頁,點進去某個使用者之後,會出現基本資料維護,下方是他可以使用的群組。

在controller 裡我們快速的呼叫這二個方法塞成params丟到gsp中馬上就可以使用。


[role_list : Roles.list() , checked_role_list : aska.roles() ]

在維護頁裡,按下確認後回到controller,我們會先刪除aska的所有群組,然後再加有勾選的部份。

aska.removeAllRoles()
aska.addRoles(params.checkedRoleId)
二行搞定… 若前端頁面變成奇怪的新式UI,使用者點一下某個群組就會動態的Ajax往後端呼叫做更新,再按一下就刪除這個群組,那也是 aska.addRole(Roles.get(rid)) 與 aska.removeRole(Roles.get(rid)) 而已… 花了蠻多時間寫的,希望對大家有幫助。

如何在Grails 上實現多對多的關係(三) : 三者的結構

首先你一定會先定義類別吧,我們一次要定義三個類別出來。

class LoginUsers {

}
class Roles{

}
class RoleUsers{
      //這個就是多對多的中間表,我們需要它來做很多事。
}
再來我們開始定義它們的關聯,也是很直觀。


// LoingUsers 1 <-> * RoleUsers * <-> 1 Roles

class LoginUsers {
    static hasMany = [ roleLink : RoleUsers ]
}
class Roles{
    static hasMany = [ userLink : RoleUsers ]
}
class RoleUsers{
    LoginUsers user
    Roles role
    static constraints = {
  
    }
    static belongsTo = [user:LoginUsers , role:Roles]
}
LoingUsers 1 <-> * RoleUsers * <-> 1 Roles 別說你看不懂喔,正規化是幼幼班的課程。
這邊比較特別的是…明明LoginUsers與Roles參照同一個表格,為何我取不同的名字,一個是roleLink一個是userLink ? 這是你使用上的自由…

從使用者的觀點來看,這是一個群組鏈結表。
從群組的觀點來看,這是一個使用者鏈結表。

所以我訂不同的名字。為了方便,你也可以都叫做 link:RoleUsers,這邊就隨你高興囉! 不過 Roles 不一定只和Users 做多對多,他也可以與功能做多對多,所以取名時還是多加幾個字比較好。

中間表裡的每一個record,每一個物件,都一定對到1個使用者與1個群組。

從使用者的觀點來說,他是一對多的,就像一個人可以寫很多本書,多方的書「屬於」作者。 所以這裡的多方 RoleUsers 是「屬於」使用者的。

從群組的觀點來說,也是一對多的,一個群組裡可以有多個使用者。

所以在這個中間鏈結表中,他同時「屬於」二者,中間表沒有什麼主權,所以他「屬於」別人。

用這樣的方式去理解應該就簡單了。

剛剛說,中間表裡的每一列都對應到一個使用者與一個群組,所以你當然要宣告他們才連的回去。 結構的部份搞清楚了,後面就不是很難理解了。內容請看下一章。

如何在 Grails 上透過 GORM 實現多對多的關係。(二) 我就是要這樣呼叫

二個類別,LoginUsers 和 Roles,還記得吧。

        LoginUsers aska = new LoginUsers(userid:'aska')
     LoginUsers gina = new LoginUsers(userid:'gina')
     Roles admin_role = new Roles(name:'admin')
     Roles normal_role = new Roles(name:'normal')
     aska.save(flush:true)
     gina.save(flush:true)
     admin_role.save(flush:true)
     normal_role.save(flush:true)
aska 是admin , gina 是一般使用者。簡單來說這個測試就是如此。 我們要做的測試是,從aska,gina的角度加入群組,以及從群組的角度加入使用者。 當然也要從aska與gina的角度來退出群組,以及從群組的角度來刪除使用者。 GORM 是「絕對直觀」派的東西。你先看我下面怎麼寫,就知道「絕對直觀」是指什麼。 我們先來測試從群組的觀點來加入使用者,並且倒過來從使用者的角度來得到群組。

        println 'add aska to admin_role' + admin_role.addUser(aska)
     println 'add gina to normal role '+ normal_role.addUser(gina)
     println 'add aska to normal role' + normal_role.addUser(aska)
     println 'aska should have two roles = '+aska.roles()
     println 'gina should have one role = ' + gina.roles()
     println 'admin user should be aska= ' + admin_role.users()
     println 'normal users should be aska and gina= '+normal_role.users()
我們實作的程式,就是這樣呼叫。也就是說,這樣寫一定是你最滿意的。得到一個群組的物件時,呼叫 addUser 就可以加入user,還有比這樣寫更簡單的方式嗎? 先說一下,這邊的程式一定都不會過,因為這些方法都還不存在。你可能有想到說,多對多不是有「中間」的表格嗎,怎麼都沒看到? 我們在實作的時候,會透過一個中間類別來做處理,這個中間類別對我來說,不應該出現在controller或是service 之中,它不是我們關心的物件。雖然等下他要做很多事! 再來我們測remove

        //test remove 
     normal_role.removeUser(gina)
     println 'remove gina from normal users, now normal role should be only aska = '+normal_role.users()
     normal_role.removeAllUsers()
     println 'remove all users from normal role, now normal role = '+normal_role.users()
     println 'aska should have no groups after unlink all roles' + aska.removeAllRoles()
好,當我們在刪除的時候,要是你是從使用者的角度出發,你只想呼叫removeRole(role) , removeAllRoles()這樣吧。我所謂的「絕對直觀」,就是程式員的本能,如果可以這樣寫就下班有多好,從這個角度來設計就對了。從群組的角度來說,就是 removeUser(user) , removeAllUsers()這樣,反之亦然,找不到更短的寫法了。 在得到使用者的群組亦是 aska.roles() , 得到管理員群組下的人也該是admins.users(),這就是直觀導向的寫法。 下面再從使用者的角度,加入一堆群組。這個在維護畫面中常常見到啊,管理者勾一勾這個人能加入的群組,將群組id送到controller等待寫入db,所以我們也是需要這樣的功能。

        aska.addRoles([1,2])
     println 'add aska to admin and normal group, now aska join : ' +aska.roles()
     println 'add gina to normal role' + gina.addRole(normal_role)
     println 'remove gian from both roles'+gina.removeAllRoles()
     println 'gina should have no roles now'+gina.roles()
     println 'add gina to admin and normal roles '+ gina.addRoles([1,2])
     println 'clear all user from role'
     normal_role.removeAllUsers();
     admin_role.removeAllUsers();
     println 'try to add people to roles.'
     println 'add aska and gina to normal role.'+normal_role.addUsers([1,2])
     println 'add aska to admin role.'+admin_role.addUsers([1])
     println 'test remove user from roles'
     aska.removeAllRoles()
     gina.removeAllRoles()
     println aska.roles()
     println gina.roles()

我不用解釋你也看的懂,這就是我們要的程式,再來下一章要講背後我們要如何實作,以得到這直觀寫法。

如何在 Grails 上透過 GORM 實現多對多的關係。(一) 測試先行

當我完成 GORM 多對多的測試時,真的覺得很酷。坦白說,我是SQL 派的人,物件導向的Query Language 背後其實沒啥效率,不管是新增或是刪除常常多做了幾筆SQL。在複雜的程式中,其實直接透過SQL 來達成任務,在效能上是比較能接受的。

不過反過來思考,多對多的維護功能其實很常見。如果一直在程式中SQL 來SQL 去的,「程式的可讀性」會大大的降低。Hibernate出現後,程式員透過物件的方式來操作資料庫,反倒成了顯學。這邊的爭論可以寫在另外一篇文章了。

這顯學蔚然成風,但我畢竟是散淡的人,多寫幾個字總覺得麻煩,但是又無處可去。

Grails 的做法非常的另人驚艷。
不講理論,先講結果,當多對多完成的時候,我可以怎樣去得到我要的東西?
首先我們的  domain class 為 LoginUsers 和 Roles。
這個很單純,就是一個使用者可以加入多個群組,而一個群組裡也可以包含多個使用者。
Grails 講的是結果。所以你想怎麼做,可以先寫在「整合測試」中。
寫在整合測試,在跑的時候,就有db連線的功能。

整合測試

我以前一直不太懂,先寫測試有什麼好處。在傳統 Java 開發中已經夠慢了,想到要寫測試就頭大。但後來終於理解「測試先行」的意思了。

那就是先決定「我就是要這樣呼叫」。
而後再把程式的肉慢慢做出來。

這樣的差別在於,你若是「呼叫」的人,一定是想寫最少的程式來得到你要的結果。而這樣的導向,會讓程式易於理解。若先寫類別與方法,寫完了有時候發現外部程式呼叫起來很不順,這也是因為沒有從別人的觀點來開始的原因。

不過我們不是要講整合測試

不囉嗦,開始寫我們想要的呼叫方式。請看下一篇。




2012年10月9日 星期二

在Grails 中如何寄 email

首先,先在Command mode輸入

 grails install-plugin mail
一般應該是用公司的mail server發送吧 ? 第二步你要設定email 的相關資訊。打開

/your_project/grails-app/conf/Config.groovy
加入 

grails{
 mail {
      host = "78.78.78.78" 
      port = 25     
    }
}
grails.mail.default.from="itdep@aska.com" 

再來要怎麼做呢… 在 傳統的 Java 來說,你要 import 一堆 Java Email 相關的的類別,中間的辛酸就不用再提了。在Grails 的話,在程式任何一處只要寫這樣就可以寄出了 !

sendMail {     
          to "aska@aska.com.tw"     
          subject "test , post created."     
          body "${params.content}" 
       }
透過動態語言的好處,每一個類別都「突然會」 sendMail 了!
也就是「動態的」他們都擁有了這個方法。
grails email plugin 的好處在於,你不需要特別再寫一個工具類別,或是繼承,或是任何方法來達成寄信的功能。
讓程式從無到有學會寄信,不到一分鐘就可以完成。
更多的細節,請參考 http://grails.org/plugin/mail

2012年10月7日 星期日

Grails 如何取得 hibernate 的 session ?

有的時候還是忍不住想要下native sql ,在 grails 中要如何取得  session 呢?
答案簡單到不行…

首先在controller 宣告一個  def sessionFactory

在你要執行的方法中,寫

def ss = sessionFactory.getCurrentSession()

這個 ss 就是hibernate 的session ! 等下,那怎麼取得 sessionFactory ? 別忘了Grails 內建Spring的注入功能,該注的他都幫你注入進去了。


def ret = ss.createSQLQuery("select * from post").list()
println ret.get(0)[0]

這種方法回傳的就是 List<Object[]> !
Grails 幫我們做掉太多事了! 邊學邊拍桌子啊 !

2012年10月1日 星期一

Grails 的ORM 關聯(一) belongsTo, hasMany

我們直接舉1對1,或是1對多的例子。

一對多來說,一個作者可以發表多篇文章。
那這個關聯我們要從哪邊開始加?我們可以先在「文章」(多)中先加 belongsTo。
文章「屬於」作者。這個應該沒有疑問。

class Post {
  String content
  Date dateCreated
  static constraints = {
   content(blank: false)

  }
  static belongsTo = [ user : User ]
}

最後的 static belongsTo = [ user : User ] 當然是我們注意的重點。
這個 [ user : User ] 其實是一個map,不過一個小寫的user ,一個大寫的 User有何用處?
這個user是給我們用做參考關聯的。透過這個寫法,Post 也可以反著查詢 User了。

例如有一個畫面是閱讀某人寫的文章,最上面需要出現那個人的id。因為我們在這裡已經建立好倒回去關聯 User 時的變數名稱 「user」,所以只要 post.user.userId 便很快的可以得到我們要的結果。

你可能也看過
static belongsTo = User 
的寫法,如果這樣寫的話,post 就不能反過來查到對應的 user了。如果沒有必要從 Post 反查 User 時,這樣寫當然有一點效能上的優勢。

belongsTo 還有要注意的一點,既然都說自己「屬於」別人了,所以當 User 從資料庫上被幹掉的同時,Post 也沒有任何存在的必要了。這個就是「屬於」的觀念。
當然在 User 這個類別上,也要記得加上
static hasMany = [ post : Post ]
如此一來關聯就正式接上了。接上之後有很多預設的神奇招式,在下一章會講到。

Grails 中 Domain class 如何自訂驗證

Groovy 彈性的語法,總是讓人有一點不習慣,但其實抓住幾個原則就行。
先來看這個自訂驗證的範例。

  static constraints = {
     userId (unique:true , size : 3..20)
     password(
       nullable:true , 
       size : 6..8 ,
       validator :{ password , user ->
          return password != user.userId
       }      
     )
     homepage(nullable:true , url : true)
   
  }


  1. 這個例子要達到的目的就是,使用者的密碼不要和帳號一樣即可。最大的限制式本身就是一個閉包。一般的驗證常用的就是 nullable , size 啊等等的。它有一點像 Java 後來加入的動態數量參數。所以你可以想到什麼就臨時加什麼 ( 當然缺點就是編譯時期無法知道是否正確 )。
  2. 我們主要看的是在 password裡的「自訂驗證」,這個時候要加 validator : value。在 value 的部份,我們傳入一個閉包,這邊我用藍色框起來。從key :value -> validator : value -> validator : 閉包。這樣就不會覺得 {   } 的出現很突兀了。
  3. 這個validator 所用的閉包,可以接受二個參數。password 當然就是使用者傳進來的參數囉。第二個參數是user 物件,這是方便你做比較用的。簡單來說,閉包的參數就是二個,一個是password ,一個是user 物件。若你不需要用到user 物件,那只要指名 password -> 即可。
  4. 後面要回傳true or false,所以你要告訴他怎樣是成功,怎樣驗證是失敗。

2012年9月30日 星期日

Grails 2.x 整合測試要注意的地方

2.x 版的整合測試 ( integration test ) 要注意的幾個地方 
  • class UsersIntegrationTests extends GroovyTestCase 記得要extends 這個類別,不然無法啟動orm。 
  • 在塞資料到資料庫時,要記得「所有的屬性 nullable 預設為 false」,也就是所有的值你都要塞入,如果不想要的話要自己明確指定 nullable:true
  • 在整合測試一直找不到原因的時候,例如當初 
def user = new Users( userId : 'joe') 
assertNotNull user.save(flush:true) 

最後一行一直出錯,錯誤訊息很模糊,一直不明其因。
在整合測試中,也請記得 

println user.errors 

很快的你就會看到原因了。印出來之後才發現我的屬性沒有全塞。
很快的再 Google 一下,才發現 2.x 預設的nullable屬性改了。
總之,希望有需要幫助的人剛好看到。 
* 在gsp/jsp 丟到controller的東西是 "blank" 喔! 沒填值的話,等於是丟空字串,並不是null。
 Grails 2.x integration test save not working failed gorm

Groovy與XML的相遇(三) : 如何找到節點

這一次就不寫 Java Code了,因為用傳統的寫法會發瘋。

一樣,以剛剛那個xml為範例

<root>
  <star age='22' blood='A'>布萊德彼特1</star>
  <star age='26' blood='C'>小勞勃道尼</star>
  <star age='24' blood='B'>基努李維</star>
</root> 

(明明他們就沒那麼年輕…)

問題1:  找到年紀小於25歲的男星,我只要他們的名字

println roots.star.find{  it.@age <=25 }.text() 

下底線又用了閉包傳入。很簡單吧! 可是執行失敗說!
因為xml讀進來都是字串啊,Groovy沒有人工智慧。
好,那怎麼辦…? 你如果有從我第一篇開始看,你應該記得轉型吧。

println roots.star.find{  it.@age as int <=25 }.text() 

你很快樂的按下去,不過結果還是錯的,你不要把Groovy當神好嗎。

println roots.star.find{  (it.@age as int) <=25 }.text() 

加個括號就行了。是那裡面的東西要轉成 int ,後面再來比條件。
問題1的解法,一行結束! 不過剛剛符合條件的男星有二個耶 ?
find 只會傳出第一個 match 的。
findAll 會傳出都符合條件的。於是我們再試一次

List list = roots.star.findAll{  (it.@age as int)  <=25 } as List
 

沒錯,還順便轉了型,裡面裝好了 groovy.util.Node。
List<groovy.util.Node> 啦。

問題2 : 把這些明星的年紀由小到大排序,呃,我要大於23歲的。然後裝成 List<Integer>。

當然以Groovy來說一定要秒殺解決的。


List list2 = (roots.star.@age.findAll{ (it as int ) >=23 } as List).sort()


我把這一行一個一個說明;
  • 取得root下的所有明星節點,但是我要他們的age而已,這時候已經是單純 age 的字串集合了。
  • 從這個結果中,幫我找 age 集合中年紀大於23 歲的,並轉型成 list。
  • 然後再排序。

是不是很方便呢? 其它的進階 xml 密技,就給你自己去研究了 !



Groovy與XML的相遇(二) : 解析文件

按照傳統,還是先來一段 Java code, 31行


import org.xml.sax.SAXException;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.IOException;

public class ParseXml {
  public static void main(String[] args) {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    try {
      DocumentBuilder db = dbf.newDocumentBuilder();
      Document doc = db.parse("src/languages.xml");

      //print the "type" attribute
      Element langs = doc.getDocumentElement();
      System.out.println("type = " + langs.getAttribute("type"));

      //print the "language" elements
      NodeList list = langs.getElementsByTagName("language");
      for(int i = 0 ; i < list.getLength();i++) {
        Element language = (Element) list.item(i);
        System.out.println(language.getTextContent());
      }
    }catch(ParserConfigurationException pce) {
      pce.printStackTrace();
    }catch(SAXException se) {
      se.printStackTrace();
    }catch(IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

我不針對上面抄來的程式做修正了,反正大約都是一層的xml簡單文件而已。下面是 Groovy 的寫法:

def roots = new XmlParser().parse("D:\\test.xml")
roots.star.each{   
   println "age="+it.attribute("age") +" " +  it.text() 
}

處理的是這份文件

<root>
  <star age='22' blood='A'>布萊德彼特1</star>
  <star age='26' blood='C'>小勞勃道尼</star>
  <star age='24' blood='B'>基努李維</star>
</root> 

你應該已經知道 it 是預設的,如果你的閉包參數只有一個,那麼it 就是指這個物件。 唯一要注意的就是,如果要取 tag 裡的文字是用 text(),要取參數就是 it.attribute("名稱")。

( 取參數也可以用 it.@age ,如果你開始習慣之後,就可以改成Groovy提供的做法。  )

roots.star.each 這種語法或許 Java 的程序員還不是很習慣,但其實jQuery 裡也大量使用這種觀念啊。

$("#talbe1 tr)".each({

});
是否你覺得似曾相識呢? 我把它換行一下。

$("#talbe1 tr)".each(  {這裡就是一個活生生的閉包啊}   );

再注意一次相同性,都是把閉包 { } 丟到方法each 中做參數。 在這裡閉包就像是一個沒名字的方法。

你還是有一點意見對吧…

roots.star.each{   
   println "age="+it.attribute("age") +" " +  it.text() 
}

你說明明就有一點不同,和jQuery不同啊,少了圓括號與最後的分號。
那我們改一下再來跑一次。

roots.star.each({     
    println "age=" + it.@age +" " +  it.text()   
});
我連分號都加了,這樣你沒話說了吧,還是可以跑啊。 why!!
為何在Groovy 不用加 ( ) 呢? 這是Groovy 提供的一種簡潔式的寫法。好啦,我直接整理給你看三種不同寫法,都可以跑。

roots.star.each({     
     
});
roots.star.each(){     
     
};
roots.star.each{     
    
};
這三種都是一樣的意思。我很快的總結一下

  1. 真正的寫法是如此,閉包是each的參數。 
  2. 為了好看,閉包可以放在each()的後面,因為它太長了嘛! 
  3. 如果each只收一個閉包做為參數,那 ( ) 就可以省了,它是2 的再精簡寫法。

再感覺一下… Groovy 是不是沒那麼奇怪了呢? 是不是常覺得…如果jQuery的寫法可以用在 Java 多好? 如果有這種感覺的話,你才是真的是個程序員!

Groovy 與 XML 的相遇(一) : MarkupBuilder + XmlSlurper

用Groovy 來產生 xml 再簡單不過了。雖然以前的 dom4j也是用物件的方式操作,但你要做的功夫相當的多。 先貼Java要怎麼寫…大約51行

import org.w3c.dom.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;

public class CreateXml {
  public static void main(String[] args) {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    try {
      DocumentBuilder db = dbf.newDocumentBuilder();
      Document doc = db.newDocument();

      Element root = doc.createElement("root");      
      doc.appendChild(root);

      Element star1 = doc.createElement("star");
      Text text1 = doc.createTextNode("布萊德彼特");
      star1.appendChild(text1);
      root.appendChild(star1);

      Element star2 = doc.createElement("star");
      Text text2 = doc.createTextNode("小勞勃道尼");
      star2.appendChild(text2);
      langs.appendChild(star2);

      Element star3 = doc.createElement("star");
      Text text3 = doc.createTextNode("基努李維");
      star3.appendChild(text3);
      langs.appendChild(star3);

      // Output the XML
      TransformerFactory tf = TransformerFactory.newInstance();
      Transformer transformer = tf.newTransformer();
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
      StringWriter sw = new StringWriter();
      StreamResult sr = new StreamResult(sw);
      DOMSource source = new DOMSource(doc);
      transformer.transform(source, sr);
      String xmlString = sw.toString();
      System.out.println(xmlString);
    }catch(ParserConfigurationException pce) {
      pce.printStackTrace();
    } catch (TransformerConfigurationException e) {
      e.printStackTrace();
    } catch (TransformerException e) {
      e.printStackTrace();
    }
  }
}
雖然非常容易理解,一個一個物件生成,但這麼單純的事,要花51行來寫,覺得你快要死了嗎? (況且他們的屬性我還沒有加進去哩……) 來一點輕鬆的吧。先寫了一個第一版,15行,很棒了吧??

import groovy.xml.MarkupBuilder
import groovy.util.XmlSlurper
def file = new File("d:\\test.xml")
def objs = [
    [ age: 22, name: "布萊德彼特", blood: "A" ],
    [ age: 26, name: "小勞勃道尼", blood: "C" ],
    [ age: 24, name: "基努李維", blood: "B" ] ]
def b = new MarkupBuilder(new FileWriter(file))
b.root {
    objs.each { o ->
        star(age: o.age , blood:o.blood ,  o.name) {          
        }
    }
}
如果不要先把資料弄成集合 ( 雖然這樣比較容易看的懂 ),要寫的行數更少…

def xml = new groovy.xml.MarkupBuilder(new FileWriter("d:\\test.xml"))
xml.root(){
  star(age: 22, blood: "A" , "布萊德彼特")
  star(age: 26, blood: "C" , "小勞勃道尼")
  star(age: 24, blood: "B" , "基努李維")
}

對,你沒看錯,就是6行。 除了行數變少之外,易讀性完全破表。 root 並不是一個死的方法,你將它改成stars,那麼產出的xml的根節點就叫做stars了。裡面所有的東西都能改,只要你結構掌握住了,xml 某一個節點要加新的屬性,易如反掌! 維護程式的人,你覺得他看上面的51行容易理解,還是下面的6行呢?

我們都知道重構的重要。若只是為了把原來就繁複的語法歸類而重構,那為何不一開始就精簡語法? 

這樣精簡的xml 語法,很大的功勞一樣是用閉包做的。程式人員只要專心在資料面,如何產生xml 的部份並非你的價值所在。除非你是開發 xml parser 的人,否則不要沉醉在自己高深的50行程式中。這裡面並沒有價值。再一次強調,就像醫生的價值在於醫好別人的病,不在於你會用幾種手術刀。若你可以空手就取出腫瘤,30秒結束回家吃飯,沒有病人會想抱怨的。

再談 Closure

上一篇講了,閉包是物件,不是方法,雖然使用上真的很像,這裡我們直接做證明…



  Closure comparator = { String a, String b ->
    a.compareToIgnoreCase(b)
  }
這樣很明白了吧,Closure的確是物件。差別在這個物件把自己當參數傳入某個方法後,這個方法會執行 Closure裡的行為。 當然在宣告的時候和傳統的「方法」不同,你要用 { } 括起來,而且要寫 = 。 參數也不是方法一樣的宣告方式,不過概念是一樣的。 只要能接受語法後,就把它想成「動態的方法」就好了。 在書上的例子寫到


Closure comparator = { String a, String b ->
   a.compareToIgnoreCase(b)
}
List fruit = [ "apple", "Orange", "Avocado", "pear", "cherry" ]
fruit.sort(comparator)
println "Sorted fruit: ${fruit}"

這是一種寫法。先定義好有名有姓的閉包,當然 sort 也要接受這樣的閉包,不然你也是白寫的。 能夠變的地方在裡面實作的「比較方式」而已。 若還記得我們上篇文章,那是一種更直接寫的寫法。就像暱名的方法一樣。

 rpt_list.sort{o1,o2->
    o1.total<=>o2.total
 }
後面還會很大量的使用 Closure,所以目前暫時還不習慣也是無所謂的。

2012年9月29日 星期六

Groovy 的一些好用語法(二) << , 動態屬性 , collect , sort ,

Groovy 是一種動態語言,可以輕易的將方法加到原來死板的 Java 類別中,這樣可以帶來很多好處,有時候太有彈性,甚至用起來有一點頭暈腦脹,所以先整理一下一些好用的方法。

先來講 << 。


List rpt_list = new ArrayList()
rpt_list << new Report(name:"as1" , m1:10 , m2:20 , total:30)
rpt_list << new Report(name:"as2" , m1:40 , m2:40 , total:80)
rpt_list << new Report(name:"as3" , m1:20 , m2:30 , total:50)

<< 看起來很奇怪,但說起來也蠻好理解的,就是「塞過去」。 看箭頭的方向也知道,我要把右邊的塞到左邊。這個是 Java 沒有的語法。 你要是看起來很不習慣,一樣還是可以用 add ,這是 Groovy 的好處,你把他想成加強版的 Java 即可。

new Report 右邊的東西,是把屬性像 map 一樣傳進去。冒號的左邊是key右邊是value。還蠻容易理解的吧。

這邊有一個有趣的地方。一般來說 Java 要動態呼叫一個方法或是屬性,有一點麻煩,也就是說我們「執行時期」才知道要呼叫誰的話,在 Java 是比較不直觀的方式處理的。再一次強調,我的觀點仍然認為,程式的價值不在這些轉換上,所以務必要很容易做到

既然你可以把 map 當建構子傳入,那你可能會想到他是怎麼動態去mapping。
假設你要自己寫也不難,在 Groovy 來說只要寫

def dynamic_prop="total"

assert rpt_list[0]."${dynamic_prop}" == rpt_list[0]."total"
assert rpt_list[0]."total" == rpt_list[0].total

下面二個斷言都會是 true。相信你已經知道 ${ } 是像jsp 裡的變數,Groovy可放在字串中。
所以 rpt_list[0]."${dynamic_prop}"  => 會變成 rpt_list[0]."total"


而造成了可以簡單的動態呼叫的效果。相對於 Java 直接又容易理解。

好了你囉嗦了這麼久,該說 collect 了吧。我們已經準備好剛剛的 List 了

List rpt_list = new ArrayList()
rpt_list << new Report(name:"as1" , m1:10 , m2:20 , total:30)
rpt_list << new Report(name:"as2" , m1:40 , m2:40 , total:80)
rpt_list << new Report(name:"as3" , m1:20 , m2:30 , total:50)

再來你 GSP/JSP 的頁面,老闆要你最上面要先列出一個一個總和。
聰明的你應該想到上一集的招式了

List rpt_total_list = rpt_list*.getTotal()


事情有這麼簡單當然最好,但是老闆改口說,業務要看的是 / 1000 的單位啦,不然值太大。
你抓了抓頭,這樣 「星點」好像不能用了,其實還有更棒的招,就是collect,一行。

List rpt_total_list = rpt_list.collect{ it.total/1000 } 

我紅色標起來的是 Closure,大陸翻閉包,很難聽,就還是叫 Closure好了。
這樣說,閉包是一個物件,這個物件裡要寫一些邏輯,包起來當參數餵給 collect。
collect 裡頭,Groovy 自己幫你實做了,你要餵它的就是閉包 ( 好吧…叫閉包也行 )
collect 收到閉包後他做了什麼事 ?
其實 collect 在上例被呼叫了三次,因為rpt_list 裡有三個物件。
這邊依序取這三個物件的 total 後除 1000 ,然後add 到List 中,有多少加多少,加到完之後結束,把 List 傳出來這樣。
如果用傳統的 Java 來寫,又要容易閱讀的話 :

private List<Report> getTotalList(){
  List rpt_total_list = new ArrayList();
  for(Report r : rpt_list){
     rpt_total_list.add(r.getTotal);
  }
  return rpt_total_list ;
}
//煩死了…一行Groovy比七行Java

閉包使用起來像一個方法,只是不用再獨立寫出來。Groovy 提供了很多對 Collection 的瘋狂技巧。再下來我們講sort。 想到要排序剛剛的sort就很頭痛,因為在 Java 來說,你得這樣寫。你要實作compare來完成這個需求,剛剛的名言又要再講一次,這種東西沒有價值,越好寫越快。價值在客戶要用什麼排序,不是在你怎麼寫出這段程式。
public void sortPeopleByGivenName(List personList) {
  Collections.sort(personList, new Comparator() {
  public int compare(Person p1, Person p2) {
  return p1.getFamilyName().compareTo(p2.getFamilyName());
  }
  });
}
//很煩吧…
用 Groovy 的話
def sortIt(list, property) {
    list.sort { p1, p2 -> 
    p1."${property}" <=> p2."${property}" 
  }
}

長的有一點奇怪,不過你呼叫 sortIt ( rpt_list , "total") 就可以得到你想要的結果了。 上面講過了,那個${}會整個被替換成你丟入的參數。你也注意到了 sort 本身就接受閉包,所以不如改成

rpt_list.sort{o1,o2->
    o1.total <=> o2.total
}

<=> 是啥啦 ! 它就是


public int compare(int i1, int i2) {
 if (i1 == i2) return 0;
 else if (i1 < i2) return -1;
 else return 1;
}

的簡寫啦 ! 所以在閉包裡如果比的是數字,除非你的比法和人家不同,否則 <=> 就行了! 這樣子 sort 你開心了嗎 ? 今天講的就是 collect 與 sort 最後,其實 sort 還可以加條件,剛剛都白寫了 ! 沒錯,Groovy的寫法真的太有彈性了,你還可以寫成

rpt_list.sort{ it.getTotal() } 
是不是很瘋狂呢? 叫你「按照裡面每一個物件的total 給我排序!」 如果你要排的是數字,最後一個方案就是你要的! 一行搞定。

想哭嗎? 很正常。
如果不是單純的排序,你有一些奇怪規則的,留給你自己,在閉包裡動動腦筋囉!

Groovy 的轉型

Groovy 的轉型寫為 as .
為什麼 Java 已經有轉型了,Groovy 還要發明一個 ? 舉個例子


String s = "this,is,a,book"
String[] arr = s.split(",");

你這個陣列用一用之後,突然後面的邏輯變了,你需要再動態 add 一些元素到這個已經拆開的陣列中,你也知道陣列在 Java 是不能隨意改變大小的,所以你只能再寫一些程式,把這個陣子轉成 List 後再處理。
我網路上隨便查了一些寫法 :

1.

List myList = new ArrayList();
String[] myStringArray = new String[] {"Java", "is", "Cool"};

Collections.addAll(myList, myStringArray);
2.
List<String> list = new ArrayList<String>(words.length);  
for (String s : words) {  
    list.add(s);  
}  
3.
List<String> ret = Arrays.asList( 裡面放字串陣列) 

好,無論怎樣,都只是在表達一件事而已,而且這些事對真正的程式邏輯來說毫不重要,不是商業邏輯的一部份。在 Groovy 來說仍舊是一行解決。

String s = "this,is,a,book"
def ret = s.split(",") as List

( 當然第一行是字串陣列不算啦 )
這樣的好處很多! 而且中間甚至可以加上型態轉換,例如我們上一集的例子

def id_array = list*.getId() as Integer[]

我們假設 list 裡是一堆 Book ,而且他的id 是字串型態,我後面的程式需要他的 id 以數字的方式,包在整數陣列裡面。

現在你看的懂了,在第一個方法呼叫後,得到的是List<String> ,但是我們要的其實是整數陣列 ( 對不起你的同事就是要整數陣列 ) ,你打幾個字便已輕舟過萬重山。

如果你的同事突然發瘋,他決定還是List<Integer> 好了,那你就把 Integer[] 槓掉改成 List 就收工。

請記住,程式的重點都是在解決一個問題,中間轉型或是換集合物件都沒有價值。這些沒有價值的事,就是要快速處理,敏捷開發,也要有敏捷的語言搭配才可以的 !




Groovy 的一些好用語法(一) *.

 Groovy 可以和Java語法混用,這一點真的是很有趣的事。
在很不熟Groovy的時候,甚至可以把 Java 的寫法套進來。但是久了之後就會發現,Java 在一件簡單的事上,往往要很重覆的不斷寫上一樣的東西。

有些人會不屑的回應說,只是語法上的甜頭,不足掛齒。
其實這只看到表面。語法上的簡化、行數減少這是事實,不能否認,但好處為何?


  • 行數少,bug 就少。100行出錯的機率一定比10行來的高。
  • 行數少,程式的「自我描述性」就高。在 Java 中的五行,其實是在說一件事,但因為這5行會消耗你閱讀上的力氣,當行數越來越多的時候,你會很難抓住程式本身的重點。行數少,程式可以集中力氣在「表達」上,也就是易讀性會大大提升。
  • 等下的例子會十分明顯。


假設我們需要得到全部的「書」,然後把他的 id 傳到 jsp 供應用…不管你程式寫幾行,或用任何的程式語言,中文來說就是這二件事,以傳統的Java 來說:

List<Book> book = session.createQuery("from Book").list(); 
//do something....
List<Integer> list_id = new ArrayList();
for(Book b : book){
   list_id.add(b.getId());
}
request.setAttribute("id_list",list_id);

這個是我們每天在寫程式的時候做的事,雖然很囉嗦但是久了也習慣了。
Groovy 怎麼處理?

def book = Book.list()
//do something
["id_list", book*.getId() ]

啥?
對,就是你眼睛看到的。
*. 是什麼 ? 其實望文生義,就是「在這個集合裡的所有物件,都都呼叫這個方法,得到的值紀錄起來用List傳出來」。
這裡的 book 如 Groovy 第一行來說,已經是List  <Book> 了
book*.getId() 在這裡就是說「 book裡全部的物件都呼叫getId() 並塞到另一個新的List 中」
於是這個繁瑣的過程就這樣結束了。如果臨時 ui 又需要 book 裡所有的作者

["author_list" , book*.getAuthor()] 

馬上一行就可以解決並傳到前端去…
再一次說明,語法上的甜頭,並不是不重要的事,語法上的精簡,可以大大增加

  • 易讀性
  • 開發速度
  • 減少出錯機率







2012年9月24日 星期一

cygwin 在 win7 如何設定ssh

浪費了不少時間,請以這一篇做安裝,否則怎樣都會安裝不起來。

  • 在安裝cygwin時要選擇什麼。
這三樣套件記得裝。
cygrunsrv
openssh
vim

  • 環境變數
環境變數記得新增一個 CYGWIN=ntsec
在path 這個變數,最後加一個 c:\cygwin\bin

  • 如何安裝 ssh
 
*千萬記住,請按右鍵「以管理者的身份執行」來跑 cygwin 的terminal。否則最後會以沒有足夠的權限失敗做結尾。



a.先開權限
chmod +r /etc/group
chmod +r /etc/passwd   
chmod +rwx /var/

b.開始設定
輸入 ssh-host-config
Should privilege separation be used? (yes/no) yes 
Do you want to install sshd as a service? (yes/no) yes 
Enter the value of CYGWIN for the daemon: [ntsec] ntsec 
Do you want to use a different name? (yes/no) no
Create new privileged user account 'cyg_server'? (yes/no) no
Do you want to proceed anyway? (yes/no) yes

cygrunsrv.exe -S sshd 來試一下是否啟動成功。

2012年8月8日 星期三

jQuery - 如何加入自動完成 ? How to achieve auto complete function?

第一步你要先 download jQuery,並且在jsp 中宣告 :

<script type="text/javascript" src="js/jquery-1.3.2.js"></script>
<script type="text/javascript" src="js/jquery-ui-1.8.7.custom.min.js"></script>

版本號就看你下載的jQuery 來宣告。
接下來在你的 js 中,針對某一個 input 欄位,此例為料號,加入 auto complete 的功能 。

$("#material_text").autocomplete({
        source : function(request, response) {
           
            $.ajax({
                url : $("#path").val() + 'ajax.action?method=list&act=find_material_text',
                type : "POST",
                dataType : "json",
                cache : false,
                delay : 1500,
                data : {
                    term : request.term
                },
                success : function(data) {
                //    $("#material_desc").unblock();
                    response(
                        $.map(data , function(item) {
                            return {
                                label : item.label,
                                value : item.value,
                                desc : item.desc
                            };
                        })
                    );
                },
                error: function(XMLHttpRequest, textStatus, errorThrown) {
                    alert(textStatus);
                
                }
            });
        },
        select : function(event, ui) {
            $("#product_desc").attr('value', ui.item.desc);            
        }     
    });

以下針對上面的程式做簡單說明。


自動完成,主要是搭配 ajax 的技術,來動態向後端要值。此例是將回傳結果,回傳成類似下拉式的選單,讓使用者可以從這些選單做選擇,而不用記得全部的輸入值資料。可以在 SQL 中做 LIKE ,或是其它搜索方式,讓使用者不一定要從「頭」開始輸入。


delay 是輸入後趨動查詢的間隔時間,如果使用者想要馬上打馬上看,那間隔可以短一點,不過對後台的呼叫次數當然也會變多,若機器不是很強的時候可以調慢一點。


 data : {
          term : request.term
         },


這是要送到後台去的,term 可以在後面當成一個 parameter 來抓取。就是使用者目前輸入的值。


select : function(event, ui) {
            $("#product_desc").attr('value', ui.item.desc);            
        } 


這裡是指,當使用者「點選了自動完成給你的結果集」之後所觸發的事件。
若沒有要特別做什麼,這裡可以不用去實作。
這裡是將 ui.item.desc 的值設入 某個 html input element 中 。
ui.item 是固定的,就是後台回傳的一個一個物件。
desc 是你自己可以在後台加入的屬性。
desc 我是放料號的說明。等下會解釋。

這樣前端的部份大概就好了。再來後端要提供觸發回傳的結果集有哪些。
至於這一段方法怎麼去觸發,就是看你web framework 使用的是哪一種,簡單來說就是要觸發到這一段,並且要用 ajax + json 的方式吐回去。


value = p("term");
sb.append("[");
List<Object[]> list = sap.createSQLQuery("select top 500 MATERIAL,MATERIAL_DESC from  MATERIAL where 1=1  and (material like  :val  or MATERIAL_DESC like :val )")
.setParameter("val", "%" + value + "%").list();
int i = 0;
for (Object[] ob : list) {
    sb.append(String.format("{\"label\":\"%s\",\"value\":\"%s\",\"desc\":\"%s\"}", getString(ob[0]) + " " + getString(ob[1]).replaceAll("\"", ""), getString(ob[0]), getString(ob[1])));
    if (i != list.size() - 1)
        sb.append(",");
    i++;
}
sb.append("]");

首先我只撈前500筆,若太多的話前端顯示會當掉。
可以用關鍵字查詢。
 json 裡則有三個屬性 label , value 與 desc ,desc是我自己加的,要和前端的程式同名才能使用。

  1. label 放料號的 id + 說明。這個是顯示用的。
  2. value 則放料號的 id。這個值在點了之後,會真的塞到原來的 input field中。
  3. desc 則只放料號的說明。我拿這個值來塞到畫面別處,讓使用者可以看到很長的料號說明。 將此三值分開,在前端可以方便使用。

組json 字串是當初花比較多時間的部份。


getString = String.valueOf 而已。  p 是取parameter 我自己簡化過的方式。

jQuery - 如何隱藏 select 下的 option. How to hide the options in select using jQuery.

$("#sel option").hide();    ?

沒辦法耶…

因為hide 不是跨瀏覽器的,至少 IE 8 無法達到這個效果。

網路上有一些solution,但是要以plugin 的方式進行,我覺得沒有必要。

最簡單的原則就是 「把option 搬走」

先宣告變數來裝。


var from_text="" ;
var to_text="";


$("#from_year").change(function(){
            if($(this).val()==''){                                                 
                from_text = $("#from_Q").html();                
                $("#from_Q option").remove();
            }else{
                $("#from_Q").html(from_text);
            }
        });

如果年選成空白的時候,把第幾季(Q1-Q4的select)的選擇搬走。
如果年選回某一年的時候,再把第幾季的文字加回來即可。


2012年8月3日 星期五

Android 如何使用外部gps 裝置?

大部份的android 智慧手機早有 GPS  晶片,何必還需要開 GPS  外部裝置呢?
我發現手機在「國外」(購買地外) ,打開 GPS  後定位的速度異常的慢。
查了一下才知道手機都有內建搜尋的國家,若非本國來說會比較慢。
並不清楚我的 GPS 裝置 ( Wintec ) 是否有類似的問題,不過試試看連外部裝置也是不錯的事。

最近離開購買國 ( 荷蘭 ) 後,在比利時旅行時的定位速度非常慢,所以想說透過之前已經買的 Wintec 來做實驗

首先去Google Play 下載Bluetooth GPS 。
安裝好之後,打開你的藍芽,也打開你GPS 的藍芽。
配對。連接。並且勾選用此軟體模擬你內部的GPS 晶片。
如此一來,你的Google Map 上就會出現小藍點了。中間不需要打開自己手機的 GPS 功能。


連外部 GPS 比較省電,還是使用內部的GPS 晶片比較省電? 目前還不得而知。

到時候去英國旅行的時候就可以知道效果如何了。
記得配溫開水服用「google 離線地圖」。
離線地圖 加 GPS ,真的是個無敵利器!
大部份的台灣人在本國使用3g 沒有感覺,但是出國旅行的時候,這樣的組合可以讓你免於迷路之苦。

如何將程式碼漂亮的post 在部落格中

最簡單的方法,就是去

http://formatmysourcecode.blogspot.nl/

將自己的程式貼上,按 format text ,再將結果貼回來即可。

Java - 如何透過jco 與 TimerTask 做自動資料同步

前提 :

  1. 自己的資料庫準備好對應的sap表格,欄位名稱和格式要確定好了。
  2. 在servlet 中也要設定這個程式何時要跑,這個就不多提了。
  3. 這個class 會繼承 TimerTask ,當servlet 設定的時間條件觸發時,便會執行。

說明 : 

  1. 本程式提供了成功、失敗寄信的考量。當失敗時會將java exception寄出。
  2. 若sap 的程式有變,只要在db 的表格中新增sap 多的欄位即可,不用動程式。
  3. 每100筆就會flush 出去,不會快速增加無用的暫存記憶體。



public void run() {
        Session s = null;
        JCoDestination jCoDestination = null;
        Transaction tx = null;
        Boolean trans_done = false;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
       
        synchronized (this) { // 一次只能有一個進程在執行
            try {    
                Boolean need_to_run = true;
                s = BaseDAO.getSession();
                tx = s.beginTransaction();
                RptLogger.logger.info("== START ==");
                RptLogger.logger.info(new Date());
                //如果沒有執行過 ,或是執行過失敗的時候會自動跑 : 有
              
                if(need_to_run){
 
                    RptLogger.logger.info("try to connect to  SAP Table ...");                                                
                    jCoDestination = ConnectSAPServer.ConnectDETest();
                    RptLogger.logger.info("connect to  SAP Table OK!");
                    JCoFunction function = jCoDestination.getRepository().getFunction("sap 提供的function name");
                    RptLogger.logger.info("get function OK!");
                    if (function == null)
                        throw new RuntimeException("function not found in SAP.");
    
                    // 傳入的參數
                    // SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
                    Calendar ca = Calendar.getInstance();
                    int this_year =  ca.get(Calendar.YEAR);
                    int this_month = ca.get(Calendar.MONTH)+1;
                    ca.add(Calendar.MONTH, -1);
                    int pre_year  = ca.get(Calendar.YEAR);
                    int pre_month = ca.get(Calendar.MONTH)+1;
                    //這邊是視你sap 程式的參數需要而加。
                    function.getImportParameterList().setValue("SALES_YEAR", this_year);
                    function.getImportParameterList().setValue("SALES_MONTH",this_month);
                    function.getImportParameterList().setValue("SALES_YACO", "COGS");
    
                    function.execute(jCoDestination);
    
                    JCoTable returnTable = function.getTableParameterList().getTable("sap 吐出來的表格名稱");
                    RptLogger.logger.info("get SAP Table OK!");
    
                    
                    List<String> header_cols = s.createSQLQuery("select COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '你自己db的表格名稱'").list();
                         
                    String sql = "Insert into 你自己db的表格名稱(  ";
                    int x = 0;
                    for (String string : header_cols) {
                        sql = sql + string;
                        if (x != header_cols.size() - 1) {
                            sql = sql + ",";
                        }
                        x++;
                    }
                    x = 0;
                    sql = sql + ") values (";
                    for (String string : header_cols) {
                        sql = sql + "?";
                        if (x != header_cols.size() - 1) {
                            sql = sql + ",";
                        }
                        x++;
                    }
                    sql = sql +" ) ";
                    Query q = s.createSQLQuery(sql);
                 
                    RptLogger.logger.info("There are " + returnTable.getNumRows() + " rows to be inserted.");
                    if (returnTable.getNumRows() > 0) {
                        
                        returnTable.firstRow();
                        for (int i = 0; i < returnTable.getNumRows(); i++, returnTable.nextRow()) { // 總共幾筆資章要塞
                            x = 0;
                            for (String col : header_cols) {
                                q.setParameter(x, returnTable.getValue(col));
                                x++;
                            }
                            q.executeUpdate();
                            if (i % 100 == 0) {
                                s.flush();
                                s.clear();
                            }
                        }
                        s.flush();
                                                                       
                        tx.commit();
                        trans_done = true;
                        RptLogger.logger.info("== YURSWEBFICS END ==");
                    }
                    if(trans_done){                       
                        sendOKEmail();
                    }
                }
            } catch (Exception ex) {
                if (tx != null)
                    tx.rollback();
                ex.printStackTrace();
                StringWriter sw = new StringWriter();
                ex.printStackTrace(new PrintWriter(sw));
                RptLogger.logger.info(sw.toString());                                  
                sendWrongEmail(sw);                                                
            } finally {
                if (s != null && s.isOpen()) {
                    s.close();
                }
             
            }
        }
    }
    
    private void sendOKEmail() {
        MessageSender msender = MessageSender.getInstance();
        //寄ok信
        
    }

    private void sendWrongEmail(StringWriter sw) {
        MessageSender msender = MessageSender.getInstance();
        //寄失敗信,把java exception 寄出去,讓it可以第一時間抓到問題點。
    }
    

Java - 如何透過jco 連接SAP 程式

這一篇主要是一個共用的類別來連結,下一篇會提供可以動態將SAP 吐出來的表格原封不動的塞入自己Database的表格,當然,你這個自己的表格內容要和SAP 的一樣,可以少,但是不可以多。詳細內容是下一篇會介紹。


package com.deu.report.util;

import java.io.File;
import java.io.FileOutputStream;
import java.util.Properties;

import com.sap.conn.jco.JCoDestination;
import com.sap.conn.jco.JCoDestinationManager;
import com.sap.conn.jco.JCoException;
import com.sap.conn.jco.ext.DestinationDataProvider;

public class ConnectSAPServer {
     //SAP服務器IP地址 
   // static  String DEI_SAP_CONN_SERVER =   ; 
    //static  String T11 =  "172.test.test.test" ; 
     
    static  String DE_SAP_CONN =  "DE_SAP_CONN" ; 
    static  String DE_TEST_SAP_CONN =  "DE_TEST_SAP_CONN" ; 
     
    static { 
        Properties connectProperties =  new  Properties(); 
        connectProperties.setProperty(DestinationDataProvider.JCO_ASHOST, "172.test.test.test");  
        connectProperties.setProperty(DestinationDataProvider.JCO_SYSNR,   "00" );         //系統編號 
        connectProperties.setProperty(DestinationDataProvider.JCO_CLIENT,  "025" );        //SAP集團 
        connectProperties.setProperty(DestinationDataProvider.JCO_USER,    "test" );   //SAP用戶名 
        connectProperties.setProperty(DestinationDataProvider.JCO_PASSWD,  "test" );      //密碼 
        connectProperties.setProperty(DestinationDataProvider.JCO_LANG,    "EN" );         //登錄語言 
        connectProperties.setProperty(DestinationDataProvider.JCO_POOL_CAPACITY,  "3" );   //最大連接數 
        connectProperties.setProperty(DestinationDataProvider.JCO_PEAK_LIMIT, "10" );      //最大連接線程 
     
        createDataFile(DEI_SAP_CONN,  "jcoDestination" , connectProperties); 
        
        connectProperties =  new  Properties(); 
        connectProperties.setProperty(DestinationDataProvider.JCO_ASHOST, "172.16.test.test");  
        connectProperties.setProperty(DestinationDataProvider.JCO_SYSNR,   "00" );         //系統編號 
        connectProperties.setProperty(DestinationDataProvider.JCO_CLIENT,  "025" );        //SAP集團 
        connectProperties.setProperty(DestinationDataProvider.JCO_USER,    "test" );   //SAP用戶名 
        connectProperties.setProperty(DestinationDataProvider.JCO_PASSWD,  "test" );      //密碼 
        connectProperties.setProperty(DestinationDataProvider.JCO_LANG,    "EN" );         //登錄語言 
        connectProperties.setProperty(DestinationDataProvider.JCO_POOL_CAPACITY,  "3" );   //最大連接數 
        connectProperties.setProperty(DestinationDataProvider.JCO_PEAK_LIMIT, "10" );      //最大連接線程 
     
        createDataFile(DE_TEST_SAP_CONN,  "jcoDestination" , connectProperties); 
        
    } 
     
    static  void  createDataFile(String name, String suffix, Properties properties){ 
        File cfg =  new  File( name+ "." +suffix); 
        if (!cfg.exists()){ 
            try { 
                FileOutputStream fos =  new  FileOutputStream(cfg,  false ); 
                properties.store(fos,  "for DEI only !" ); 
                fos.close(); 
            } catch  (Exception e){ 
                throw  new  RuntimeException( "Unable to create the destination file "  + cfg.getName(), e); 
            } 
        } 
    } 
 
    public  static  JCoDestination ConnectDE(){ 
        JCoDestination destination = null ;    
        try  { 
            destination = JCoDestinationManager.getDestination( DE_SAP_CONN ); 
        }  catch  (JCoException e) { 
            e.printStackTrace();
        } 
         
        return  destination; 
    } 
    public  static  JCoDestination ConnectDETest(){ 
        JCoDestination destination = null ;    
        try  { 
            destination = JCoDestinationManager.getDestination( DE_TEST_SAP_CONN ); 
        }  catch  (JCoException e) { 
            e.printStackTrace();
        } 
         
        return  destination; 
    } 
}

Java - jco3 如何在windows + tomcat 下使用 ( 部署篇)

SAP JCO 主要用於 Java 呼叫 Sap 程式時使用。當然sap 的程式要開放remote call 與帳號密碼。

1. 首先要下載 jco  的不同版本,windows 主要是分32位元與64位元。
( 這部份可以向貴公司的sap 管理者請求,因為下載時需要提供sap伺服器的資訊 )

2. 將 sapjco3.jar 丟到 tomcat 的共用lib 下面。

3. 將 sapjco3.dll 丟到 C:\\Windows\System32 下

4. 要安裝 Microsoft Visual C++ 2005 SP1 在 tomcat server 那台機器上! 否則無法使用。

下二篇將紀錄如何透過 jco 呼叫 SAP 的程式。