2019-06-29

初步測試 tbroyer 版 plugin

TL;DR

目前找不到從 mojo 跳到 tbroyer 的理由,甚至有可能想跳也跳不過去… 囧rz

測試環境

  • JDK 1.8(但是 maven.compiler.* 是給 1.7)
  • Maven 3.2.1
  • GWT 2.7.0
  • GXT 3.1.0
  • guava-gwt 19.0
  • tbroyer Maven Plugin for GWT 1.0-rc-10

這兩天(終於)試了一下 tbroyer 版的 GWT Maven plugin,結果還蠻慘烈的。

首先是 GF 改成 tbroyer 版(據說 GMD 前陣子也改了,想跟風),把 pom.xmlpackage 改成 gwt-libmvn install 好像也沒啥用、source code 沒有跟著包進去,不確定到底發生了啥事情。因為只是個 library project,於是決定跳過,專心搞 app project。

拿了一個發展緩慢的 project 來試試看(aka 在改版之前是能正常 build 的),沒想到 SDM 開不起來,一直炸 NPE…

從官方文件上看不出異常,只好去他的 integration tests 跟 GMD demo project 裡頭找答案。盲目地一個一個把東西加上去之後,終於發現是 plugin.configuration 裡頭得設定 moduleName(也許 moduleShortName 也要?沒有詳測),即使 skipModule 已經給 true 了… =="

現在回頭檢討,其實官方文件 Usage 第一步就有說要設定(GWT)module,只是跟第四步的 generate-module 搞混了… [被毆飛]。這也產生第一個吐點:「mojo 版不用設定 GWT module」

事情還沒完,目前看起來 tbroyer 版在 generate-resources phase 只有作(個人認為不痛不癢)gwt.xml 的 generator。所以原本 mojo 版會幫忙生的 RpcServiceAsync 現在也沒了(看 integration tests 裡頭有 GWT RPC 的範例也是直接給 code),以此類推恐怕 I18N 也得自己搞了?

好,沒關係,即使我這個 GWT RPC 鐵粉也在考慮是不是該拋棄(尤其前陣子試 gwt-jackson 成功、而 AsyncCallback.onFailure() 從來沒處理過 XD),I18N 也不知道有沒有那個命去處理到,大不了在 JISS 裡頭也再搞個 code generator 嘛!這部份無視跳過!

結果還是繼續炸 error,而且這下真的死透了:

  • guava(FutureCountDownLatch)用到 java.lang.InterruptedException 但是沒有 source code
    • 而且見鬼了,拿來測試的 project 裡頭只有因為 GF 宣告而 inherit com.google.common.base.Base,所以應該沒 inherit 到 Concurrent 的東西…
  • GXT 的 XmlReader.XmlSplittable 有 abstract method 沒實作?

立馬切回去 mojo 版,還是可以正常開啟 SDM… WTF?一個 GWT compiler 各自表述?去狗了一下 GXT 有沒有災情… 沒找到,倒是發現 GXT 4.x 文件給的 archetypes 依然還是在用 mojo 版…

這下也懶得再去找看看是不是改個設定 or 版本就能解決,直接宣告放棄。

結論

整體看起來還是 mojo 版比較實在,tbroyer 版看起來比較像是一個理想崇高但是缺乏廣泛使用回饋的產物?

也許哪一天徹底跟 GWT RPC 以及 GXT 說掰掰再來考慮?是說那時我能理解 tbroyer 版的優點嗎? Orz

2019-06-04

JSON 日期碎碎念

最近(終於)在測 gwt-jackson,負責在 server side 噴 JSON 的是 Gson,然後測到日期(java.util.Date)的時候炸了一輪,所以來碎念一下留個紀念… XD

一開始以為是 gwt-jackson 炸掉,畢竟 GSON 資格老關係好(?),GWT 現在還有多少人在用都是個問題 lol。但是狗了一圈沒發現啥災情,事情開始有點詭異…

