2016年12月18日 星期日

AngularJS中使用$http的POST不要傳送JSON格式給JSP, Servlet的方法

AngularJS 在用$http的angularjs service傳POST時,參數預設會用JSON的格式傳至url指定的後台,例如如果用以下的傳送方式:
$http({
   method: 'POST',
   url: 'xxx.do',
   data: {param1: "param1", param2: "param2"}
  })

後台例如Servlet會得到一串JSON格式的文字,像是 {'param1':'param1, 'param2' : 'param2},
但通常這並不是我們想要的key-value的型式,如果想要傳遞key-value型式的話,必須要用到
$httpParamSerializer這個angularjs service,並且設定header (不設定的話預設的Content-Type是application/json;charset=utf-8),使用方法如下:

$http({
   method: 'POST',
   url: 'xxx.do',
   data: $httpParamSerializer({param1: "param1", param2: "param2"}),
   headers: {
       'Content-Type': 'application/x-www-form-urlencoded'
   }
  })


這樣AngularJS就會以param1=param1&param2=param2的方式傳至後台了。

2016年12月12日 星期一

JQuery Ajax傳遞陣列時參數名不要帶中括弧的方法

JQuery的Ajax如果使用POST的話,似乎在1.4+版本後都會把陣列的參數名加上中括弧,例如以下:
$.ajax({
 method: "POST",
 url: "/xxx.do",
 data: {
    arrayParameter : ['1','2','3','4','5']         
 }
});

預設arrayParameter會以arrayParameter[]的參數名,值為1,2,3,4,5的方式傳進url指定的後台,例如果用Servlet去接的話就要用
String[] arrayParameter = request.getParameterValues("arrayParameter[]");
才能接到。
為了能夠讓JQuery傳的陣列參數不要帶上中括弧,必須指定ajax的traditional為true,例如:

$.ajax({
 traditional: true,
 method: "POST",
 url: "/xxx.do",
 data: {
    arrayParameter : ['1','2','3','4','5']         
 }
});

或是設定全局設定,讓每個ajax都能使用traditional的參數傳遞方式:
$.ajaxSetup({traditional: true});

參考資料:

  1. How do I remove the square brackets at the end of a JS variable name during AJAX calls?

2016年12月11日 星期日

Angular2簡易安裝使用 - Eclipse + Typescript plugin + System.js

Angular2的配置比起它的前一代AngularJs來說,稍微麻煩一點,因為涉及了typescript和module的使用。

在這裡我要介紹在Eclipse的開發環境中,以一個簡單的Angular2範列來說明如何簡單的在Eclipse中設置Angular2的初始設置。Typescript使用了Eclipse的typescript外掛、module使用了System.js。


需準備的步驟如下:

  1. node.js因為等下會使用到npm (Angular CLI),並且Eclipse的Typescript也需要用到node.js的功能,所以我們要先安裝node.js。
    node.js官網
  2. git for windows
    因為Angular CLI會需要用到git,所以需安裝windows版本的git。
    windows版本下載
  3. Angular CLI (選用)
    一個對於建置Angular2專案非常方便的工具,不過這裡為了要學習自己建置,所以只利用了Angular CLI來取得Angular2所需的modules,取得modules不一定要用Angular CLI,所以為選用。
    Angular CLI官方網站
  4. system.js
    這裡我們使用了system.js來使用javascript module,但也可選擇其他的module工具,例如webpack

取得Angular2所需的modules
我們可以利用node.js的npm來安裝Angular2所需的modules (會放到node_modules資料夾中),
但這裡我們也可以利用Angular CLI來簡單取得modules,
安裝好Angular CLI後,可以在命令列視窗中使用
ng new XXX
來在當下資料夾中建立Angular專案,在建立好的專案資料中,可以
找到Angular2所需的modules已經都下載後放在node_modules資料夾中

Eclipse (這裡安裝的是neon版本) + Typescript plugin
Help --> Install New Software...
--> 加入一個連結 : http://eclipse-update.palantir.com/eclipse-typescript/
下載安裝Eclipse的Typescript plugin

建立我們的專案
以下是我們的檔案結構圖

其中紅色框裡的是我們會建立的,藍框則是Typescript plugin幫我們編譯產生的。
在這裡Typescript plugin不是也不需使用如tsconfig.json的設定檔,而是直接在Eclpse裡設定,
對著專案按右鍵,選擇 Properties --> Typescript 可以看到如下畫面,其中
Source folder(s): ts檔放置的位置
Exported folder(s): 在 import module時,預設的路徑位置,例如node_modules之下的import路徑不用再打node_modules,預設會先重這裡找
Output folder: ts編譯成的js檔要放置的位置
接著設定 Compiler,這邊可以位照下面的設定就好,紅色框是比較重要的設定
再來就可以開始製作Angular2的範例了。

/index.jsp:
<%@ page language="java" contentType="text/html; charset=BIG5" pageEncoding="BIG5"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=BIG5">
<title>Insert title here</title>

<script src="node_modules/core-js/client/shim.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="app/js/system.js"></script>
<script>
 System.config({
  defaultJSExtensions: true,
  paths: {
   'app/*': './app/*',
   '@angular/*': './node_modules/@angular/*',
   'rxjs/*': './node_modules/rxjs/*'
  },
  packageConfigPaths: ['./node_modules/@angular/*/package.json']
 });
 SystemJS.import('app/js/index.js');
</script>
</head>
<body>
 <my-app>Loading...</my-app>
</body>
</html>
其中Angular所需的shim.js、Reflect.js、zone.js雖然有被放在node_modules中,但因為它們並沒有寫成modules的型式,所以要直接用<script>來載入。
在這邊我們使用了system.js來載入module,System.config()設置了module的載入設定,以下說明:
defaultJSExtension: 如果為ture,則import module時可以不用寫副檔名,預設為js
paths:規定了module查找路徑,需注意的是在這裡除了使用了@angular之下的module,還有rxjs之下的module。
packageConfigPaths:預設先去尋找指定路徑資料夾內的package.json設定檔,如果找到了main屬性,以main指定的js檔案為要import的module

設定完後,用SytemJS.import()來載入此頁要用的,一開始的js程式。

/ts/index.ts:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

/ts/app.module.ts:
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }   from './app.component';
@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

/ts/app.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  template: '<h1>My First Angular2!!! App</h1>'
})
export class AppComponent { }


最後啟動Server,瀏覽index.jsp,應該就可以看到頁面一開始有Loading...字樣,然後很快變成My First Angular2!!! App的字的效果了
原始碼下載:

2016年11月28日 星期一

如何在Java上使用 pngquant png壓縮工具 (JNI)

pngquant是一個不錯的png圖檔縮工具,但它是用C語言寫成的,如果要用Java來使用它的話,就必需使用JNI (Iava Native Interface)的方式來調用C的程式,剛好工作上需要在Java的環境上使用它,特別在這紀錄下要使用pngquant的JNI流程。

pngquant的官網上有提供圖形化GUI的工具及命令列的Command-Line工具,如果要用程式碼的使用方式的話,可以點 "a library" 連結到它的 lib 網站 (libimagequant),在lib網站中,可以看到許多不同語言的使用方式。

我的環境是Windows 7,jdk1.5.0_15,必須製作相應libimagequant專案的dll檔。
制作dll檔有許多方式,libimagequant官網給Windows的建議為使用Viusal Studio或MinGW工具來產生dll,我選擇最簡單的方式,去Microsoft官網下載安裝Visual Studio 2015來用。

點擊 "MSVC-compatible branch of the library" 可連到MSVC版lib的Git網站,把整個專案下載下來後,就可以開始進行JNI的步驟。

