2013年12月31日

GORM custom id 另解

在前篇 使用 Grails 開發應用系統之感 提到 "框"不"框"住的問題, 其中提到不同版本 Grails 的 domain class 用到 transients 的技巧來避免使用 (Hibernate) object id 做為資料庫的 PK。該算是自己一時失查 (此查非彼"察") 呢? 還是才疏學淺?! 就在無意間瀏覧 Blog 時, 看到一篇 GORM custom id mapping (作者: Ilya Sterin) 的技巧才算找到我的答案。

因此, 依樣畫葫蘆做了一個 domain class:
class Employee {
    String id
    String code // 員工編號
    String name // 員工姓名
 
    static constraints = {
        id maxSize: 3
        code maxSize: 3, nullable: false, blank: false, unique: true
        name maxSize: 20
    }

    static mapping = {
        id generator: 'assigned', name: 'code'
    }

    String getId() {
        this.id ?: getCode()
    }
 
    void setCode(code) {
        this.code = code?.toUpperCase() // 大寫
    }
}

在典型的 CRUD controller 中如果有提供  code 的欄位變更, 因設計了 code 來代替 id 欄位, 而 id 負責查詢而 code 負責輸入的情形下, 需要特別處理 save() 與 update() 兩個 methods :
  1. save() 中...
    • unique validation 需要自行處理, 而非讓它發生 org.springframework.dao.DataIntegrityViolationException
  2. update() 中...
    • 須先行以 params.code 查詢是否輸了已經存在的資料了
    • 變更 code 欄位值之前, 先刪掉"舊"的資料 (它不像下 SQL 那般直接 update key value 就好, 參考資訊: Hibernate, alter identifier/primary key )