那找「gson date」試試看… WTF?GSON 預設的日期格式會隨平台不同而變?這就別提什麼 W3C 之類的標準了,根本亂搞一通嘛… =="

不過至少有解,就是可以用 new GsonBuilder().setDateFormat() 來設定 format 然後用這個 builder 來建立 Gson instance。

那麼,什麼是 JSON 標準定義的日期格式呢?根據這個 stackoverflow 的說法,JSON 根本沒做出定義… =="

好吧,至少 JS / W3C 貌似有,就是 ISO-8601

因為懶得查,所以直接狗「java dateformat 8601」,我的搜尋結果第一條是 stackoverflow,不過那是要把字串還原回 Date,不太對題。第二條跟第三條(居然)是簡體中文,都是對岸的 CSDN。

先說第三條,文章裡頭給的 pattern 是 yyyy-MM-dd'T'HH:mm:ss.SSS'Z',hmmm… 看起來跟前面 stackoverflow 附的範例一樣,丟下去跑也能正常 parse,太好了可以收工了… 才怪… 得到時間不對,整整多了 8 小時。WTF?時區問題?

算啦算啦… 去測第二條吧,直接拿文章內說可行的 yyyy-MM-dd'T'HH:mm:ss:SSSZZ 來試試看… 連 parse 都過不了?WTF?等等,為什麼反覆測試下發現有時候會 parse 過、而且值也正確?但有時候卻又 parse 不過?幹這是七月半提前報到嗎?

鬼打牆了 n 分鐘之後,終於發現… 測試會過的時候是測完第三條之後把 Z 前後的單引號拔掉,而測試不過的時候是直接複製第二條的文字…

對,X 他 X 的第二個給的 pattern 根本有問題,按照 ISO-8601 的規格,在 sec 跟 ms 中間應該是「.」而不是「:」。肉眼不仔細比對根本很難發現… Orz

到了這個時候,真的是被嚇到了,乖乖回頭看文件。在 Java API 的定義當中,「X」跟「Z」都表示時區,「X」是 ISO-8601 格式、「Z」是 RFC-822 格式(不過目前看不出差異)。ISO-8601 要求日期與時間之間以一個寫死的大寫「T」連結,寫成 Java 的 pattern 就要前後加上單引號。所以第三條 pattern 尾巴的「Z」是寫死不變的。那,為什麼 parse 會過呢?

ISO-8601 當中規定,如果時區剛好是 UTC(零時區)就顯示 Z,所以得到的是合法的字串,但是除非人在 UTC 不然解出來的實際 Date 就會有時差問題… Orz

心得與結論:

  • 即使有名有號的 library 還是有可能存在奇妙的行為。即使到現在我還是無法替 Gson 在日期上的作法想出任何合理的理由…
    • 寫這篇文章的時候認真找了一下,原來 Gson 有開過 issue-281 還 close 了,整個看起來有點莫名而且最後路人們還是在用 GsonBuilder 這招? WTF?
  • google 出來的結果未必可信,即使排名在很前面。stackoverflow 上也是。像這個解答還有 62 個推,底下也只有一個人指出這是不對的答案。
  • 雖然說官方文件也不是不會出錯,但相比來路不明的 blog,機率還是低的多(雖然啃起來也難過很多)

2019-04-05

GF TextUtil debug 雜記

自從在前前公司接觸 GXT、接著把 Chart 的底層 DrawComponent 給翻了一遍之後,就對這玩意很感興趣,之後陸陸續續以 DrawComponent 為基礎搞了一些東西出來。去年終於搞了一個 GF 版的 TextButton 出來。TextButton重點 惡搞之處在於文字的字體會隨著整體大小而自動調整。好不好用很難說,自己是頗為得意啦… 囧>,因為算是集大成之作:

  • 驗證了 GF Layer 機制的可用性
  • 處理 TextSprite 在視覺上的 y 軸位移問題(雖然沒有相對正統地用後來搞出來的 FontMatrics 來校正 XD)
  • 大幅解決效率問題(因為發現有 Sprite.redraw() 而不用每次都搞 DrawComponent.redraw()

不過實務上陸陸續續有炸出一些問題,在某些狀況下初始的字體並沒有變成正確的大小。但是因為一直都能 workaround 掉,所以沒提煉 SSCCE、自然也沒深究

2015-11-25

Git repo 之間的 sync 操作步驟

這篇文章是 GitHub 作為 repo host,不過下列操作都沒有 depend on GitHub。或著說,就是不希望用 GitHub 的功能,所以才要有這些步驟,不然 pull request 應該可以解決大部分的狀況。

2015-11-17

試論述 Listbox 的 check 與 Image id 的關係

ZK 版本:6.5.3

故事是這樣開始的:畫面上有一個設定 checkmark=true 的 Listbox。每個 item 除了自動生出來的 checkbox 之外就是 Image,src 是用 data URI(所以基本上沒有 loading 時間的問題),也有設定 height 讓 Listbox 不會長得太可怕。也因此,使用者希望點圖片可以看到夠大的圖、但是又不希望出現 scroll bar。

所以當使用者點圖片的時候,就用 Executions.createComponents() 製造出一個 Window,然後設法讓圖片等比例縮小到長或寬撐滿整個 Window。Window 的程式碼長這樣(VM 就不附了):

<window apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('ScaleImageViewModel')" 
    closable="true" title="Image" width="90%" height="90%">

    <vlayout vflex="1" hflex="1">
        <div id="cave" vflex="1" hflex="1" style="overflow:auto;">
            <image id="image" src="@load(vm.image)" />
        </div>
        <hlayout hflex="1">
            <button label="Fit Window" w:onClick="resetRatio()" />
            <slider id="scale" hflex="1" w:onScroll="changeRatio()"/>
            <button label="Pre." onClick="@command('prev')" disabled="@load(vm.start)"/>
            <button label="Next" onClick="@command('next')" disabled="@load(vm.end)"/>
        </hlayout>
    </vlayout>
    <script defer="true"><![CDATA[
    zk.Widget.$("$cave").onAfterSize = function() {
        this.$super("onAfterSize");
        resize();
    }

    scaleWgt = zk.Widget.$("$scale");
    caveElmt = $("$cave")[0];
    imageElmt = $("$image")[0];
    imageOriginWidth = imageElmt.width;
    imageOriginHeight = imageElmt.height;

    resetRatio();
    ]]></script>
</window>

<script><![CDATA[
    var scaleWgt;
    var caveElmt;
    var imageElmt; 
    var imageOriginWidth;
    var imageOriginHeight;
    var ratio;

    function resetRatio() {
        var wRatio = caveElmt.offsetWidth / imageOriginWidth;
        var hRatio = caveElmt.offsetHeight / imageOriginHeight;
        setRatio(
            Math.min(
                (wRatio > 1) ? 1 : wRatio,  //不能比 100% 還大,所以最多壓成 1
                (hRatio > 1) ? 1 : hRatio
            )
        );
        scaleWgt.setCurpos(ratio * 100);
    }

    function resize() {
        imageElmt.width = imageOriginWidth * ratio;
        imageElmt.height = imageOriginHeight * ratio;
        console.log("[after] " + imageElmt.width + "x" + imageElmt.height);
    }

    function setRatio(value) {
        ratio = Math.min(1, value); //不能比 100% 還大,所以最多壓成 1
        resize();
    }

    //其實應該要可以直接 call setRatio(),只是 ZK 我實在...
    function changeRatio() {
        setRatio(scaleWgt.getCurpos() / 100);
    }
]]></script>

我想盡量讓事情都只停在 client side 就處理完成,是說我也不確定調整圖片大小這件事情在 server side 能不能順利解。總之,程式碼可能不夠精煉,而且有些莫名其妙地方是靠 trial and error 得到的可行解、完全不明所以…… 只能說我對 ZK 完全沒有愛,能在零零落落毫無章法的 ZK 文間當中湊出這些,我都覺得減壽三天了…… Zzz

好的,前情提要終於講完了,要開始進入正題了。用實際資料測試的時候發現,有些圖片就是無法改變大小(slider 也沒反應)。原本以為是圖片的問題,但是拿 data URI 的值直接塞 browser 能得到正確的 size……

在經過一番折騰之後,終於發現規律:

Listbox 有一個(以上)item 勾選,就會無法改變大小

既然確定是 ZK 搞出來的問題,那要懷疑的東西就少了一點。又一輪的 trial and error 後,終於發現:Image 的 id 不能是 “image”,不然 $("$iamge") 不知道會 select 到哪個 element 去。

是的,只要換個 id,一切統統都沒事了(為了保險起見,我試過 fullImagescaleImage,不保證其他值不會有問題 Zzz)。

WTF?這到底什麼鬼東西?為什麼 Listbox 有沒有 item 被勾選會影響到 $("$image") 的結果?那個 node 還是在 Window 裡頭(不是說 Window 有自己的 id space?)?而為什麼 ZUL 設定的 id 值對應到 JS / DOM 當中居然不是 unique 的?以後開發人員要自己讓 id 值 unique 嗎(不然有 bug 連要從哪裡開始懷疑起都不知道?)

雖然平常就有在 blame ZK,但是沒遇到過這麼令人傻眼的,這件事情足足讓我笑了十分鐘… lol

教練我想寫 GWT…… [淚目]


後來要修正其他功能的 bug,所以重寫了一次,關鍵部份還是一樣,只是程式碼變得清爽一點,茲更新如下:

<window apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('ScaleImageViewModel')" 
    closable="true" title="Image" width="90%" height="90%">

    <vlayout vflex="1" hflex="1">
        <div id="cave" vflex="1" hflex="1" style="overflow:auto;">
            <image id="scaleImage" src="@load(vm.image)">
                <attribute w:name="setSrc">
                function (src) {
                    this.$setSrc(src);
                    resetRatio();
                }
                </attribute>
            </image>
        </div>
        <hlayout hflex="1">
            <button label="Fit Window" w:onClick="resetRatio()" />
            <slider id="scale" hflex="1" w:onScroll="changeRatio()"/>
            <button label="Pre." onClick="@command('prev')" disabled="@load(vm.start)"/>
            <button label="Next" onClick="@command('next')" disabled="@load(vm.end)"/>
        </hlayout>
    </vlayout>
    <script defer="true">
    //第一次 Image.setSrc() 不會真正 resetRatio(),所以得在 defer 的 script 指定來一次
    resetRatio();
    </script>
</window>

<script><![CDATA[
function resetRatio() {
    var imageElmt = $("$scaleImage")[0];

    if (!imageElmt) { return; }

    var tempImage = new Image();
    tempImage.src = imageElmt.src;

    var caveElmt = $("$cave")[0];
    var wRatio = caveElmt.offsetWidth / tempImage.width;
    var hRatio = caveElmt.offsetHeight / tempImage.height;
    setRatio(
        Math.min(
            (wRatio > 1) ? 1 : wRatio,  //不能比 100% 還大,所以最多壓成 1
            (hRatio > 1) ? 1 : hRatio
        )
    );
    zk.Widget.$("$scale").setCurpos(ratio * 100);
}

function resize() {
    var imageElmt = $("$scaleImage")[0];
    var tempImage = new Image();
    tempImage.src = imageElmt.src;
    imageElmt.width = tempImage.width * ratio;
    imageElmt.height = tempImage.height * ratio;
}

/** value 的值域理論上是 [0,1] **/
function setRatio(value) {
    ratio = Math.min(1, value); //不能比 100% 還大,所以最多壓成 1
    resize();
}

//其實應該要可以直接 call setRatio(),只是 ZK 我實在無法... Orz
function changeRatio() {
    setRatio(zk.Widget.$("$scale").getCurpos() / 100);
}
]]></script>