以下是JNI的詳細步驟:

  1. 使用命令列視窗(cmd)到下載並已被解壓縮的libimagequant-msvc資料夾,打上以下指令來產生相應PngQuant.class、Image.class、 Result.class (Class檔請自行compile產生)的 標頭檔 (副檔名為h)
    javah org.pngquant.PngQuant
    javah org.pngquant.Image
    javah org.pngquant.Result
    產生出來的檔名會被多加一些前綴,把它們都刪掉,再把得到的pngQuant.h、Image.h、Result.h丟到org/pngquant的目錄下。
  2. 開啟Visual Studio 2015,New 一個 Win32 Project,Application type選 DLL ,Additional options選Empty Project就好。
  3. 先把org/pngquant/PngQuant.c丟到org外面那層(因為要配合裡面寫的include的path路徑),在Solution Explorer的Source File中,將全部檔(包括org資料夾之下)都Add進去(有些其實用不到,不過沒關係都先丟進去)
  4. 這時看PngQuant.h可能會發現有被畫紅線的Error,對專案安右鍵選擇"Properties" --> Configuration --> C/C++  設定 Additional Include Directories ,請設定jdk中include和include/win32的位置,例如以下路徑:
    C:\Program Files (x86)\Java\jdk1.5.0_15\include
    C:\Program Files (x86)\Java\jdk1.5.0_15\include\win32
    然後將專案的mode從debug改成release
  5. 開啟Eclipse,建立一個測試專案,將org/pngquant下的java檔全部丟到專案中,並且再將Visual Studio release出來的dll檔丟進去。
    撰寫一個測試的程式,例如叫做libimagequantTest.java,內容如下:
    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.io.IOException;
    
    import javax.imageio.ImageIO;
    
    import org.pngquant.PngQuant;
    
    public class libimagequantTest {
    
     public static void main(String[] args) {
       BufferedImage newImg;
       
       PngQuant pngQuant = new PngQuant();
       
       try {
        newImg = pngQuant.getRemapped(ImageIO.read(new File("D:\\old_picture.png")));
        ImageIO.write(newImg, "png", new File("D:\\new_picture.png"));
       } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
       }
       
       pngQuant.close();
     }
    
    }
    
    最後的檔案結構會像這樣
  6. 在專案上按右鍵選擇Properties --> Java Build Path 設定 Nativ Library Location,例如此例我設定的是
    libimagequant_Test/src/org/pngquant
  7. 最後放上一張檔案大小較大的圖 D:\\old_picture.png,執行libimagequantTest.java,如果有出現另一張圖D:\\new_picture.png,就代表大功告成了。
源始碼下載:
pngquant JNI_Test.7z

2016年11月27日 星期日

Ardulink - Arduino的Java控制方案

Ardulink是一個開源Arduino的Java解決方案,提供了使用Java來控制Arduino的方法,讓Java操作Arduino版子變的更簡單,其源裡是先再Arduino上寫入程式,用來監聽從序列阜(Serial Port)上傳來的指令並做相應的操作、也用序列阜傳回相應的回應,序列阜的連接方式用到了之前文章"用Java與Arduino的序列阜溝通(使用RXTX)"講到的RXTX,而Java就用包好的API來對Arduino送指令。

要使用Ardulink,可以先到Ardulink的下載頁下載,會得到一個壓縮檔,找個位置解壓縮後就可以開始進行Ardulink的Java範例,LED閃光(BlinkLED),下面講解詳細步驟:
  1. 下載Ardulink並解壓縮以後,可以看到如下的檔案結構。
    我們會需要用到的有"winDLLs"及"lib"資料夾。
    打開winDLLs,可以看到32bit和64bit兩個資料夾,依自已需求選擇打開資料夾後可以再看到三個檔案:LibusbJava.dll、RXTXcomm.jar、rxtxSerial.dll
    LibusbJava.dll及rxtxSerial.dll是Java使用JNI要用的檔案。
    RXTXcomm.jar是Java要引入lib的其中一個檔案。
    lib資料夾裡的檔案是要給Java要引入的lib。
    而在bin資料夾裡面,放了兩個bat檔,其內容就只是幫忙把LibusbJava.dll、RXTXcomm.jar、rxtxSerial.dll放到lib資料夾而已。
    為了之後方便且起,在這裡我也把LibusbJava.dll、RXTXcomm.jar、rxtxSerial.dll通通放到lib資料夾。
  2. 接著是Arduino這端,我們必需要先再Arduino版子上寫入Serial Port溝通相關的程式,打開sketches資料夾,可以看到許多角本,這邊選擇ArdulinkProtocol裡的ArdulinkProtocol.ino執行就好,當然如果有相應的版子也可以選擇相應的角本,例如Digispark可以選擇ArdulinkProtocol4Digispark。
    需要注意的是,也為不同型號的版子有些許不同,例如Arduino Due目前沒支援tone()和noTone()這兩個函式,所以要稍微修改角本,因為我使用的是Arduino Due,為了簡單測試方便,就先把用到tone()和noTone()的指令先注解掉。
  3. 再來是Java這端,這裡以Eclipse Neon IDE及jdk1.8.0_05為例,檔案結構如下,
    BlinkLED是我們的主程式,用來控制Arduino的LED燈開闗(第13號PIN),先把剛剛lib資料夾裡的jar檔及RXTXcomm.jar引入library中。
    接著設定專案的Native library location,路徑為LibusbJava.dll、rxtxSerial.dll所在的路徑。
    BlinkLED.java的內容如下(參考https://github.com/Ardulink/Ardulink-2),作用為一秒亮、一秒暗Arduino板子的LED燈(Pin 13)。
    import java.io.IOException;
    import java.util.concurrent.TimeUnit;
    
    import org.ardulink.core.Link;
    import org.ardulink.core.Pin;
    import org.ardulink.core.Pin.DigitalPin;
    import org.ardulink.core.convenience.Links;
    
    public class BlinkLED {
    
     public static void main(String[] args) {
          Link link = Links.getDefault();
             DigitalPin pin = Pin.digitalPin(13);  //這裡13 pin是板子的LED燈
             boolean power = true;
             while (true) {
                 System.out.println("Send power:" + power);
                 try {
                  //swicthDigitalPin()為數位輸出,對應了Arduino的digitalWrite()
         link.switchDigitalPin(pin, power);  
         power = !power;
                  TimeUnit.SECONDS.sleep(1);  //1秒亮、1秒暗
        } catch (IOException | InterruptedException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
        }             
             }
     }
    }
    
  4. 執行BlinkLED.java後,應該就可以看到Arduino板子上的LED燈一亮一暗了。
    執行時可能會有下警告訊息,主要是RXTX版本及SLF4J Class衝突等警告,不過並不會影響程式運行,所以可暫且忽略。
    Stable Library
    =========================================
    Native lib Version = RXTX-2.2-20081207 Cloudhopper Build rxtx.cloudhopper.net
    Java lib Version   = RXTX-2.1-7
    WARNING:  RXTX Version mismatch
     Jar version = RXTX-2.1-7
     native lib Version = RXTX-2.2-20081207 Cloudhopper Build rxtx.cloudhopper.net
    SLF4J: Class path contains multiple SLF4J bindings.
    SLF4J: Found binding in [jar:file:/C:/Users/Administrator/Downloads/ardulink/lib/slf4j-jdk14-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
    SLF4J: Found binding in [jar:file:/C:/Users/Administrator/Downloads/ardulink/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
    SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
    SLF4J: Actual binding is of type [org.slf4j.impl.JDK14LoggerFactory]



附注:
Ardulink有做一個UI介面可以跟Arduino板子作連接並控制(ardulink-console-2.0.1.jar),放在下載下來的 lib/ardulink-console-2.0.1.jar,在執行set32bitWindowsRXTX.bat或set64bitWindowsRXTX.bat(或自己手動把LibusbJava.dll、RXTXcomm.jar、rxtxSerial.dll丟到lib資料下)後,可以直接開啟ardulink-console-2.0.1.jar,看到如下視窗介面,按下Connect與Arduino連接後,在許多功能可以使用,對於想要測一下Arduino,還不想花太多時間寫程式時是個不錯的工具。

2016年10月2日 星期日

JForum advanced search照日期排序的bug

JForum的Search的功能似乎有些bug,在search時如果選擇以date來做排序的話,會不管選正排還是逆排,結果都是沒有照date來排序,事實上它總是以(id)主鍵來排序的,應該算是一個bug。

經過追蹤程式碼及上網搜尋相關資料後,找到了bug的產生原因,特別在這邊做個紀錄並提供一個個人的解決方法。

bug發生原因:
JForum使用了Lucene來實做文章(Post)檢索搜尋功能,Lucene的實做是沒有問題的,在search時選擇以date來做ASC及DESC排序時,Lucene的確是返回正確排序的post_id列表,但是JForum在用它自己寫的Sql語法去找對應這些post_id的Post detail詳細資料時,使用了IN [post_id]的寫法,造成了找出Post detail後,排序又變成以主鍵排序的順序了。

我們先來看一下出問題的地方,首先是
GenericLuceneDAO.java的getPostData(int[] postIds)方法,輸入參數postIds是Lucene返回的以正確排序的post_id,但在要取得對應的Post detail時,使用了SearchModel.getPostsDataForLucene這個Sql語法,這個語法寫在generic_queries.sql,內容為

SearchModel.getPostsDataForLucene = SELECT p.post_id, p.forum_id, p.topic_id, p.user_id, u.username, p.enable_bbcode, p.enable_smilies, p.post_time, pt.post_subject, pt.post_text, t.topic_title \
 FROM jforum_posts p, jforum_posts_text pt, jforum_users u, jforum_topics t \
 WHERE p.post_id IN (:posts:) \
 AND p.post_id = pt.post_id  \
 AND p.topic_id = t.topic_id \
 AND p.user_id = u.user_Id


可以看到在Sql語法中用了 p.post_id IN (:posts:) 這樣的語句( :posts: 字串會被替換掉),所以不管post_ids怎麼排序,Sql語法回傳的結果還是照著主鍵排。

我的解法是修改GenericLuceneDAO.java的getPostsData()方法,在Sql語法查完post detail後,再將它照著postIds的順序重新丟到一個新的ArrayList回傳,這樣這個方法回傳的結果就可以被修正成正確的了。

原本有bug的程式
public List getPostsData(int[] postIds)
 {
  if (postIds.length == 0) {
   return new ArrayList();
  }
  
  List l = new ArrayList();
  
  PreparedStatement p = null;
  ResultSet rs = null;
  
  try {
   String sql = SystemGlobals.getSql("SearchModel.getPostsDataForLucene");
   sql = sql.replaceAll(":posts:", this.buildInClause(postIds));
   
   p = JForumExecutionContext.getConnection().prepareStatement(sql);
   rs = p.executeQuery();
   
   while (rs.next()) {
    Post post = this.makePost(rs);
    post.setPostUsername(rs.getString("username"));
    
    l.add(post);
   }
  }
  catch (SQLException e) {
   throw new DatabaseException(e);
  }
  finally {
   DbUtils.close(rs, p);
  }
  
  return l;
 }


修正後的程式
public List getPostsData(int[] postIds)
 {
  if (postIds.length == 0) {
   return new ArrayList();
  }
  
  List l = new ArrayList();
  Post[] posts_orderByLucene = new Post[postIds.length];
  
  PreparedStatement p = null;
  ResultSet rs = null;
  
  try {
   String sql = SystemGlobals.getSql("SearchModel.getPostsDataForLucene");
   sql = sql.replaceAll(":posts:", this.buildInClause(postIds));
   
   p = JForumExecutionContext.getConnection().prepareStatement(sql);
   rs = p.executeQuery();
   
   while (rs.next()) {
    Post post = this.makePost(rs);
    post.setPostUsername(rs.getString("username"));
    
    for (int i = 0; i< postIds.length ; i++){
     if (postIds[i] == post.getId()){
      posts_orderByLucene[i] = post;
      break;
     }
    }    
   }
   l = Arrays.asList(posts_orderByLucene);
  }
  catch (SQLException e) {
   throw new DatabaseException(e);
  }
  finally {
   DbUtils.close(rs, p);
  }
  
  return l;
 }


參考資料:

  1. JForum 搜索时按时间排序的问题解决
  2. Bug for searching with descending order

Google Chrome Web Push (客製化訊息 - 需加密)

Google Chrome Web Push是Google的一項服務,可以讓管理者向User主動推播訊息,
跟之前介紹的對Android GCM推播很像(如何向GCM Server傳送資料如何接收GCM Server發送的Registration ID訊息,以php、Java及JSP為例,以php、Java及JSP為例),事實上Google Chrome Web Push目前也是利用GCM服務來實現的。

Google Chrome Web Push可以寫死彈跳訊息的code在client端(註冊時會記錄),也可以動態訊息給client,不過因為安全的問題,必須使用Google規定的方式來加密訊息。

在這邊就來一步步展示如何做一個簡單的Google Chrome Web Push實現:

2016年9月28日 星期三

ByteArrayOutputStream - Java - close 後可取出資料的OutputStream

ByteArrayOutputStream繼承OutputStream,但跟平常看到的例如FileOutputStream、ServletOutputStream等不同,它是可以在資料輸出至OutputStream後、甚至在被close()後,還可以將資料取回的一種OutputStream,例如可用其擁有的方法,toByteArray(),來取得byte[]型別的資料。

例如以之前的文章為例,使用Java製作GIF動畫 - AnimatedGifEncoder,其中
OutputStream o = response.getOutputStream();
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
encoder.start(o);
o.flush();
o.close();

也可寫成:

ByteArrayOutputStream oo = new ByteArrayOutputStream();
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
encoder.start(oo);  //
/* some code */
oo.flush();
oo.close();

byte[] gif_btyeArray = oo.toByteArray();
/* you can do something with gif_byteAray */
OutputStream o = response.getOutputStream();
o.write(gif_btyeArray );
o.flush();
o.close();

可以看到我們先把輸出的資料先送到ByteArrayOutputStream裡,在其close()後,還可以利用ByteArrayOutputStream的toByteArray()取回資料,對資料做想要的處理之後,再送給ServeltOutputSteam

參考資料:
  1. java.ByteArrayInputStream与ByteArrayOutputStream再次理解http://blog.csdn.net/rcoder/article/details/6118313
  2. How can we read or use the contents of outputstream [closed]

2016年9月27日 星期二

使用Java製作GIF動畫 - AnimatedGifEncoder

今天要來介紹一個對於用Java生成動態GIF特別好用的類別, AnimatedGifEncoder,
由Kevin Weiner所撰寫,

下面以一個簡單的例子來演示如何使用。

需求如下:

  1. 寫一個servlet,其接到request後可輸出一個GIF動畫。
  2. 在一個htm內用<img>的src接接看測試

實作的檔案結構如下:

各檔內容如下(AnimatedGifEncoder.java就不介紹了):

  1. index.html
    <!DOCTYPE html>
    <html>
        <head>
            <title>Animated Gif Test</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <style>
                /* 為img加上背景色,測試透明GIF動畫 */
                img {
                    background-color: black;
                }
            </style>
        </head>
        <body>
            <div>Animated Gif Test</div>
            <!-- 測試GIF動畫 -->
            <img src="AnimatedGif.do" />
        </body>
    </html>
  2. AnimatedGif.java
    /*
     * To change this license header, choose License Headers in Project Properties.
     * To change this template file, choose Tools | Templates
     * and open the template in the editor.
     */
    package servlet;
    
    import java.awt.Color;
    import java.awt.Graphics2D;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.io.OutputStream;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import tools.AnimatedGifEncoder;
    
    @WebServlet(name = "AnimatedGif", urlPatterns = {"/AnimatedGif.do"})
    public class AnimatedGif extends HttpServlet {
    
        /**
         * Processes requests for both HTTP <code>GET</code> and <code>POST</code>
         * methods.
         *
         * @param request servlet request
         * @param response servlet response
         * @throws ServletException if a servlet-specific error occurs
         * @throws IOException if an I/O error occurs
         */
        protected void processRequest(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            OutputStream o = response.getOutputStream();
            response.setContentType("image/gif"); //設定-- header -- ContentType
            
            Color transparentColor = Color.WHITE;
            
            AnimatedGifEncoder encoder = new AnimatedGifEncoder();
            encoder.setTransparent(transparentColor); //設定透明色
            encoder.setDelay(100);  //設定frame之間的間隔 (毫秒)
            encoder.setRepeat(0);  //設定重覆迴圈次數(不設就是不重覆), 0:無限, 1:重覆一次...
            encoder.start(o);      //綁定OutputStream  
            
            //產生畫布
            BufferedImage generatedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
            //產生繪圖工具
            Graphics2D graphics2D = generatedImage.createGraphics();
            for (int i = 0; i < 10 ; i++){
                //畫底色
                graphics2D.setColor(transparentColor);
                graphics2D.fillRect(0, 0, generatedImage.getWidth(), generatedImage.getHeight());
                //畫圓圈
                graphics2D.setColor(Color.RED);
                graphics2D.drawOval(i*3, 25, 50, 50);
                //加到GIF動畫frame中
                encoder.addFrame(generatedImage);
            }
            graphics2D.dispose(); //釋放資源
     encoder.finish();  //GIF動畫結束
            //串流輸出
            o.flush();
            o.close();
        }
    
        // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
        /**
         * Handles the HTTP <code>GET</code> method.
         *
         * @param request servlet request
         * @param response servlet response
         * @throws ServletException if a servlet-specific error occurs
         * @throws IOException if an I/O error occurs
         */
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            processRequest(request, response);
        }
    
        /**
         * Handles the HTTP <code>POST</code> method.
         *
         * @param request servlet request
         * @param response servlet response
         * @throws ServletException if a servlet-specific error occurs
         * @throws IOException if an I/O error occurs
         */
        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            processRequest(request, response);
        }
    
        /**
         * Returns a short description of the servlet.
         *
         * @return a String containing servlet description
         */
        @Override
        public String getServletInfo() {
            return "Short description";
        }// </editor-fold>
    
    }
    
    
    
成果展示:

原始碼下載:
AnimatedGifEncoderTest.7z

參考資料:

  1. Java – How To Overlay One Image Over Another Using Graphics2D [Tutorial]
  2. 在Java應用程序中創建圖像的方法和技巧
  3. Is there a way to create one Gif image from multiple images in Java? [closed]
  4. 在Java中使用AnimatedGifEncoder生成GIF動畫
  5. [Android] AnimatedGifEncoder
  6. java将gif动态图片分开展示源代码简单示例

2016年9月17日 星期六

typescript + SystemJS + npm + jQuery (Netbeans, Eclipse)

這篇主要紀錄在Netbeans和Eclipse中使用Typescript,並配合SystemJS的範例,再來我們要利用npm來安裝JQuery並用SystemJS引入JQuery模組,學習建立自己的javascript module。

這篇要實現的需求如下:
  1. 在Netbeans和Eclipse中使用typescript外掛編譯我們的ts檔成js檔。
  2. 使用npm安裝JQuery。
  3. 使用SystemJS引入JQuery模組並使用。
  4. 建立一個自製的JS module, Greeter,配製好相應的package.json和export設定,放到node_modules中。
  5. import自制的Greeter,並建立一個myGreeter去entend(繼承)Greeter擴充功能。
以下是我們完成的目錄結構,需注意到的是,藍色框線標示的js及map檔是ts檔被typescript編譯出來的檔案:



接下來講解步驟:

2016年9月10日 星期六

使用raycaster實現Object的選取 - three.js

three.js可以在Scene放置許多的Object3D,但如果我們想要讓User跟那些Obejct做互動,那要如何做呢,例如User在Camera的畫面中看到許多的物件,想用滑鼠去點擊那些物件去做互動。

在three.js中,有一個Raycaster類可以做到上述需求,它的原裡是,想像有一條光束(ray)從Camera畫面中滑鼠指的位置,以垂直於Camera的X-Y平面的方式向Camera的負Z方向射出,並且計算在Scene的children中有哪些物件被這條線射中(ray可以穿過所有Object3D)。 

在這裡,我要來示範一個使用three.js的Raycaster的範例,需求是這樣的:

需求:
  1. 畫面上有許多的Object3D物件,有各自的顏色。
  2. 當滑鼠指到(hover)某一個Object時,將指到的第一個Object變成紅色(即它後面的Object不會變色)。
  3. 滑鼠移開後,剛剛指到但現在沒指的Object變回原來的顏色。
接下來一步步的講解每一個步驟:

2016年8月28日 星期日

Duration的格式化 - DurationFormatUtils - Java

這邊介紹一個Java很好用的類別,DurationFormatUtils,收錄在
Apache Commons Lang中,下載地址在這
package的位置在org.apache.commons.lang.time.DurationFormatUtils,
說明文件在這

DurationFormatUtils可以對代表 " 一段持續時間" 的資料做格式化,在DurationFormatUtils中
這個 "一段持續時間" 是用 millis-second 來的儲存的。

我們可以利用DurationFormatUtils來對持續時間用我們提供的自訂格式來格式化,例如181800000毫秒可以用 "dd'天'HH'時'mm'分'ss'秒'" 來表示成 "02天02時30分00秒"

下面就來紀錄一下DurationFormatUtils的使用方法:
import java.time.Duration;
import java.time.Instant;
import org.apache.commons.lang3.time.DurationFormatUtils;

public class DurationFormatUtilsTest {

    public static void main(String[] args) {
        Instant startInstant = Instant.parse("2015-05-01T00:00:00Z");  //ISO 8601 表示法
        Instant endInstant = Instant.parse("2015-05-03T02:30:00Z");        
        Duration duration = Duration.between(startInstant, endInstant); //得到兩個Instant差的Duration
        
        String resultDurationString1 = DurationFormatUtils.formatDuration(duration.toMillis(), "dd'天'HH'時'mm'分'ss'秒'");
        System.out.println(resultDurationString1); //輸出: 02天02時30分00秒
        
        String resultDurationString2 = DurationFormatUtils.formatDuration(duration.toMillis(), "dd'天'HH'時'mm'分'ss'秒'", false);
        System.out.println(resultDurationString2); //輸出: 2天2時30分0秒  //(10以下的數字不補0)
        
        String resultDurationString3 = DurationFormatUtils.formatDuration(duration.toMillis(), "HH'時'mm'分'ss'秒'");
        System.out.println(resultDurationString3); //輸出: 50時30分00秒  //(沒有設定小時以上的單位,小時以上全部由小時表示:50小時)
        
        String resultDurationString4 = DurationFormatUtils.formatDurationISO(duration.toMillis());
        System.out.println(resultDurationString4); //輸出: P0Y0M2DT2H30M0.000S //ISO 8601標準格式
        
    }
}

參考資料:

  1. Class DurationFormatUtils
  2. 常用jar包之commons-lang使用
  3. Java 8的日期與時間(Date-Time)API

2016年8月20日 星期六

使用Ajax上傳檔案 - (Javascript, JQuery or AngularJS) + FormData

在以前我們要在網頁中傳送檔案資料時,通常會需要設計一個html Form,並設定Form的encype=multipart/form-data和準備一個input type="file",在User按下submit按鈕後,跟據Form
所設定的action="URL",將整個頁面request移動到action所指定的地方,例如Servlet,
等處理完後在將User導至其他網頁頁面。

但有沒有可能不要讓User被導到其他頁面,留在原頁面就完成檔案資訊的傳送呢?
答案是可以的,這邊就要利用新的javascript類別FormData和Ajax的技術來達到Ajax傳
送檔案(也可順便傳遞其他input資料)資料。


在這邊我們要利用Netbeans、Servlet3.0、Tomcat 8來實做我們的範例,
實現了三個版本的檔案上傳:Javascript、JQuery、AngularJS
首頁是專案檔案結構,如下圖:

所用的版本JQuery為v2.0.3、AngularJS為v1.5.8,
主要的重要檔案有:

  1. fileUploadAjaxExample.html
    給User上傳檔案的html網頁。
  2. FileUploadAjax.js
    處理檔案資料上傳的Javascript File.
    包含Javascript、JQuery、AngularJS三個版本。
  3. FileUploadAction.java
    用來接收檔案資料的servlet,這裡設定URL patter為/fileUpload.do
  4. web.xml
    設定檔,其中必須在要接收檔案的servlet中設定的tag,
    tag裡可以有以下的子tag設定:
    <location>         檔案存放位置 (使用Part.write(fileName)可以在寫入檔案,但如果fileName
                               為絕對路徑則以絕對路徑為準)
    <max-file-size>  最大檔案size
    <max-request-size>   最大request size  (例如POST的request size)
    <file-size-threshold>
       超過file-size-threshold的檔案request將會以臨時暫存的方式存到硬                                       碟中,預設為0
接下來我以來各別看下每個實作檔案的內容:

2016年8月14日 星期日

使用FileReader讀取file資料 - javascript

FileReader是HTML5的新Javascript物件,可以用來讀取input type="file"的file資料(其實就是FileList對像),其實不只input type="file"的FileList對像,還可以讀取許多不同的資料來原,例如Blob對像、拖拉產生的DataTransfer對像等。

今天要來實作一個簡單的圖片預覽器,須求如下:

  1. 選擇檔案後,如果是可顯示的圖片檔,會在下面的"圖片預覽"顯示出圖片。
  2. 如果沒有選擇檔案(例如取消了檔案選取),則"圖片預覽"的圖片會被清掉。
  3. 為了簡單起見,這裡不檢查file來源是不是圖片檔。


程式實作如下:
說明:

  1. FileReader.readAsDataURL(source)可以輸入file source並得到一串當下可用的URL,
    型式為 dtat:..........,存在FileReader.result中
    還有其他的read函式可用,例如:
    readAsArrayBuffer()、readAsBinaryString()、readAsText()
  2. FileReader在讀取完後會觸發load事件,還有其他的事件可用,例如:
    onabort:當讀取操作被中止時調用.
    onerror:當讀取操作發生錯誤時調用.
    onload:當讀取操作成功完成時調用.
    onloadend:當讀取操作完成時調用,不管是成功還是失敗.該處理程序在onload或者onerror之后调用.
    onloadstart:當讀取操作將要開始之前調用.
    onprogress:在讀取數據過程中周期性調用.
  3. FileReader:
    FileReader.readyState代表資料的讀取狀態,總共有三個值:
    FileReader.EMPTY = 0:還沒有加載任何數據.
    FileReader.LOADING = 1:數據正在被加載.
    FileReader.DONE = 2:已完成全部的讀取請求.

源碼(怕JsFiddle有問題):
html:

圖片預覽:
javascript:
var fileUploader = document.getElementById("fileUploader");
var imageView = document.getElementById("imageView");
//用來讀取file資料的FileReader
var fileReader = new FileReader();

//監控#fileUploader的值變化
fileUploader.addEventListener("change", function(event) {
  if (this.files.length > 0) {
   //有選取file時,使用fileReader讀取file資料
    //readAsDataURL可以將讀取到的file資料轉成
  //data:......的URL型式,在讀取完後觸發load
  //事件,URL存在FileReader.result中
    fileReader.readAsDataURL(this.files[0]);
  }else{
   //沒有選取file時,例如選擇取消,
    //將的src設成""
   imageView.src = "";
  }
}, false);

//fileReader讀取完file資料後會觸發load事件
fileReader.addEventListener("load", function(event) {
 //讀取後設定的src
  imageView.src = this.result;
}, false);

--------------------------------------------------------------------------------------------------
另做了一個讀取 video 檔案長度 (秒數) 的範例,程式碼基本上差不多,只是改成把readAsDataURL() 得到的 src 設定在 <video> 上:
HTML :
<div>Choose a browser supported video file to load video duration.</div>
<input id="fileUploader" type="file" />
<div>Video length : <span id="videoLengthDisplayer"></span> (s)</div>

JS :
var fileUploader = document.getElementById("fileUploader");
var videoLengthDisplayer = document.getElementById("videoLengthDisplayer");
//用來讀取file資料的FileReader
var fileReader = new FileReader();

//監控#fileUploader的值變化
fileUploader.addEventListener("change", function(event) {
  if (this.files.length > 0) {
   //有選取file時,使用fileReader讀取file資料
    //readAsDataURL可以將讀取到的file資料轉成
  //data:......的URL型式
    fileReader.readAsDataURL(this.files[0]);
  }else{
   
  }
}, false);

fileReader.addEventListener("load", function(event) {
 var video = document.createElement('video');
  video.src = this.result;
  video.load();
  video.addEventListener("loadeddata", function(){
   videoLengthDisplayer.innerHTML = video.duration;
  });
  
}, false);

參考資料:
  1. FileReader

2016年8月13日 星期六

自製檢查file size的directive (input type="file") - AngularJs

AngularJS的表單、欄位驗證(Validation)非常好用,但有時會碰到想要自訂驗證方式、
或是某個欄位AngularJS並沒有實作Validation時(例如input type="file"),
就需要自訂有客製化驗證能力的directive

在這邊的需求如下:

  1. 製作一個directive,名稱為file-validator
  2. 配合input type="file"使用
  3. 可以設定file size(大小,單位Byte)和file type(副檔名), 用法範例:
  4. 如果fileSize沒指定或小於等於0,則不對fileSize做限制驗證。
  5. 如果fileType沒指定或為空字串,則不對fileType做限制驗證。
完成的程式碼成品如下:


說明:

  1. 當我們在ng-form裡面的input設置name及ng-model (及ngModelController)後,如果AngularJS有實作此種input type類型的話,Angular會在View中的值或對應model的
    值改變時,進行View及model的雙向挷定、同時變更。
    ngModelController裡面會存放model的值,即ngModel.$modelValue。
    也會存放view(通常為input type中顯示的值),即ngModel.$viewValue。
    並且也會管理此input的valid狀態。

    但因為AngularJS並沒有對input type="file"進行實作,也就是,不管User在input中選了什麼檔案,皆不會存值到ngModel.$modelValue及ngModel.$viewValue中,當然雙向挷定的model中也不會有值。

    所以為們要帶自制的directive中,指定
    required : ngModel
    來得到管理這個input的ngModel,並且手動的設定model的值。
    (!!不要手動用ngModel.$setViewValue()設定viewValue的值,會導致model value 會被蓋掉)
  2. 因為要設定model的值,所以我們要引入$parse,$parse(code)可以代入一串程式碼,有點
    像eval,$parse(code)會返回一個函式,並且此函式有一個函式,assign(scope, value),
    可以在scope中執行程式碼,並將value賦值給程式碼執行得到的變數。
    例如:
    $parse("myCtrl.fileUpload").assign(scope, file);
    代表在scope中執行 :  myCtrl.fileUpload = file;
  3. 在ngModel中,可以使用
    ngModel.$setValidity( validType, isValid)
    來設定此input的validType是valid還是invalid。
  4. 在解析如以下字串時,
    file-validator="fileType:'png'; fileSize:333;"
    我們使用了兩個正規表達式:
    fileSize\s*:\s*(\d+)\s;

    fileType\s*:\s*(["'])(\w+)\1\s*;
    其中第二個正規達式中的 \1 的意思是跟第一個Group相匹配,而第一個Group就是(["']),
    也就是fileType:"png"; 或 fileType: 'png';可以匹配,但
                fileType:'png";  或 fileType:"png'; 不可以匹配,
    雙引號及單引號要成對使用。

原碼紀錄(怕jsFiddle出問題):
使用angular.js
html:

請選擇size小於3MB的PNG檔案


file valid: {{myForm.fileUpload.$valid}}
file size (Byte): {{myCtrl.fileUpload.size}}
副檔名錯誤
檔案過大


javascript:
 var myApp = angular.module("myApp", []);

 myApp.controller("myController", [function() {
   //
 }]);
 //自製的file validator
 myApp.directive('fileValidator', ["$parse", function($parse) {
   return {
     restrict: 'A',
     scope: true,
     require: 'ngModel', //代表管理input type="file"這個input的ngModelController,例如此例
     //ngModel.name = "fileUpload",
     //可以連結ng-model裡指定的變數值(ngModel.modelValue) 和
     //input view欄位中顯示的值(ngModel.viewValue)                          
     link: function(scope, elm, attrs, ngModel) {
       var expression = attrs.fileValidator;

       var fileSizeReg = /fileSize:\s*(\d+)\s*;/;
       var fileTypeReg = /fileType\s*:\s*(["'])(\w+)\1\s*;/;

       //規定file的size,單位Byte
       var fileSizeLimit = fileSizeReg.exec(expression)[1] || 0;
       //規定副檔名
       var fileTypeLimit = fileTypeReg.exec(expression)[2] || "";

       elm.bind('change', function() { //發現input type file值改變時
         scope.$apply(function() {
           var file = elm[0].files[0]; //取得file資料
           var fileType = /.+\.(.+)/.exec(file.name)[1];

           //$parse("程式碼")可以返回一個function,之後可以用這個function的
           //assign(scope, value)來在scope中進行賦值(value)動作,
           //例如: 在scope中墸行: 程式碼 = value (有點像eval())
           $parse(attrs.ngModel).assign(scope, file);

           //檢查file副檔名及size
           //用ngModel設置fileSize的valid
           if (fileTypeLimit === "" || fileType === fileTypeLimit) {
             ngModel.$setValidity("fileType", true);
           } else {
             ngModel.$setValidity("fileType", false);
           }
           //用ngModel設置fileSize的invalid
           if (fileSizeLimit <= 0 || file.size < fileSizeLimit) {
             ngModel.$setValidity("fileSize", true);
           } else {
             ngModel.$setValidity("fileSize", false);
           }
         });
       });
     }
   };
 }]);

2016年7月30日 星期六

使用Google Play Developer API讀取Google Play Store上APP的資訊 - Java

Google Play Developer API是Google提供的API,基本上用各種url以http來溝通,
使用它可以讓客戶端存取Play Store上APP的各種設定,就像是在Google Play Store Console上一樣。 (當然了客戶端必須被授權)

在這邊我們使用Java來讀取Google Play Store上APP的版本資訊,
使用 Google Play Developer API samples裡的ListApks.java範例來示範。

主要分成兩個部份:

下面開始說明一步步所需要做的步驟:
  • APP管理者:
    • Google Developers Console
      1. APP管理者指的是有APP在Play Store上上架的人,首先要進到Google Developers Console裡開一個新專案project(或使用舊專案)。
      2. 到「資料庫」搜尋並啟用「Google Play Android Developer API」
      3. 到「憑證」建立一個「服務帳戶金錀」
      4. 在「服務帳戶」選擇「新增服務帳戶」建立一個服務帳戶(Service Account),打入「服務帳戶名稱」後在下面就會得到一個「服務帳戶 ID」,這個之後會用到。
        「金鑰類型」選擇「P12」。

        按下建立後,就會得到一個副檔名為p12的檔案,這個之後也會用到。
    • Google Play Developer Console
      1. 接下來在Google Play Developer Console,到設定頁面(一個齒輪的圖案),選「API存取權」,選擇一個專案進行連結(就是在Google Developer Console設定的那個專案project)。
      2. 接著到下面的「服務帳戶」,選擇剛剛在Google Developer Console得到的「服務帳戶 ID」,點「賦予存取權」進入設定權限畫面 。
      3. 在設定畫面,選擇「新增應用程式 」,選擇要存取資訊的APP。
  • 服務請求者:
    •  Google Play Developer API samples
      1. Client Libraries and Code Samples中,可以下載Java的「androidpublisher_v2_public.zip」,裡面有需要Import進Java Project的Client Library,並且可以參考Code Sample如何使用Client Library。
      2. 這裡以ListApks.java為例,AndroidPublisherHelper.java是用來幫助我們使用Google Android Play API的輔助類,進去源碼可以看到有定義一個
        String, SRC_RESOURCES_KEY_P12 = "src/resources/key.p12" , 這就是我們要放置剛剛我們得到的副檔名為p12的地方。
      3. 在resources資料夾放好p12檔後,我們要到ApplicationConfig.java中設置一些東西。在ApplicationConfig.java中,我們可以看到以下設置:
        • String APPLICATION_NAME  
          • (應用程式名字,不是很重要,照這個"MyCompany-Application/1.0"格式就行)
        • String PACKAGE_NAME
          • (APP套件名稱,忘記了可以在Google Play Developer Console選APK按任一個Version的APK詳細資訊查到)
        • SERVICE_ACCOUNT_EMAIL
          • (這就是我們剛剛看到的服務帳戶 ID)
        • APK_FILE_PATH
          • (可以用在例如Upload APK時,這邊沒用到先忽略)
      4. 最後,我們執行ListApks.java,就可以看到在主控台中輸出如以下的訊息了:
      5. 檔案結構就像是這樣:
參考資料:

  1. Client Libraries and Code Samples
  2. Google Play Developer API samples
  3. Google Play Developer API
  4. Google Play Developer API V2 Rev32 1.22.0 (有Marven、Gradle等方式)
  5. Google Play Android Developer API return 401
  6. Why getting error 'The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console.

2016年6月22日 星期三

D3.js + AngularJS - 製作長條圖及圓餅圖動畫

今天要來結合好用的圖表工具D3.js和AngularJS來製作有動畫的長條圖和圓餅圖

範例的程式如下,相關的解釋都在寫在程式的註解中:
這裡對做點說明:

  1. 使用AngularJS的客製化directive來製做長條圖及圓餅圖,送入directive的資料跟<input>綁定。
  2. 使用svg來進行長條圖及圓餅圖的繪製。
  3. D3.js可用圖表幫助function將data包成datum,datum會包含原來的data(為傳reference的方式),並且擁有一些幫助繪圖的屬性,例如圓餅圖會用到的startAngle、endAngle。
  4. D3.js的data是使用如傳reference的方式,所以此例當AngularJS改變data時各element的data會跟著改變;但datum不是傳reference的方式,各element有自己的datum,改變parent的datum時記得手動改變下層element的datum。
  5. 在D3.js裡,select()、append會將parent的data和datum帶到下一個element。
    但selectAll()不會,要手動對下一個element呼叫data()或datum()。
  6. select()只會為呼叫者group的每一個element返回搜尋到的第一個element;而selectAll()會每呼叫者group的每一個element返回一個陣列,包含所有找到的element,其中each(function(d,i){//......});中的i為陣列的index。
  7. data()可送入一個陣列,陣列中的每一個元素會分給呼叫data()的每一個element。
    也可送入一個function,例如data(function(d,i){//....;  return [d];},在這裡d為parent的data或datum,return值為送入data()的參數。
參考資料:

  1. SVG D3.js - transition ( tween、interpolate )
  2. SVG D3.js - 直條圖 ( Bar Chart )
  3. SVG D3.js - transition 基本篇
  4. D3 on AngularJS

2016年6月8日 星期三

AngularJS - 自製跟ng-repeat一樣能力的directive (my-repeat)

在這篇文中,我們要來製作一個跟AngularJS的ng-repeat有幾乎相同能力的自製directive(當然ng-repeat更完整,有更多的可用變數、接受表達示等)。

我們先定義出自製directive要擁有的能力,在這裡我們取名我們的directive叫作my-repeat:
  1. 使用型式如同ng-repeat,可用attribute的型式放在html element tag的attribute中使用。
  2. 其中,屬性裡面可以用 "XX in YYY" 這種型式的表達示來表示要對哪個array型式(YYY)的內部資料(XX)做迴圈。
  3. 在directive中,可以解析YYY中的XX來顯示資料,也可解析其他資料,例如controller中的變數。
  4. 能夠使用 "-start" 和 "-end" 的multiElement的用法。
  5. 此directive要可以嵌套到另一個directive中(其他的directive或自己)。

下面就是實現出的程式碼,可以在Result看到,我們的my-repeat擁有和ng-repeat相同的上述能力。(**有使用jQuery做$(element)和before()的動作)


在程式碼中已經寫了詳細的註解,不過我們還必須對執行完上述程式碼後,AngularJS產生出了什麼樣的Scope關係。
在程式碼裡,我們已經在有scope出現的地方進行了console.log()的輸出,可以由其中的屬性物件、$id、_proto_了解到scope之間的關係,以下已經用簡單圖示來表示從console.log()結果中看出的關係:

2016年5月6日 星期五

html5 的 setCustomValidity(message) 觀念釐清

在這篇文中主要是要釐清在使用setCustomValidity(message)的一些觀念,
setCustomValidity(message)可以跟據送入的message是否是空字串來蓋住input元素的validity狀態,但是要注意的是,並不會蓋掉input元素的validity狀態,input元素的真實validity狀態並不會因此而改變。

為了驗證這個現像,我們可以看一下下面這個例子:
當我們呼叫
setCustomValidity("自訂訊息");
時,input的真正valid訊息會被隱藏起來,而這時去看input的validity.valid的話就得到false的結果,就算我們在input中打字也是一樣,
但其實input在有打字內容時其直正的validity.valid應該是true。
我們可以呼叫setCustomValidity("")來讓input的真正validity.valid不要被蓋往,這時我們可以從isValid(after setCustomValidity("")):來得到之前被隱藏的正確結果。

========================================================================

這裡我再舉出一個比較常用的使用setCustomValidity("")的例子:

可以看到我們再input的oninvalid和oninput都呼叫check(this),這是因為當在input中打字時,就算打的內容不符合設定的pattern,也不會觸發oninvalid,只會觸發oninput。

我們在check(target)中先呼叫setCustomValidity("")得到input的真正valid狀態,如果真正valid為false就呼叫setCustomValidity("自訂訊息"),而因為此呼叫會讓之後valid的真正狀態被隱藏,所以我們之後會再呼叫check時呼叫setCustomValidity("")來讓input的真正valid狀態不要被隱藏起來。

這樣之後再用if()詢問input的狀態時,就不會得到錯的、與實際不符的valid狀態了。
重點就是,在檢查valid的狀態時必須要真正的(沒被隱藏的),再判斷要不要設定自訂訊息後。

========================================================================

接下來我再舉出一個例子,有時候我們會想要對一個Group,即一群name一樣的input元素去做判斷,例如對於input="radio"只會想要同一個Group裡的radio被選取,像下面這樣:


如果想在radio的oninvalid和onclick使用上述的check(target)方法的話,會造成錯誤。
如果有兩個設置rquired的同name的radio且都沒有被點擊時,按下submit後會對每一個radio呼叫check(target)並都被隱藏真正valid狀態。
當點擊第二個radio時,會只對第二個radio呼叫check(target),此時第二個radio的valid會是true,但對一個radio的valid還是false,所以此時按下submit會無法submit,接著又因為submit會讓每一個check(target)都被呼叫,所以此時每一個radio的valid狀態都才會正常,再按一次submit就可以成功submit了。

結果是submit要按兩下才會成功,這很顯然不是我們想要的,所以我們要做一點改良。

以下就是改良的方法,checkValid(target),在其中也對一個input用radio來設置不同的pattern check:

重點就是在checkValid(target)裡面必須對所有group裡面的input都去做檢查並設置正確的valid狀態,如此一來,不管是submit還是onclick就都能對所有的input設置正確的valid狀態了
(當然submit會輪詢一次所有input,而checkValid(target)又會輪詢一次,所以會輪詢兩次造成效能上的浪費,不過這裡就忽略此issue了)。



2016年5月3日 星期二

html5 自訂表單驗證的警示文字 - setCustomValidity(message)

html5 的表單驗證非常方便,能根據設置的type (例如:email, phone等)、requiered屬性的有無設置等在form中有input為invalid時,顯示提示文字,

不過我們通常會想要的不是預設的,而是自訂的提示文字,這時就可以用到setCustomValidity(message)這個javascript方法。

setCustomValidity(message)可以由 input type='xxx' 的元素呼叫,
它接受一個字串作為自訂的提示文字,要注意的是,當此方式呼叫該方法時,input元素的valid狀態會被invalid蓋住(不是蓋掉)。如果我們送入的字串為空字串,則此input元素的valid狀態會回復正常(沒被蓋往)。

所以我們可以在input驗證不通過時呼叫setCustomValidity('XXX'),並在它的內容值有改變或有被key in值時呼叫setCustomValidity('')來得到input的真正valid狀態(否則那個input就永遠是invalid)。

看一下下面的例子:

這裡有兩個 form,並且各自有一個設置required的input type='text',兩個form都設置了
oninvalid="setCustomValidity('XXX')"
所以當沒有填值按下form的submit按鈕時,就會跳出自訂的提示文字並且阻止form的submit動作。

因為第一個form的input設置了onchange="setCustomValidity('');",所以在input中的值改變時會對此input進行invalid error的清除,但此時我們會發現當在此input中打字時,提示文字不會消除且一直出現。
這是因為onchange在此input失去焦點(off focus)且值有改變(change)時才會觸發,當我們點擊Form1的第二個input再點回要reqired的input並打字時,提示文字就不會出現了。
當然,這通常不是我們想要的結果。

第二個Form的input設置了oninput="setCustomValidity('');",使用了oninput而不是onchange來呼叫重設input的invalid狀態,oninput會在使用者對input打字時被觸發,於是當提示文字出現時,只要再input中打字(不用使其焦點),就可以得到input的invalid狀態了,而這個結果也是我們所期望的。

參考資料:

  1. JavaScript Validation API
  2. oninput Event
  3. onchange Event
  4. HTML5應用:setCustomValidity(message)接口


源碼:
<form>
  Form 1
  <input type='text' oninvalid="setCustomValidity('Cusotm Alert 1');" onchange="setCustomValidity('');" placeholder='This input is required' required/>
  <input type='text' />
  <input type='submit' />
</form>
<form>
  Form 2
  <input type='text' oninvalid="setCustomValidity('Cusotm Alert 2');" oninput="setCustomValidity('');" placeholder='This input is required' required/>
  <input type='text' />
  <input type='submit' />
</form>

2016年4月25日 星期一

Java : SimpleDateFormat、TimeZone和Date -- 觀念釐清

在這篇裡主要紀錄了在Java中,對於SimpleDateFormat、TimeZone和Date的理解,釐清之前對它們有誤會及不太清楚的地方。

java.util.Date是一個早期Java中專門用來紀錄日期的一個類別,同時也是常用java.sql.Date的父類別,它並沒有儲存時區的資訊,當我們要對其進行不同時區的值(String或Date)轉換時,可以利用SimpleDateFormat及TimeZone來幫助我們。

TimeZone是個儲存時區資訊的類別,而SimpleDateFormat是一個用來進行日期格式轉換的類別,可以利用SimpleDateFormat.setTimeZone()來設定TimeZone,而常用的為以下兩個方法:


  1. String SimpleDateFormat.format(Date)
  2. Date SimpleDateFormat.parse(String)

SimpleDateFormat.format(Date)
可以送入一個Date物件,並返回一個表示日期的String。
如果SimpleDateFormat沒有設定TimeZone的話,
等同設定了TimeZone.getDefault()。
SimpleDateFormat.format(Date) 會將送入的Date參數轉成用設定的TimeZone來看時,正確的日期String。
例如:
如果 Date 是 2022-01-01T23:00:00+08
Timezone 設定 UTC-08
那 format() 出來的 String 就會是
2022-01-01T07:00:00

如果你的 SimpleDateFormat 建構子的字串有設定代表時區的 Z 符號的話,就可以印出時區,
例如:
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
上例的結果會是 2022-01-01T07:00:00-08

SimpleDateFormat.parse(String) 
可以送入一個表示日期的String,並返回一個Date物件。
如果SimpleDateFormat沒有設定TimeZone的話,
等同設定了TimeZone.getDefault()。
SimpleDateFormat.parse(String) 會將送入的 String 轉成當用設定的 Timezone 來看時
正確的 Date 物件。
例如:
如果 String 是 2022-01-01T07:00:00
Timezone 設定 UTC+08
那 parse() 出來的 Date 就會是 
2022-01-01T07:00:00+08

========================
接著下面舉一個例子:

如果我們有一個表示Local TimeZone(假設程式放在台灣server上)日期的Date物件,例如:
System.our.println(dateObject_local);   //2016/4/25 16:47:59


我們想把它轉成在TimeZone為America/Los_Angeles時,看到這個日期應看到的Date物件,例如
System.our.println(dateObject_America);   //2016/4/25 01:47:59


應該要如何做呢?
首先宣告TimeZone並設定給SimpleDateFormat
TimeZone timeZone_America = TimeZone.getTimeZone("America/Los_Angeles");
TimeZone timeZone_default = TimeZone.getDefault();

SimpleDateFormat simpleDateFormat_America = new SimpleDateFormat();
simpleDateFormat_America.setTimeZone(timeZone_America);

SimpleDateFormat simpleDateFormat_Taiwan = new SimpleDateFormat();
simpleDateFormat_Taiwan.setTimeZone(timeZone_default);

Date dateObject_local = new Date();
System.out.println("dateObject_local:" + dateObject_local);  //dateObject_local:Mon Apr 25 18:11:27 CST 2016

接著我們用simpleDateFormat_America去format上面那個Date,
String dateString_America = simpleDateFormat_America.format(dateObject_local);
System.out.println("dateString_America:" + dateString_America); //dateString_America:2016/4/25 上午 3:11

會看到時間被減了15小時,這正是美國比台灣慢15小時的時差。
如果我們想得到輸出是時間 3:11的Date物件,下面這樣是錯的:
Date Wrong_dateObject = simpleDateFormat_America.parse(dateString_America);
System.out.println("Wrong_dateObject:" + Wrong_dateObject); //Wrong_dateObject:Mon Apr 25 18:16:00 CST 2016

原因是當用simpleDateFormat_America去parse上面那個String, dateString_America時,它會用simpleDateFormat_America設定的TimeZone去看,也就是說,它認為這是美國時間3:11,所以要parse回local time的Date時,它會將時間加回15小時,所以得到非我們預期的結果。

正確的做法是下面這樣:
Date Correct_dateObject = simpleDateFormat_Taiwan.parse(dateString_America);
System.out.println("Correct_dateObject:" + Correct_dateObject); //Correct_dateObject:Mon Apr 25 03:16:00 CST 2016

我們用simpleDateFormat_Taiwan去parse,讓它以為這是本地時間3:11,再parse成local time的Date時,就不會進行時差的動作了(因為時差為0)。

只要記得,Date本身沒有時區的資訊,基本上都用Local TimeZone去對待得到一個無時區概念的毫秒數,不管是被format還是由parse得到。

而String則會用SimpleDateFormat設定的TimeZone去對待,不管是由format得到還是被parse。

2016年4月18日 星期一

ng-repeat-start和ng-repeat-end -- AngularJS

在AngularJS中,我們常會用ng-repeat這個directive來將一串資料顯示出來,不過ng-repeat的功能是有局限性的,例如如果我們有以下的data,

datas = [{name : 1 , phone:111},{name : 2 , phone:222},{name : 3 , phone: 333}];

並想讓以下重覆顯示的話,ng-repeat就不夠用了。

<h1>Name : {{data.name}}</h1>
<p style="border-bottom:thick solid #ff0000;">Phone : {{data.phone}}</p>

這時我們可以使用ng-repeat-start及ng-repeat-end,

在ng-repeat-start標示的tag及ng-repeat-end標示的tag之間(包括被ng-repeat-start和ng-repeat-end標示的tag),能夠存取到同一個串列資料中的各元素,下面就是一個使用的簡單例子:


我們先定義了ng-app、ng-controller及其中的datas。

var myapp = angular.module('myapp',[]);
myapp.controller('Ctrl',[function(){
 var self = this;
  self.datas = [{name : 1 , phone:111},{name : 2 , phone:222},{name : 3 , phone: 333}];
}]);

接著在設計要顯示的html:

<div ng-app="myapp" ng-controller="Ctrl as ctrl">  
  <h1 ng-repeat-start = "data in ctrl.datas">Name : {{data.name}}</h1>
  <p ng-repeat-end style="border-bottom:thick solid #ff0000;">Phone : {{data.phone}}</p>  
</div>

可以看到每一次的repeat都從串列資料(datas)中取出一組資料(包括了name及phone的資訊),並顯示一組
<h1>......</h1>
<p>.......</p>

參考資料:

  1. ngRepeat

2016年4月13日 星期三

AngularJS的$compile() - 動態增加DOM

在AngularJS中,會在瀏覽器對html解析成DOM後,先對AngularJS(ng-app)的進行編譯,給定scope後才會進行數據綁定。

但是如果是用例如JQuery等方式新增了新的DOM元素後,因為沒有用AngularJS編譯的關係,所以就算裡面有AngularJS的語法也不會呈現預期的結果。

例如像以下的例子:

HTML:
<div ng-app="myApp">
  <div id='DOMWrapper' ng-controller="myCtrl">
    <button ng-click='addDOM()'>Add DOM</button>
  </div>
</div>
Javascript:
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', '$compile', function($scope, $compile) {
	$scope.textOfDOM = 'Text show successfully';

  $scope.addDOM = function() {
    var DOMToAdd = $('<p>{{textOfDOM}}</p>');
    
    //Wrong code -- start
    $('#DOMWrapper').append(DOMToAdd);
    //Wrong code -- end    
    
    //Right code -- start
    //var linkOfDOMToAdd = $compile(DOMToAdd);
    //var nodeOfCompiledDOM = linkOfDOMToAdd($scope);
    //$('#DOMWrapper').append(nodeOfCompiledDOM);
    //Right code -- end
  };
}]);

 從結果中可以看到當按下"Add DOM"按鈕時,出現的字是 "{{textOfDOM}}",而不是我們預期的 "Text show successfully"。

這是因為要新增的DOM沒有被AngularJS編譯的關係,以下才是正確的程式碼:
HTML:
<div ng-app="myApp">
  <div id='DOMWrapper' ng-controller="myCtrl">
    <button ng-click='addDOM()'>Add DOM</button>
  </div>
</div>
Javascript:
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', '$compile', function($scope, $compile) {
	$scope.textOfDOM = 'Text show successfully';

  $scope.addDOM = function() {
    var DOMToAdd = $('<p>{{textOfDOM}}</p>');
    
    //Wrong code -- start
    //$('#DOMWrapper').append(DOMToAdd);
    //Wrong code -- end
    
    //Right code -- start
    var linkOfDOMToAdd = $compile(DOMToAdd);
    var nodeOfCompiledDOM = linkOfDOMToAdd($scope);
    $('#DOMWrapper').append(nodeOfCompiledDOM);
    //Right code -- end
  };
}]);
我們可以呼叫$compile()並把要編譯的DOM送進去當參數,它會返回一個 link 函式,呼叫 link 函式並把scope當作輸入參數,就可以將scope與被編譯的DOM產生連結,所以DOM就可以得到textOfDOM的值,link 函式會返回已經跟資料連結好的新的DOM物件,接著我們再把新的DOM物件用JQuery的語法放到 #DOMWrapper裡面,就可以正確的得到我們要的 "Text show successfully"了。

參考資料:

  1. 18.5. Compile的细节
  2. $compile

2016年3月6日 星期日

[SQL] 於多筆重複資料中取得該重複群組中最新一筆資料

最近要做一件特別的SQL查詢,有個一個Table1,如下所示

Table1 :
group_iduser_id
111
1111
111111
222
2222
333
3333

我們想要選出各group_id最大的user_id,也就是說我們希望的結果要像是如下這樣:
group_iduser_id
111111
2222
3333

先來看一下ROW_NUMBER()語句的,如下所示,詳細資料可參考這裡

ROW_NUMBER ( ) OVER ( [ PARTITION BY value_expression , ... [ n ] ] order_by_clause )

它可對傳回結果集的各分割區進行編列序號,各分割區序號從 1 開始,利用這個我們就可完成我們的需求。

思路是這樣的:先把Table1以group_id進行分組(partition),各組進行以user_id的DESC排序並標上編號(例如group_id=1的三組資料編號1,2,3、group_id=2的二組資料編號1,2),接著取出各組最上面那行資料,也就是各組編號為1的那行即可。

這時我們就可以使用rownumber, over, partition語法指令來幫助我們達成目標。

我們先用以下語法對Table1以group_id進行分組,並對各組以user_id做DESC排序,並為給組的排序標上編號,Sort
SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id Order By user_id DESC) As Sort 
FROM Table1

這樣就會得到以下結果:
group_iduser_idSort
1111111
11112
1113
22221
2222
33331
3331

接著在跟Table1結合並選出Sort=1但不會select Sort欄位就行了,可以使用如下語句:
SELECT group_id, user_id FROM (
         SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id Order By user_id DESC) As Sort
         FROM Table1
) SortTable
WHERE SortTable.Sort = 1
這樣就大功告成了。

參考資料:
  1. [SQL] 於多筆重複資料中取得該重複群組中最新一筆資料

Parse ISO 8601 duration 的方法

ISO 8601 Duration是ISO 8601的Duration標準,是用來表示持續時間的字串,例如
"P3Y6M4DT12H30M5S" 就表示 "3年6個月4天12小時又30分5秒",
像Youtube A{I回傳的影片長度就是用這個標準的字串來表示,
在Java中,除了自己想辨法解析外,也可以使用以下的方法:
  1. 在Java 1.8之後,只要使用java.time.Duration這個Class就可以輕易地解析由ISO 8601 Duration表示的持續時間,範例程式如下:
  2. import java.time.Duration;
    import java.text.NumberFormat;
    import java.text.DecimalFormat;
    
    public class HelloWorld
    {
      public static void main(String[] args)
      {
        Duration duration = Duration.parse("PT58S");
        NumberFormat formatter = new DecimalFormat("00");
    
        String hours = formatter.format(duration.toHours());
        String minutes = formatter.format(duration.toMinutes());
        String seconds = formatter.format(duration.getSeconds());
    
        System.out.print(hours + ":" + minutes + ":" + seconds); //00:00:58
      }
    }

  3. 而在Java 1.8之前,因為沒有java.time.Duration,所以可以借助第三方的Class,在這邊介紹一個很好用的第三方JAR,joda-time,到官網下載引用後,就可以使用它的Period, PeriodFormatter, ISOPeriodFormater等Class來幫助我們解析ISO 8601 Duration,範例程式如下:
  4. import java.text.DecimalFormat;
    import java.text.NumberFormat;
    import org.joda.time.Period;
    import org.joda.time.format.ISOPeriodFormat;
    import org.joda.time.format.PeriodFormatter;
    
    public class DurationTest {
    
     public DurationTest() {
      // TODO Auto-generated constructor stub
     }
    
     public static void main(String[] args) {
      PeriodFormatter periodformatter = ISOPeriodFormat.standard();
      Period p = periodformatter.parsePeriod("PT58S");
      NumberFormat numberformatter = new DecimalFormat("00");  
    
      String hours = numberformatter.format(p.getHours());
      String minutes = numberformatter.format(p.getMinutes());
      String seconds = numberformatter.format(p.getSeconds());
    
      System.out.println(hours + ":" + minutes + ":" + seconds); //00:00:58
     }
    }
參考資料:
  1. How to convert Youtube API V3 duration in Java