如下所示:
    @Transactional
    def save(Employee employeeInstance) {
        // ... (略) ...

        try {
            employeeInstance.save flush:true

        } catch (DataIntegrityViolationException dive) {
            employeeInstance.errors.rejectValue('code', 'default.not.unique.message' ,['code', message(code: 'employee.label'), employeeInstance.code] as Object[], message(code: 'default.not.unique.message'))
            employeeInstance.code = params?.id // 還原
            respond employeeInstance.errors, view:'create'
            return
        }

        request.withFormat {
        // ... (略) ...
    }
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    def beforeUpdate(employeeInstance) {
        if (Employee.findByCode(params?.code?.toUpperCase())) {
            employeeInstance.errors.rejectValue('code', 'default.not.unique.message' ,['code', message(code: 'employee.label'), employeeInstance.code] as Object[], message(code: 'default.not.unique.message'))
            employeeInstance.code = params?.id // 還原
            throw new ValidationException(null, employeeInstance.errors)
        }
    }
    @Transactional
    def update(Employee employeeInstance) {
        // ... (略) ...

        if (params?.id != params?.code?.toUpperCase()) {
            try {
                 this.&beforeUpdate(employeeInstance)
            } catch(e) {
                employeeInstance.discard()
            } finally {
                if (employeeInstance.hasErrors()) {
                    respond employeeInstance.errors, view:'edit'
                    return
                }
            }
            // 先刪後增
            employeeInstance.delete()
            employeeInstance.discard()
            employeeInstance.id = null
        }
        employeeInstance.save flush:true // 功能變為 insert 而不是 update 了

        request.withFormat {
        // ... (略) ...
    }
要注意的是, 這是 v2.3.x 的作法: save() 與 update() methods 都冠上了 @Transactional, 而早期 transaction 處理則是交由 service 來做。所以, 為避免先查詢是否已存在的資料時, 發生
org.hibernate.HibernateException: identifier of an instance of ... was altered from ... to ...
因此將它獨立為另一 method: beforeUpdate(), 並宣告為 transaction "NOT_SUPPORTED"。

完成。

ps. 花了不少時間!

2013年12月30日

Java實戰兩三事

多年前的 presentation:

使用 Grails 開發應用系統之感

ihower 的一篇 FAQ 開頭中提到 [Rails 發明人... : Rails Is Not For Beginners], 不知這自問自答式的內容是否也同意了 Rails 發明人的說法?! 但, 卻又像傳教士般的引導閱者進入 RoR 的世界之中?!

lyhcode 的 [程式設計師小心別被框架給「框」住了...] 一篇也提到 [... 如果一位「資深」的 Java 程式設計師,在過去 3-5 年間在專案中應用這些技術,可是不深入瞭解這些框架解決了甚麼問題、用什麼方法實作、底層如何運作以及如何擴充或調校,就很難有真正屬於自己的 Know-how。...], 文中意在說明"用過"(而非"適當運用")一些 framework 時存在的風險。

當個人近幾年發展的一些小案專使用 Grails 時, 也意識到了一點點以上的狀況: 需要溝通並傳達一些基礎與概念, 讓其他同仁放下排斥的意念並願意花時間來接受, 同時也須避免被它給"框"住了。

譬如: Grails 1.1.x 時代, GORM 並不支援 enum, 如果要使用 inList constraints 並活用在 GSP viewer 中, 確實要花一點技巧才能達到; 雖然在 v1.2.1 以後就支援了, 但當下專案驗收卻是不會等人的。

又如: legacy RDB 與 GORM 結合, 其實談的不是 ORM 而是 R-->O mapping; 因為資料庫是 DBA 在管的, 不是 AP 開發人員想怎樣就怎樣, 恣意使用了 object ID 而挷架了 DB。因此, adopt 欄位時定義了諸如下列的 domain class:
class Department {
    String id // 不使用內定的 long type
    String code
//    ...
    String description
 
    static transients = ['code']

//  ...

    static constraints = {
        id(maxSize: 3)
        code(nullable: false, blank: false, size: 2..3, unique: true)
//        ...
    }
 
    static mapping = {
        id generator: 'assigned'

        columns {
            id column: 'code' // code 即 id
//            ...
        }
    }

//    ...

// 以 code 代替 id
    void setCode(String code) {
        this.id = code.toUpperCase() // 轉 upper case
    }

    String getCode() {
        this.id
    }
}

但, 在到了 v2.3.x 之後卻不能正常運作了, 因為 code 欄位並不是真正的欄位 (尚不知是 Hibernate 版本的問題, 還是 Grails 版本的問題); 如果真的無法增加 id 欄位而只能修改的話, 應該會如下:
class Department {
// ...(略)...
 
    static constraints = {
        id nullable: false, unique: true, maxSize: 3
        code blank: false, size: 2..3
//        ...
    }

    static mapping = {
        id column: 'code', generator: 'assigned'
//        ...
    }

 
//    ...(略)...
 
    String getId() {
        this.id ?: '' // 防止 null value 造成 retrieving 問題
    }

    void setCode(String code) {
        setId(this.code = code?.toUpperCase())
    }
}
除了喪失了對 code 欄位進行 unique constraint 作用, 不料輸入相同重複的 code(即id) 之後, 即使在 controller 中加了 rejectValue() 控制:
def save(Department departmentInstance) {
//    ...

    try {
        departmentInstance.save flush:true
    } catch (e) {
        if (ExceptionUtils.getRootCause(e) instanceof NonUniqueObjectException) {
            departmentInstance.errors.rejectValue('code', 'default.not.unique.message',
                ['code', Department.class.name, departmentInstance.code] as Object[], '')
            respond departmentInstance.errors, view:'create'
        }
        return
    }
//    ...
}
仍無法阻止資料的存檔( create失敗 卻變成 update成功 )!
一時之間找不到解法, 腦袋中頓時出現 "框" 住的感覺。

不過, 正如過去長官的提點: 做事情要有中心思想。如果, 不該"重新造輪子"、"站在巨人的肩膀上"... 角度思考來使用 Grails, 理應快速開發客戶所需的應用系統。畢竟它背後有 SpringSouce(spring.io) 及眾多的 user group 在支撐。

2013年10月31日

用 unoconv 轉出我的 .xlsx 資料

典型的應用程式開發多會處理資料庫(RDB)。
有很多情形是版本會依不同的開發環境不同而資料庫也有所不同,例如:
  • Dev. App sever --> Dev. DB server
  • UAT App server --> UAT DB server
對於 Dev. DB server 上的資料,無論是測試案例或設定資料會不斷、不斷的異動;或者像 UAT DB server 上的資料會從不同的測試 phase 往前或往後進、退版時而所有異動。

App 前後版本的異同有很多開發工具可用而能看出差異,但對於要觀察或審視因此而調整的差異資料,要如何比對出來呢?

這個問題對於精熟資料庫及工具的 DBA 來說,也許不是什麼大問題;要模擬前後不同的(資料庫中)測試案例或設定資料,無論是觀察或資料補遺,免不了要進行資料兩相比對,這種情形可能來說,但對於應用程開發人員來說會是不小的困擾。

以個人而言,以  Unix 為作業平台使然,會第一時間聯想到很多 command 來操作文字型態的資料差異,例如:diff。再者,很多資料庫相關的工具有很多匯出的功能,例如 SQL Developer 能匯出 .csv 檔案。如此來說,可以用 diff 指令比對不同時點或不同資料庫同一 Table 資料的匯出檔,即可分辨其差異。

但,個人也遇過有些開發或資料管理人員喜歡使用 Excel 來進行資料統整;若遇上要用此來做為資料比對的對像,又要如何呢?

直觀上,就是使用 Excel 來轉存 .csv 檔案即可。

但幾種情形可能不見得適用:
  • 沒有 license 供安裝 Excel
  • 要大量、批次比對 (這裡指的對像是 Tables)
  • 非 Unix / Linux 平台使用者 (意指 command line 型式)
想到的方法是 OpenOffice/LibreOffice,以 command line 方式匯出,供 diff 指令來進行比對,然後以 shell script 型式存在,就可以進行批次、自動化作業。(當然,也有人會用 VBS 對 Excel 調用 API 來做,再用排程來進行此一作業)

使用 OpenOffice/LibreOffice 時可借助 unoconv (python code) 來做,可省去很多寫 script 的時間。做法:
  • 安裝 OpenOffice/LibreOffice (通常 python 是預裝好的,若否也要安裝)
  • 設定 PATH,確定能執行到 python、OpenOffice/LibreOffice、unoconv
  • 分別匯出欲比對的兩個 .xlsl 檔案成 .csv 檔案
    unoconv -f csv myTableVer1.xlsx
    unoconv -f csv myTableVer2.xlsx
    
  • 儲存比對的結果
    diff myTableVer1.csv myTableVer2.csv > myTable.diff
    cat myTable.diff | colordiff | less -R
that's it.

2013年10月27日

Upgrade 我的 Snow Leopard 到 Mavericks

使用 OSX 10.6 有一段時間了,對於 software development 來說開始有些不方便了;像是 XCode 工具的版本限制以及 OpenJDK 7 安裝問題等。自從 Apple 公司發表新款 iPhone 5S/C 與 iPad Air 同時,突然佛心來著,OSX 10.9 提供免費升級:
當然,對個人來說簡直提天掉下來的禮物,於是開始著手安裝。
(1) 程序上總是先求保險,得準備第 2 顆硬碟進行資料備份:
(2) 設定第 2 顆硬碟開機,測試並確認資料有抄寫:
(3) 無誤後再設回原開機磁碟,重開機後開始下載安裝:
安裝完畢後,便是檢查所有常用應用程式的相容問題;多數情形下使用系統更新作業或程式換版就可以解決了。不過,最麻煩的是得下載幾個重量級的更新,例如:XCode;而且原系統中的 JDK 6 會被移除,須自行下載與安裝 Java for OS X 2013-005

整個升級的過程最需的是體力與細心,因為備份時的資料整理與枯等,安裝後的目錄、檔案的移置、缺漏確認等,所需的時間很長不消十數小時。萬一不小心,大多資料的遺失可是會損失慘重。

而初步的使用心得,有 3 個小地方待適應:
(1) mouse / pad 的 scrolling 手勢方向與 v10.6 相反
(2) [swipe between pages] 手勢的功能,因不熟悉而常將正在瀏覧的頁面切到上一頁或下一頁
(3) 輸入法 hint 與過去的方式有點不同,尤其是標點符點的字鍵有更動,會一時之間找不到

that's it!

ps. 另外,中文字體看起怪怪、醜醜的

2013年10月15日

Multiple assignment in Groovy

在 Groovy 的說明資料中提到, 物件可以進行 multiple assignment。從其範例可看出 values 是 List 型別的: ...The syntax works for arrays as well as lists, as well as methods that return either of these...。

在 HA.KI blog 的範例 中最後一個也舉出了使用 regular expression 的技巧:
def money  = '12 Euro'
def regexp = /(\d+) (\w+)/
def (exp, amount, currency) = (money =~ regexp)[0]

assert '12' == amount
assert 'Euro' == currency

不過, 對於 List 的表述方式:
listObject[index]
// 或
listObject.get(index) // 保守寫法: listObject?.get(index)
是存在當掉的風險的, 因為 index 有可能超過索引範圍。
以上例來說, 只要 money 的值不是依  '數值 幣別'  呈現就會發生。 (例如 'Euro  12')

所以, 可以改善的技巧是用 inject() [參考上篇Groovy 的 inject() method 應用] 來呈現 List 物件即可:
def (exp, amount, currency) = (money =~ regexp)?.inject([]) { res, itm ->
    res += itm
}
ps. 參考資訊 http://rosettacode.org/wiki/Return_multiple_values

2013年10月5日

GVM 與 GroovyServ

執行 .groovy code 不外乎要設定 PATH 與 CLASSPATH,以找到 interpreter(groovy)、compiler(groovyc) 與相關的 library;它往往需要在啟動的 shell 或 user profile 來加以處理。但,以系統維運的角度來說,則需要"安裝",以便整合至 environment 之中,維持系統固定、一致版本的 interpreter 與 compiler。以 ubuntu 系統為例:
sudo apt-add-repository ppa:groovy-dev/groovy
sudo apt-get update
sudo apt-get install groovy

groovy -version

Groovy Version: 1.8.6 JVM: ...
使得 groovy script 有如 shell script 一般,只要在 script 的第一行指出 interpreter 即可:
#!/usr/bin/bash
# this is a bash shell script

echo "1 + 2 = $((1+2))"

# ...
#!/usr/bin/env groovy
// this is a groovy script

println "1 + 2 = ${1+2}"

// ...

另一種安裝的選擇:GVM;它有如 linux 的 alternatives 來管理不同版本的 package 路徑一般 (alternatives 可用來安裝多版本 JDK),安裝後並指定 groovy 的版本。使得 groovy script 的第一行寫法保持一致、不受影響。

話說 language ,不免會被拿來與過去熟知的做比較;Java 也曾被"提過"很慢,但隨時代的演進這已是不再拿來作文章的話題了。近年來 JVM 上熱門的 dynamic language:Groovy ;也一樣引起話題:慢。

也就是說 script 會因功能的增加而 source code 越來越長,也造成編譯與執行的時間相對變長;這往往會造成 developer 的困擾。這種在編譯時期一樣有這"症頭"的還有 Scala;不過,人家它有 fsc;那 Groovy 呢?

還好出現了 GroovyServ。

以抓取某熱門森林遊樂區的訂房首頁的應用為例:
僅使用 groovy interpreter,執行的時間:
real 0m6.180s
user 0m4.875s
sys  0m0.271s
而搭配 GroovyServ 使用,在執行一次以後則:
real 0m1.445s
user 0m0.018s
sys  0m0.040s
看的出它的"效果"驚人!這對於 developer 的工作有相當大的幫助。並且, GVM 工具也支援安裝 GroovyServ,維護上相當方便。

ps.
停用 GroovyServ 的指令:
groovyserver -k

2013年8月19日

Upgrade subversion 至 v1.7.11

繼上篇解決 OSX 10.6 中安裝 SVN 與 Git 的 zlib 問題;為 upgrade subversion 至 v1.7.x 好讓 GGTSSubclipse v1.8 開發工具所使用,進行下列指令的處置:
cd /usr/local/Library/Formula/
brew update
brew versions subversion
git checkout 0060dc3 /usr/local/Library/Formula/subversion.rb
卻發生無法安裝情形:
處理的方法是改安裝 v1.7.11:
brew tap homebrew/versions
brew install --universal --java subversion17
這樣子只要再檢查 /Library/Java/Extensions 下有沒有相關的 symbolic link:
隨即開啟 GGTS 設定: [Preferences/Team/SVN] 下的 [SVN interface] 至 "JavaHL (JNI) 1.7.11 (r1503888)" 即可。

2013年8月7日

Java 的檔案傳輸

說到網路通訊,在一般 Java 的實作上多半會兩種方式: HTTP 或 RMI (當然也可寫個 socket server / client)。無論坊間的教育中心或書籍,都會有基本的範例;而且現今的 Internet 中也有很多成熟的 sample code (只要問對谷歌大神)。

那檔案傳輸呢:

一般提到 Java I/O 時,很多的 software developer 並不會對它陌生;遇到 HTTP 的檔案傳輸可使用如 Apache HTTPClient 這樣子的 package,以 multi-part 方式 (MultipartEntity) 來傳輸。

但個人有遇過 "咬" 住的測試經驗。於是乎搬出 PipedInputStream / PipedOutputStream 再加上啟用一個 Thread 來處理,就可以解決這個問題了。以 Groovy 為例:
...
def InputStream pipeOutput(Closure write) {
 def output = new PipedOutputStream()
 def input = new PipedInputStream(output)

 Thread.start {
  try{
   write(output)
   output.flush()

  } finally {
   try { output?.close() } catch (e) {/*nothing to do*/}
  }
 }
 return input
}
...
...
def ctn = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE)
ctn.addPart("zipFile",
 new InputStreamBody(
  pipeOutput { output ->
   Thread.currentThread().sleep(1000)
   inFile = new FileInputStream(fileName)
     output << inFile
  }
  ,'application/x-gzip'
  ,'test.gz'
 )
)
...
...
// HttpClient的處理 <略>
...
@@ (暈了嗎?)
那 RMI 呢? (想到就頭大?)

還好有 RMIIO 這個好心的 package (是 LGPL 哦,感動吧!感謝作者)
它的優點,就是如同它的說明所描述:  makes it as simple as possible to stream large amounts of data
我個人使用它的心得是,沒有 "咬" 住的情形。而且當初看上它的原因是 client code 與 server code 之間,不需要做 nofity 與 ack 這樣多餘的機制來傳檔與送回接到檔案的訊息:
A --- send ---> B
A <--- ack --- B

只要控制是否 throw RemoteException 以及 try / catch 這個 exception 即可。以 Java code 為例:
// 傳送端
// 用壓縮的 InputStream
try {
  ...
  RemoteInputStream remoteIs = exporter.export(new GZIPRemoteInputStream(fileInputStream));
  ...
  remoteObject.receiveData(remoteIs);
  ...
} catch ...
...
// 接收端
public void receiveData(RemoteInputStream remoteIs) throws RemoteException {
  ...
  InputStream is = RemoteInputStreamClient.wrap(remoteIs);
  ...
  if (somethingWrong) throw new RemoteException("some message");
  ...
  // InputStream / OutputStream 後續處理 
  ...
}

2013年8月2日

Java 使用 Acrobat Reader 列印 PDF

以 Java 要列印 PDF document 基本上來說並不是什麼大問題,無論是用 AWT PrintJob 或 print service 的 DocPrintJob。

但對於字體/字型的處理,除非買授權的商用套件或統一規範產生 PDF 時的方法,不然想要以 opensource package(ex. Apache PDFBox) 來處理 PDF 中各種造字字體/字型的問題,可以說是一件殺死腦細胞的工作。(雖然我已經幹了)

除此之外,最常見的解決方法是:利用 Windows 平台,以 Acrobat Reader 來印製;好處是,字型檔的處理與前端使用者的 PC 無異。 而程式通常使用 Runtime / Process 或 ProcessBuilder class 來 invoke Acrobat Reader。

不過,依過去發生的案例發現,大體上會發生幾個狀況:
  • 要不要處理 DOS 視窗輸出的訊息
  • 如何取得 return code
  • Acrobat Reader 因發生狀況而未結束 process

解決的策略是:[避免] 發生上述狀況, 取而代之的是去 [控制] 這些狀況
方法是 Java code ----> bat/cmd file ----> Acrobat Reader ;而在 bat/cmd 中加一些基本的處理:
  • 檢查 pdf file 是否存在
  • 檢查 printer (name) 是否存在

例如:
SET PDFFILE=%~1
...
IF NOT EXIST "%PDFFILE%" GOTO PDFNOTFOUND
...
GOTO END

:PDFNOTFOUND
echo file(%PDFFILE%) not found

:END
而檢查 printer 的方法:
rundll32 printui.dll PrintUIEntry /q /Xg /n"{printer name}"

結合上述判斷後再執行 Acrobat Reader 來列印:
"C:\Program Files\Adobe\Reader 10.0\Reader\AcroRd32.exe" /h /o /s /t /n "%PDFFILE%" "{printer name}"

但機車的事發生了,依 AcriRd32.exe 說明而加的參數,理當在執行它之後會結束 bat/cmd ,而回到 Java code 繼續處理;但事實並非如此,無論使用了什麼 cmd.exe 或 start 指令相關參數的功能。

目前唯一測試 OK 的方式是:

  • 在 Java code 運作之前,先啟用 Acrobat Reader 並把它縮小視窗擱在旁邊。

我只能想像的是 Acrobat Reader 是 Windows  program,要控制它除非拿到它的 handle,否則很難與 DOS 視窗的 process 結合,而該 process 只是負責 launch Acrobat Reader 而已。 XD

ps.
(1) Rundll32 printui.dll PrintUIEntry 適用平台 : http://technet.microsoft.com/zh-tw/library/ee624057(v=ws.10).aspx
(2) Apache PDFBox 近年來更新開始頻繁了,好事!

2013年8月1日

SwingBuilder 中佈建 panel

Groovy 使用 SwingBuilder 進行 panel 佈置可有 3 種手法:
(1) 逐一佈建
import groovy.swing.SwingBuilder
import java.awt.*
import javax.swing.*

def swing = new SwingBuilder()

swing.edt {
 frame (
  title: "Ex",
  defaultCloseOperation: JFrame.EXIT_ON_CLOSE,
  pack: true,
  show: true ) {
  
  borderLayout()
  panel(constraints: BorderLayout.CENTER) {
   
   borderLayout()
   panel(constraints: BorderLayout.PAGE_START) {
    label(text: 'header')
   }
   panel(constraints: BorderLayout.CENTER) {
    label(text: 'center 1')
   }
   panel(constraints: BorderLayout.PAGE_END) {
    label(text: 'footer')
   }
   
  }
 }
}
(2) 先簡單的佈建, 接著再佈置主要的 panel (以避免擁擠、縮排過深)
import groovy.swing.SwingBuilder
import java.awt.*
import javax.swing.*

def swing = new SwingBuilder()

swing.edt {
 frame (
  title: "Ex",
  defaultCloseOperation: JFrame.EXIT_ON_CLOSE,
  pack: true,
  show: true ) {
  
  borderLayout()
  panel(constraints: BorderLayout.PAGE_START) {
   
   borderLayout()
   panel(constraints: BorderLayout.PAGE_START) {
    label(text: 'header')
   }
   panel(constraints: BorderLayout.PAGE_END) {
    label(text: 'footer')
   }
   
  }.add(
   panel(constraints: BorderLayout.CENTER) {
    label(text: 'center 2')
   })
 }
}
(3) 先簡單的佈建, 事後再佈置主要的 panel
import groovy.swing.SwingBuilder
import java.awt.*
import javax.swing.*

def swing = new SwingBuilder()
def thePanel

swing.edt {
 frame (
  title: "Ex",
  defaultCloseOperation: JFrame.EXIT_ON_CLOSE,
  pack: true,
  show: true ) {
  
  borderLayout()
  thePanel = panel(constraints: BorderLayout.CENTER) {
   
   borderLayout()
   panel(constraints: BorderLayout.PAGE_START) {
    label(text: 'header')
   }
   panel(constraints: BorderLayout.PAGE_END) {
    label(text: 'footer')
   }
   
  }
...
...
  thePanel.add(
   panel(constraints: BorderLayout.CENTER) {
    label(text: 'center 3')
   })

 }
}

在 SwingBuilder 中建立 CardLayout 的方式

Groovy 透過 SwingBuilder 可以用 DSL 方式表述 swing 各種 components 的佈建。
而建立 CardLayout 有 3 種方式可以進行:
(1) 由 builder 產生
import groovy.swing.SwingBuilder
import java.awt.*
import javax.swing.*

def swing = new SwingBuilder()
def cardLayout = swing.cardLayout()
def thePanel

swing.edt {
 frame (
  title: "Ex",
  defaultCloseOperation: JFrame.EXIT_ON_CLOSE,
  pack: true,
  show: true ) {
  
  borderLayout()
  panel(constraints: BorderLayout.PAGE_START) {
   
   borderLayout()
   panel(constraints: BorderLayout.PAGE_START) {
    label(text: 'header')
   }
   panel(id: 'cards', constraints: BorderLayout.CENTER, layout: cardLayout).with {
    thePanel = it
   }
   panel(constraints: BorderLayout.PAGE_END) {
    label(text: 'footer')
   }
  }

  thePanel.add(
   panel(border: emptyBorder(5,5,5,5)) {
    label(text: 'card 1 label')
   }, 'card1')
  thePanel.add(
   panel(border: emptyBorder(5,5,5,5)) {
    label(text: 'card 2 label')
   }, 'card2')
  cardLayout.show(thePanel, 'card2')
 }
}
(2) new 產生
import groovy.swing.SwingBuilder
import java.awt.*
import javax.swing.*

def swing = new SwingBuilder()
def thePanel

swing.edt {
 frame (
  title: "Ex",
  defaultCloseOperation: JFrame.EXIT_ON_CLOSE,
  pack: true,
  show: true ) {
  
  borderLayout()
  panel(constraints: BorderLayout.PAGE_START) {
   
   borderLayout()
   panel(constraints: BorderLayout.PAGE_START) {
    label(text: 'header')
   }
   panel(id: 'cards', constraints: BorderLayout.CENTER, layout: new CardLayout()).with {
    thePanel = it
   }
   panel(constraints: BorderLayout.PAGE_END) {
    label(text: 'footer')
   }
  }

  thePanel.add(
   panel(border: emptyBorder(5,5,5,5)) {
    label(text: 'card 1 label')
   }, 'card1')
  thePanel.add(
   panel(border: emptyBorder(5,5,5,5)) {
    label(text: 'card 2 label')
   }, 'card2')
  thePanel.getLayout().show(thePanel, 'card2')
 }
}
(3) 由 container 進行 setLayout()
import groovy.swing.SwingBuilder
import java.awt.*
import javax.swing.*

def swing = new SwingBuilder()
def cardLayout
def thePanel

swing.edt {
 frame (
  title: "Ex",
  defaultCloseOperation: JFrame.EXIT_ON_CLOSE,
  pack: true,
  show: true ) {
  
  borderLayout()
  panel(constraints: BorderLayout.PAGE_START) {
   
   borderLayout()
   panel(constraints: BorderLayout.PAGE_START) {
    label(text: 'header')
   }
   panel(id: 'cards', constraints: BorderLayout.CENTER).with {
    setLayout(cardLayout = new CardLayout())
    thePanel = it
   }
   panel(constraints: BorderLayout.PAGE_END) {
    label(text: 'footer')
   }
  }

  thePanel.add(
   panel(border: emptyBorder(5,5,5,5)) {
    label(text: 'card 1 label')
   }, 'card1')
  thePanel.add(
   panel(border: emptyBorder(5,5,5,5)) {
    label(text: 'card 2 label')
   }, 'card2')
  cardLayout.show(thePanel, 'card2')
 }
}
ps. groovy 版本 v2.0.7

2013年7月13日

解決 OSX 10.6 中安裝 SVN 與 Git 的 zlib 問題

對於 Mac 平台的 Java 應用系統開發者來說,安裝與使用 SVN 或 Git 是工作的一環;網路資源有許多資訊可供快速建置,如:How to Install Xcode, Homebrew, Git, RVM, Ruby & Rails on Snow Leopard, Lion, and Mountain Lion

然而,對我這個 Snow Leopard 用戶來說,就沒這麼順利了;原因在於過去安裝過的 package 歴經硬碟換裝、備份、移除...等,有些開發工作上一些少用的東西不復存在,而且 Apple Developer 網站也不再有與 Snow Leopard 相關的檔案下載資源。直觀上自然會借助 Homebrew 工具來安裝 SVN 或 Git。然而卻出現以下情形:
沒有 zlib 啊~。據了解它就在那些沒有下載支援的 Command Line Tools 中,而早些時候 Xcode 安裝上卻未在預設之列。
索性,open source 還存在,很高興地前往下載:
咦?zlib 下載連結在哪裡?source 路徑還在呀(.../source/zlib/zlib-6),該不會要 file by file 下載吧?!
察看別的 source,路徑規劃的很一致(/source/cvs/cvs-42):
那看看它的下載路徑(.../tarballs/cvs/cvs-42.tar.gz):
就來比照辦理吧!在瀏覧器上輸入 URL:.../tarballs/zlib/zlib-6.tar.gz
成功下載了。接著就解開 zlib 壓縮檔、編譯與安裝:
cd zlib-6
make clean
./configure
make
make install
最後再重新用 brew 安裝 SVN 與 Git:

That's it!

但, 透過 brew 指令裝 subversion 沒有成功:
brew install --universal --java subversion
那只好自己手動解開 cached 下載檔 /Library/Caches/Homebrew/subversion-1.8.0.tar.bz2
接著:
make clean
./configure --prefix=/usr/local/Cellar/subversion/1.8.0 --with-apr=/usr/bin --with-zlib=/usr/local --with-sqlite=/usr/local/opt/sqlite --with-serf=/usr/local/opt/serf --disable-mod-activation --disable-nls --without-apache-libexecdir --without-berkeley-db --enable-javahl --without-jikes
make
make install-javahl
brew unlink subversion
brew link subverion
但不幸的是,v1.8.0 不適用於 OSX 10.6,做了白工。
而接下來的解法有兩種:
(1) 使用人家做好的 package 來安裝。(它會安裝在 /opt 下,而不是一般的 /usr/local 下)
(2) 下載 v.1.6.7 的 formula file:subversion.rb 放到 /usr/local/Library/Formular ;再做一次 brew install

別忘了檢查 /Library/Java/Extensions 下有沒有 libsvnjavahl-1.jnilib 的 symbolic link,沒有的話就自己建立,再設定 IDE (如:eclipse) 的 SVN interface 即完成: