2017年10月11日 星期三

以Java 由Url下載圖片

今天要來記錄如何使用Java來下載網路上的圖片(或檔案,此例只下載圖片),

Java可以對Url進行Http Request來取得Response,
得到input stream後將檔案儲存下來,

為了避免有些Server會擋程式的Request,
我們必須模仿瀏覽器,在Request中加上User-Agent的Header (或更多的其他Header,模仿的越像越不容易被擋),

如果圖片的Url是http的話比較簡單,
但如果是https的話,即SSL,那就要取得對方Server網站的憑證,
或是自己實作一個 X509TrustManager,來所有憑證檢查都通過,

如果是JDK 1.8 以下,可能會有此Exception :
Could not generate DH keypair
使用 JDK 1.8 就可解決此問題,
可參考 [Java] 處理無法透過SSL抓取網站資料的問題

以下為程式碼範例,說明都寫在註解中:


import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class ImageDownloader {

 public static void main(String[] args) {

  String fileName_http = downloadImageFromUrl("http://www.image.com/files/8813/5551/7470/cruise-ship.png","D:" + File.separator, "HttpImgTest");
  System.out.println("Http的圖片下載: " + fileName_http);
  
  String fileName_https = downloadImageFromUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Plithocyon_armagnacensis.JPG/220px-Plithocyon_armagnacensis.JPG","D:" + File.separator, "HttpsImgTest");
  System.out.println("Https的圖片下載: " + fileName_https);

 }

 public static String downloadImageFromUrl(String url, String fileDirectoryPath, String fileNameWithoutFormat) {
  String filePath = null;
  
  BufferedInputStream in = null;
  ByteArrayOutputStream out = null;
  HttpURLConnection httpUrlConnection = null;
  FileOutputStream file = null;

  try {
   
   if (url.startsWith("https://")) {
    //HTTPS時
    httpUrlConnection = getHttpURLConnectionFromHttps(url);
   }
   //如果不是HTTPS或是沒成功得到httpUrlConnection,用HTTP的方法
   if(httpUrlConnection == null) {
    httpUrlConnection = (HttpURLConnection) (new URL(url)).openConnection();
   }
   
   // 設置User-Agent,偽裝成一般瀏覽器,不然有些伺服器會擋掉機器程式請求
   httpUrlConnection.setRequestProperty("User-Agent",
     "Mozilla/5.0 (Linux; Android 4.2.1; Nexus 7 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166  Safari/535.19");
   httpUrlConnection.connect();

   String imageType;
   if (httpUrlConnection.getResponseCode() == 200) {
    //成功取得response,
    //取得contentType
    String contentType = httpUrlConnection.getHeaderField("Content-Type");
    // 只處理image的回應
    if ("image".equals(contentType.substring(0, contentType.indexOf("/")))) {
     //得到對方Server提供的圖片副檔名,如jpg, png等
     imageType = contentType.substring(contentType.indexOf("/") + 1);

     if (imageType != null && !"".equals(imageType)) {
      //由HttpUrlConnection取得輸入串流
      in = new BufferedInputStream(httpUrlConnection.getInputStream());
      out = new ByteArrayOutputStream();

      //建立串流Buffer
      byte[] buffer = new byte[1024];

      file = new FileOutputStream(new File(fileDirectoryPath + File.separator + fileNameWithoutFormat + "." + imageType));

      int readByte;
      while ((readByte = in.read(buffer)) != -1) {
       //輸出檔案
       out.write(buffer, 0, readByte);
      }      

      byte[] response = out.toByteArray();
      file.write(response);      

      //下載成功後,返回檔案路徑
      filePath = fileDirectoryPath + File.separator + fileNameWithoutFormat + "." + imageType;
     }
    }

   }
  } catch (MalformedURLException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  } finally {
   //關閉各種串流
   try {
    if (out != null) {
     out.close();
    }
    if (in != null) {
     in.close();
    }
    if (httpUrlConnection != null) {
     httpUrlConnection.disconnect();
    }
    if (file != null) {
     file.close();
    }
   }catch (IOException e) {
    e.printStackTrace();
   }
   
  }
  return filePath;
 }

 public static HttpURLConnection getHttpURLConnectionFromHttps(String url) {
  HttpURLConnection httpUrlConnection = null;
  //建立一個信認所有憑證的X509TrustManager,放到TrustManager裡面
  TrustManager[] trustAllCerts;
  try {
   // Activate the new trust manager
   trustAllCerts = new TrustManager[] { new X509TrustManager() {

    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {     // TODO Auto-generated method stub
     //不作任何事
    }

    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {     // TODO Auto-generated method stub
     //不作任何事
    }

    public X509Certificate[] getAcceptedIssuers() {
     //不作任何事
     return null;
    }

   } };

   //設置SSL設定
   SSLContext sslContext = SSLContext.getInstance("SSL");
   sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
   HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

   //跟HTTP一樣,用Url建立連線
   httpUrlConnection = (HttpURLConnection) (new URL(url)).openConnection();
  } catch (KeyManagementException e) {
   e.printStackTrace();
  } catch (NoSuchAlgorithmException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  }
  
  return httpUrlConnection;
 }

}


源碼下載:
ImageDownloaderFromHttpOrHttps.7z

參考資料:

2017年10月5日 星期四

正規表逹式 - Java 範例

紀錄下Java的正規表示法使用方法範例:

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegTest {

 public static void main(String[] args) { 
  //(?i) 代表大小寫忽略
  //.*?為非貪婪演算,盡可能找最小範圍的結果值
  doRegularExpression("[ABC/def]kk[123/456][ABC/def]kk[123/456]", "(?i)\\[(a.*?)/(.*?)\\]");
  //.*為貪婪演算,盡可能找最大範圍的結果值
  doRegularExpression("[ABC/def]kk[123/456][ABC/def]kk[123/456]", "(?i)\\[(a.*)/(.*)\\]");
  
  /* 輸出為:
  ===========================
  測試的句子: [ABC/def]kk[123/456][ABC/def]kk[123/456]
  正規表示法: (?i)\[(a.*?)/(.*?)\]
  ---------------------------
  第1次匹配,找到2個Group:
  匹配的字串為: [ABC/def]
  第1個Group: ABC
  第2個Group: def
  ---------------------------
  第2次匹配,找到2個Group:
  匹配的字串為: [ABC/def]
  第1個Group: ABC
  第2個Group: def
  ===========================
  
  ===========================
  測試的句子: [ABC/def]kk[123/456][ABC/def]kk[123/456]
  正規表示法: (?i)\[(a.*)/(.*)\]
  ---------------------------
  第1次匹配,找到2個Group:
  匹配的字串為: [ABC/def]kk[123/456][ABC/def]kk[123/456]
  第1個Group: ABC/def]kk[123/456][ABC/def]kk[123
  第2個Group: 456
  ===========================
  */
 }
 
 static void doRegularExpression(String text, String regularExpression) {
  System.out.println("===========================");
  System.out.println("測試的句子: " + text);
  System.out.println("正規表示法: " + regularExpression);
  
  Pattern pattern = Pattern.compile(regularExpression);
  Matcher matcher = pattern.matcher(text);  
  
  
  for(int matchCount = 1 ; matcher.find(); matchCount++) {
   // groupCount不包括匹配的字串,即matcher.group(0)
   System.out.println("---------------------------");
   System.out.println("第" + matchCount + "次匹配,找到" + matcher.groupCount() + "個Group:"); 
   System.out.println("匹配的字串為: " + matcher.group(0));
   for(int groupCount = 1; groupCount <= matcher.groupCount(); groupCount++) {
          System.out.println("第" + groupCount + "個Group: " + matcher.group(groupCount));
      }
  }
  System.out.println("===========================");
  System.out.println();
 }

}


2017年6月22日 星期四

Gifsicle 和 Giflossy(Gifsicle的一個Github Fork) (nmake, visual studio)

Gifsicle是個功能強大的GIF處理程式,使用命令列視窗來執行,
可以將許多張圖合成一張GIF動畫、
查詢GIF的size (包括各frame)、
切割、提取、加入frame到GIF動畫中、
壓縮GIF等。

以下是它的資訊:
Gifsicle
官網 :  http://www.lcdf.org/gifsicle/
Github : https://github.com/kohler/gifsicle

gifsicle只提供了三種壓縮輸出,
-O1, -O2, -O3,其中-O3效果最好,
例如 cmd 指令:
gifsicle -O3 froSrc.gif  -o toSrc.gif
但對於一些GIF可能無法再壓縮的更小。

這時就可以使用Giflossy來幫忙了。
Giflossy是一個在Github上fork了gifsicle的一個分支專案,
擴充了gifsicle的功能,加了一個參數,--lossy=XX
XX可選數字,越大壓縮比率越高,但也越失真,
可以用如下cmd指令來壓縮:
gifsicle -O3 froSrc.gif --lossy=30 -o toSrc.gif

以下是Giflossy的資料

Giflossy
官網 : https://kornel.ski/lossygif
Github : https://github.com/pornel/giflossy


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

Gifsicle和Giflossy的Github上都沒有提供給Windows現成的 exe 檔,
但有提供Makefile,可以利用Visual Studio 內建命令列視窗中的 make 工具來進行
編譯,
下載好Gifsicle或Giflossy後,如果有裝Visual Studio,可以打開"開發人員命令提示字元" ( Developer Command Prompt ),進到 gifsicle/src/ 內,打上
nmake -f Makefile.w32

就可以在當下路徑中看到編譯出來的 gifsicle.exe 了,
要注意的是它並沒有幫忙編譯 gifdiff.exe 。

如果有人沒有 Visual Studio,或是需要32位元的編譯版本,
我在這裡發現有人幫忙編譯好了 ,順便也放在這裡分享。


2017年5月29日 星期一

AngularJS的UI-Router練習

今天要來練習AngularJS很多人使用的外掛,UI-Router,UI-Router以各種不同的 State (狀態) 來管理AngularJS的路由,擴充了AngularJS原生路由的使用功能 (只單純以Url來組織路由),所以網站是以一個State到另一個State的方式去設計。

以下是今天演示的需求:

  1. 設計三個State, home、heroPanel、heroDetail,
    home為首頁,heroPanel顯示英雄(hero)的 id 和 name 的列表,點列表的某一個 hero 會進到heroDetail,顯示 Detail 頁面。
  2. 頁面結構分成三個區塊,header、body和footer,三者的內容皆會跟據State而有所改變。

成品就像下面影片這樣:




首先先來看一下設計的檔案結構:

index.html為主要頁面,且此例也只有一個頁面,並在同一頁中利用UI-Router切換內容。

因為這邊我用npm的方式下載安裝AngularJS和UI-Router,所以有package.json,AngularJS和UI-Router都裝在node_modules裡,當然自己去官網下載也OK。

app資料夾下的為主要JS程式,第一層以app開頭的JS為主要AngularJS Module及其相關設訂,代表著 "home" 的狀態。
其他的資料夾, heroPanel 和 heroDetail 代表 heroPanel 和 heroDetail 兩個狀態,為ui-view是body時的內容設定。
common  資料夾下放著跟 footer 和 header 兩個 ui-view相關的設定

main.css 為 header、 body、footer 標上外框以利識別,也為 class="active" 的元素標上底色以利識別現在狀態。

接下來來看各檔案裡的程式內容

2017年5月21日 星期日

Typescript + Webpack (or SystemJs) 練習

今天要來練習使用Typescript編寫Javascript,順便紀錄下如果JS Module的使用方式。

Typescript可以讓我們編寫的 ts 檔編譯成 js 檔,而如果有在 ts 檔裡撰寫Module的語法,
例如export, import等語句,Typescript可以依我們指定的module spec 來將 ts 檔編譯成不同
寫法的 js 檔,而可提供的有例如 amd, system, commonjs 等。

這裡會練習以下項目;

模組化 JS 執行工具:
SystemJs是一個可以執行JS Module的工具,
支援許多module spec,目前有
esm (ECMAScript Module),
cjs (CommonJS),
amd (Asynchronous Module Definition),
global (Global shim module format),
system (System.register or System.registerDynamic compatibility module format),
可以使用在node.js及Web應用中,在Web應用中,
可以在頁面上真接用 <script src="XXX/system.js"></script> 引入,
然後直接寫Javascript code 並指定程式進入點即可。

Webpack是一個功能強大的模組化打包工具,可以將多個資源(JS, CSS, images...)打包成一個檔案,可以解析例如commonjs的module spec,主要以命令列的方式使用,也可搭配其他外掛使用。

在這邊我們會先練習利用Typescript編譯 ts 檔,並且練習兩種不同的 module spec ,
system 和 commonjs, Typescript 對不同的 module spec 編譯出不同的 js 檔,
然後
system 我們會利用 SystemJs 來執行模組化的 js。
commonjs 我們會利用 Webpack 來打包編譯好的 js 檔成一個可執行的模組化 js。

Typescript IDE :
在這邊,可以準備一個有 Typescript 功能的 IDE,例如在這邊我使用 Visual Studio Code ,它有不錯的 Typescript 提供檢查功能及許多外掛,並且本身乾淨可以當一般的編輯器使用,當然也可以根據開發環境使用想應的工具外掛,例如 Netbeans 及 Eclipse (可以參考"Angular2簡易安裝使用 - Eclipse + Typescript plugin + System.js") 也有人提供Typescript外掛,享受使用 Typescript 的好處。
** Note **
Visual Studio Code 如果看不到JS、Typescript的提示文字,可以用"系統管理員"的方式開啟,應該就會出來了。

Typescript 的編譯:
在這邊為了了解最源頭的運作,我選擇使用最單純的方式,用 npm 安裝了 Typescript 後,直接用 tsc 指令配合 tsconfig.json 來執行編譯,許多開發環境的外掛,例如 Visual Studio Code 自己就有編譯Typescript的能力,不過其實最底層也都基本是呼叫 tsc 指令來完成工作。

簡易Server:
這邊使用了 lite-server 來在專案當下目錄建立簡易Server,因為Module JS的寫法會用到Ajax的呼叫,所以需要建立Server,當然要用其他例如webpack-dev-server、Netbeans, Eclipse 配合 Tomcat 之類的也是OK。

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

首先,請先安裝 node.js (自動有 npm),
在全局安裝 Typescript、Webpack
npm install typescript -g
npm install webpack -g

2017年5月14日 星期日

將網頁要引入的前端套件Code再到網頁中 - bower + gulp + wiredep + gulp-inject


bower可以很方便的下載安裝前端套件(JS、CSS等),而這些下載下來的套件可以利用
gulp-inject及wiredep配合gulp來執行自動化工作,把要用的套件引入程式碼加到網頁檔案的(html, jsp等)。

以下提供一個使用的範例,用到的有(當然要先裝node.js):
  1. bower : 用來管理前端套件(JS、CSS等)。
  2. gulp :用來執行自動化工作(task)。
  3. gulp-inject :gulp的外掛之一,可以用來將JS、CSS的引入Code寫到網頁檔案中。
    例如html中的 <script src=“xxx.js"></script>
  4. wiredep:跟gulp-inject一樣,不過是用來將bower安裝的套件引入Code寫到網頁當案中,會自動偵測被引入套件中的bower.json來獲知要引入的JS、CSS等,可以直接寫Code用node.js執行,也有提供gulp的使用方式。
安裝node.js後,進行以下步驟:

  1. 首先先來安裝bower、gulp到命令列模式打上指令進行全域安裝。
    node install bower -g
    node install gulp -g
  2. 接著再到已建立的專案的專案路徑下,用以下指令建立一個package.json來管理此專案用node.js安裝的套件(之後可以直接用node install安裝回全部要的套件)。
    node init
  3. 接著再到已建立的專案的專案路徑下執行gulp、gulp-inject、wiredep的安裝(非全域,只是當下專案會用到,但因為之後要寫require("gulp")的語法,所以gulp要在專案下再安裝一次),使用--save-dev來寫入到package.json的devDependencies(表示開發用)中。
    node install gulp --save-dev
    node install gulp-inject --save-dev
    node install wiredep --save-dev
  4. 使用以下指令建立一個bower.json來管理此專案用bower安裝的套件(之後可以直接用bower install安裝回全部要的套件)。
    bower init
  5. 接著使用bower來安裝前端套件,例如angularJS、angular-fancy-modal
    bower install angular --save
    bower install angular-fancy-modal --save
    安裝好的套件預設會被放到bower_components資料夾中。
  6. 建立一個網頁(index.html)、一個JS檔案、一個CSS檔案做測試,
    然後再建立一個檔名為gulpfile.js的檔案,裡面會寫上gulp的自動化工作,
    現在的檔案結構應該會例如像下面這樣:

  7. 先在index.html寫下以下程式碼,其中,
    bower:css、bower:js和endbower是用來告訴wiredep要在哪加進JS、CSS引入程式碼。
    inject:css、inject:js和endinject是用來告訴gulp-inject要在哪加進JS、CSS引入程式碼。
    <!DOCTYPE html>
    <html>
        <head>
            <title>TODO supply a title</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            
            <!-- bower:css -->
            <!-- endbower -->
            
            <!-- bower:js -->
            <!-- endbower -->
            
            <!-- inject:css -->
            <!-- endinject -->
            
            <!-- inject:js -->
            <!-- endinject -->
        </head>
        <body>
            <div>TODO write content</div>
        </body>
    </html>
  8. 接著在gulpfile.js裡寫下如下程式碼:
    var gulp = require("gulp");
    var wiredep = require("wiredep").stream;  //wiredep提供給gulp使用的方式,使用stream來讓gulp能使用wiredep
    var gulpInjct = require("gulp-inject");
    
    //設定gulp工作(task)
    gulp.task("injectScriptCssTask", function () {
      //用wiredep來在index.html加入從bower install來的JS、CSS套件
      //Use wiredep to inject script, css installed by bower.
      gulp.src("index.html").pipe(wiredep({
                                optional : "configuration",
                                goes : "here"
                             }))    
      //用gulp-inject來在index.html加入自已指定的JS、CSS套件
      //Use gulp-inject to inject our own script, css.  
                            .pipe(gulpInjct(gulp.src(['./app/**/*.js', './app/**/*.css'],
                                                     {read : false}  //因為不用讀取JS、CSS檔的內容,只是想知道它們的path,
                                                                    //所以設定 {read : false} 增進效能
                                                    ),
                                            {relative : true}))  //設置{relative : true}指定用相對如徑的方式來寫入引人程式碼 
                            .pipe(gulp.dest("."));
    });
  9. 最後使用命令列視窗,到專案目錄執行gulp的工作(此例工作名稱為injectScriptCssTask):
    gulp injectScriptCssTask
    再到index.html中,應該就可以看到JS、CSS引入碼被成功地寫入到檔案中了:
    <!DOCTYPE html>
    <html>
        <head>
            <title>TODO supply a title</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            
            <!-- bower:css -->
            <link rel="stylesheet" href="bower_components/angular-fancy-modal/dist/angular-fancy-modal.css" />
            <!-- endbower -->
            
            <!-- bower:js -->
            <script src="bower_components/angular/angular.js"></script>
            <script src="bower_components/angular-fancy-modal/dist/angular-fancy-modal.js"></script>
            <!-- endbower -->
            
            <!-- inject:css -->
            <link rel="stylesheet" href="app/css/main.css">
            <!-- endinject -->
            
            <!-- inject:js -->
            <script src="app/js/app.js"></script>
            <!-- endinject -->
        </head>
        <body>
            <div>TODO write content</div>
        </body>
    </html>
原始碼下載:
AngularJsUI-RouterTest.7z

參考資料:
  1. gulp-inject
  2. wiredep
  3. Gulp 學習 2 - 打包壓縮 CSS 與 JS
  4. Gulp 學習 3 - 打包壓縮 HTML

2017年5月12日 星期五

AngularJS 自製有Service、directive的外掛module練習

今天要來製作一個自製的AngularJS外掛,用來練習AngularJS的模組化特性。

要實作的是一個簡單的Lightbox (或Modal),要練習的點是:

  1. 此外掛為一個Module,可引入至其他的AngularJS Module。
  2. 此Module提供一個Service,可以由引入此Module的程式呼叫使用。
  3. 此Module提供一個Directive,Directive可以與參數雙向(或單向,看需求)綁定,並且Directive可以呼叫上述Service來使用。

目標結果為:

  1. Lightbox外掛提供一個Service,可以開始及關閉Lightbox,並且可以在開啟Lightbox時提供要顯示在Lightbox中的文字。
  2. Lightbox外掛提供一個Directive,Directive經過設計,有提供一個按鈕,其被按下會執行Service的開啟lightbox功能,Directive可以給入參數,設定Lightbox中要顯示的文字。
  3. 開啟的Lightbox提供一個Close按鈕,按下可以關閉Lightbox。
  4. 一次只會有一個Lightbox被開啟。

其成品就像如下視頻那樣:


首先先 看一下檔案結構:


  1. /index.html:主要的網頁頁面。
  2. /js/index.js :index.html使用的JS,主要頁面的AngularJS邏輯。
  3. /js/angular.js : 要引入使用的angularjs。
  4. /js/myAngularJsPlugin.js : 要撰寫的自製Lightbox AngularJS Plugin。
程式碼如下,相關的說明都已經寫在註解中了:
  1. /index.html :
    <!DOCTYPE html>
    <html>
        <head>
            <title>TODO supply a title</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
            <script src="js/angular.js"></script>
            <script src="js/myAngularJsPlugin/myModalModule.js"></script>
            <script src="js/index.js"></script>
        </head>
        <body>
            <div ng-app="myApp" ng-controller="myController as myCtrl">
                可自行輸入要在Modal內顯示的文字
                <div style="margin-bottom: 20px;">
                    <input type="text" ng-model="myCtrl.modalText"/>
                </div>
                在Controller裡呼叫自製Module提供的Service來開啟Modal
                <div style="margin-bottom: 20px;">                
                    <button ng-click="myCtrl.openModal(myCtrl.modalText)">Open Modal manually</button>
                </div>
                
                使用自製Module提供的Directive來開啟Modal
                <div>                
                    <my-modal-directive modal-text="myCtrl.modalText"></my-modal-directive>
                </div>
            </div>
        </body>
    </html>
  2. /js/index.js :
    angular.module("myApp", ["myModalModule"]);
    
    angular.module("myApp").controller("myController", ["myModalService", function(myModalService){
        var self = this;
        self.modalText = "測試文字";
        self.openModal = openModal;
        
        function openModal(modalText){
            myModalService.open(modalText);
        }
    }]);
  3. /js/myAngularJsPlugin.js:
    //Module
    angular.module("myModalModule", []);
    //Service
    angular.module("myModalModule").factory("myModalService", ["$rootScope", "$compile", function ($rootScope, $compile) {
            //設計一次只能有一個Modal開啟
            var scope;
            var modal;
            
            var $body = angular.element(document).find('body');
            var isModalOpened = false;
            //modal template中有跟angularjs scope有關的程式碼,
            //須要使用$compile編譯並與scope連結才能有作用
            var modalTemplate = '<div style="position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; background-color: rgba(158, 158, 158, 0.68); ">' +
                                    '<button ng-click="close()" >Close</button>' +
                                    '<div style="display: flex; align-items: center; justify-content:center; width: 100%; height: 100%;">' +
                                        '{{textContent}}' +
                                    '</div>' +
                                '</div>';
            //functions
            function open(textContent) {
                if (!isModalOpened) {
                    //創建暫時的scope
                    scope = $rootScope.$new();
                    //在scope中設置close函式
                    scope.close = close;
                    //在scope中設置textContent
                    scope.textContent = textContent;
                    //使用$compile來編譯modalTemplate,得到link函式,執行link函式並代入scope來與scope連結,
                    //最後得到連結好的DOM
                    modal = $compile(modalTemplate)(scope);
                    //將得到的DOM加進<body>中
                    $body.append(modal);
                    //設置modal opened狀態
                    isModalOpened = true;
                }
    
            }
            function close() {
                if (isModalOpened) {
                    //將Modal DOM移除
                    modal.remove();
                    //移除暫時scope
                    scope.$destroy();
                    //設置modal opened狀態
                    isModalOpened = false;
                }
            }
            //自製Service對外提供的API
            return {
                open: open,
                close: close
            };
        }]);
    //Directive
    angular.module("myModalModule").directive("myModalDirective", ["myModalService", function (myModalService) {
            return {
                restrict: "E",
                replace: true,
                scope: {
                    //自製Directive可接受modalText參數(為表達示)
                    modalText: "=modalText"
                },
                template: generateTemplate,
                link: function ($scope, $elm, $attrs, $ngModel) {
                    $scope.openModal = myModalService.open;                
                    /* 以上寫法等同以下,上面雖簡潔但下面寫法可讀性比較好
                    $scope.openModal = function (modalText) {
                        myModalService.open(modalText);
                    }
                    */
                }
            };
    
            function generateTemplate($elm, $attrs) {
                 //用function回傳template,也可以直接用字串不用函式
                return '<button ng-click="openModal(modalText)">Open Modal by Directive</button>';
            }
        }]);
原始碼下載:
angularJS-servicePractice.7z


參考資料:
  1. 深入解析 CSS Flexbox

2017年4月30日 星期日

能找到最內層的tag的正規表示法

今天要來介紹一個能找到最內層一組tag的正規表示法,

例如有一個需求如下:
tag格式為[quotePostId=XX]YY[/quotePostId],XX填數字,YY填文字,
可嵌套,如何從以下tag嵌套中找出最內層的XX和YY:
[tagId=1]kk[tagId=2][tagId=3]kk[/tagId]kk[/tagId][/tagId]

這裡提供一個解法,匹配的Group1即為XX、Group2為YY
\[tagId=(\d+)\](?=((?:[^\[]|\[(?!(?:tagId=\d+\]|\/tagId\])))*))\2\[\/tagId\]

下面來做解釋:
思路是:

  1. 必須符合[tagId=XX]YY[/tagId]。
  2. XX必須為非空值之數字。
  3. YY可為空值或任何字元。
  4. YY不可含有[tagId=??]或[/tagId],因為含有即代表此次匹配YY不為最內層tag之內容。

實作步驟(???為還未確定的部份,用紅色標色,當下確定的地方用藍色標色,之前確定的用黑色標色):

  1. 首先因為必須符合[tagId=XX]YY[/tagId],即XX必須為非空值之數字,所以先寫出一開始的正規表示法。
    \[tagId=(\d+)\](???)\[\/tagId\]
  2. 歩驟1的???需要符合條件(思路4)才須匹配,否則匹配不成功,因為x(?=y)這個語法不會把y算在匹配成功的字串內,所以用\2來裝Group2選到的YY放到tag中間。
    \[tagId=(\d+)\](?=(???))\2\[\/tagId\]
  3. YY可為空值,所以在Group2裡面放一個不記Group編號的Group,然後此Group可以0到多個。
    \[tagId=(\d+)\](?=((?:???)*))\2\[\/tagId\]
  4. 開始匹配YY,分成碰到非左中括弧( [ )、或是左中括弧的兩種情況。
    當是左中括弧時,根據後面接的字串決定要不要匹配。
    \[tagId=(\d+)\](?=((?:[^\[]|\[(???))*))\2\[\/tagId\]
  5. 如是是左中括弧時,根據後面接的字串情況決定要不要匹配,不匹配的情況有兩種,
    第一種:為[tagId=x]
    \[tagId=(\d+)\](?=((?:[^\[]|\[(?!(?:tagId=\d+\]|???)))*))\2\[\/tagId\]
    第二種:為[/tagId]
    \[tagId=(\d+)\](?=((?:[^\[]|\[(?!(?:tagId=\d+\]|\/tagId\])))*))\2\[\/tagId\]

最後我們完成了正規表示法:
\[tagId=(\d+)\](?=((?:[^\[]|\[(?!(?:tagId=\d+\]|\/tagId\])))*))\2\[\/tagId\]

以下為可測試的線上工具:

  1. 圖示化正規表示法
  2. 正規表示法線上實驗,可以檢查Group1、Group2有沒有取到。

2017年4月29日 星期六

Instagram

今天要介紹的是如何取得Instagram的Access Token,及
用Access Token來取得使用者的資訊,如發佈的貼文(Instagram的貼文都是圖片或影片)圖片url等。

今天的需求是:

  1. 想要取得自己在Instagram上面的貼文圖片。
  2. 想要將取得的貼文圖片展示在網頁上。

使用技術:

  1. JSP
  2. Jquery

先講Instagram 官方步驟

  1. 一開始如果沒有Instagram帳號,請註冊一個。
  2. 進入Instagram的developer console,填好相關資訊註冊成為Developer。
  3. 自己建立一個網頁,假設網址為REDIRECT-URI
  4. Manage Clients建立一個App,其中"Valid redirect URIs"填上步驟3的REDIRECT-URI
  5. 進入
    https://api.instagram.com/oauth/authorize/?client_id=CLIENT-ID&redirect_uri=REDIRECT-URI&response_type=code
    其中 CLIENT-ID 為已建立之App的 CLIENT-ID , REDIRECT-URI 即為自己建立網頁的網址。
  6. 待使用者登入並授權以後,會被引導至 REDIRECT-URI 頁面,並會以GET方式在Url後面帶上一個 code 參數。
  7. 這個 code 只能用一次,
    以以下參數
    client_id : App的client_id
    client_secret : App的client_secret
    grant_type : "authorization_code"
    redirect_uri : App的redirect_uri
    code : 剛剛得到的code
    請求 https://api.instagram.com/oauth/access_token,
    成功的話會在Response中得到access_token的參數,即我們要PO文資訊時需要的access_token。
  8. 要到access_token以後,就可以用來要各種使用者的資料了,在Developer console的左邊"Endpoints"中有提供各種Endpoints,這邊我們使用下面這個就好:
    https://api.instagram.com/v1/users/self/media/recent/?access_token=ACCESS_TOKEN
    請在ACCESS_TOKEN填上剛要到的,代表使用者的ACCESS_TOKEN,成功的話會回傳使用者最近的照片PO文資料。

下面以實測的程式碼來演示,

2017年2月26日 星期日

Chrome WebPush - node.js (客製化訊息 - 需加密)

Web Push

之前介紹的Google Chrome Web Push (客製化訊息 - 需加密)是使用Java來傳送加密的客製化訊息,其中有提到Google有提供了一個node.js版本的webpush程式,其中已經把加密的方法封裝好了,可以很容易地幫助我們傳送客製化的Web Push訊息。

在使用之前記得把node.js安裝好,可以去node.js官網下載適合版本安裝。

Google製作的webPush project的github連結在這,web-push
要安裝只要下以下指令即可:
npm install web-push --save

即著在同一個資料夾建立一個webPushTest.js來測試,其中內容如下,需要說明的部份已寫在註解中:

webPushTest.js:
const webpush = require('web-push');  //引入web-push模組

webpush.setGCMAPIKey('XXXXXXXXXXXX');  //打入FireBase Server Key或Google API Key

// This is the same output of calling JSON.stringify on a PushSubscription
const pushSubscription = {
  endpoint: 'XXXXXX',  //webpush註冊後得到的endpoint,前面會跟據Webpush Server而不同,
                       //例如Google可能是https://updates.push.services.mozilla.com/wpush/v1/,
                       //FireFox可能是https://updates.push.services.mozilla.com/wpush/v1/
  keys: {
    auth: 'XXXXX',  //加密金鑰的其中一個,auth
    p256dh: 'XXXXX'  //加密金鑰的其中一個,p256dh
  }
};

webpush.sendNotification(pushSubscription, '{"myText": "Hugo Test", "myIcon": "https://www.vermontteddybear.com/media/wysiwyg/VTB-CMS-Content/featuredcat-KBKF36004-20170119.jpg"}');

2017年2月19日 星期日

java抓取网络图片放到本地

這篇紀錄了如何使用Java獲取網路上的檔案,程式碼很簡單,直接把程式碼寫出

2017/10/11 更新,寫了新的一篇,包含了HTTPS,User-Agent,Content-Type等更詳細的
細節
以Java 由Url下載圖

package readFileFromUrlTest;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ReadFileFromUrlTest {

    public static void main(String[] args) {
        ReadFileFromUrlTest readFileFromUrlTest = new ReadFileFromUrlTest();
        String url = "https://www.vermontteddybear.com/media/wysiwyg/VTB-CMS-Content/featuredcat-KBKF36004-20170119.jpg";
        String filePath = "D:\\xxx.jpg";
        try {
            readFileFromUrlTest.readFileFromUrl(new URL(url), new File(filePath));
        } catch (IOException ex) {
            Logger.getLogger(ReadFileFromUrlTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void readFileFromUrl(URL url, File file) throws IOException {
        BufferedInputStream bufferedInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            bufferedInputStream = new BufferedInputStream(url.openStream());
            fileOutputStream = new FileOutputStream(file);
            int data;
            //從串流讀取資料寫到檔案中
            while ((data = bufferedInputStream.read()) != -1) {
                fileOutputStream.write(data);
            }
        } catch (IOException ex) {
            throw ex;
        } finally {
            //關閉串流
            try {
                if (file != null) {
                    fileOutputStream.close();
                }
                if (bufferedInputStream != null) {
                    bufferedInputStream.close();
                }
            } catch (IOException ex) {
                throw ex;
            }
        }
    }

}


參考資料:

  1. java抓取网络图片放到本地


2017年2月18日 星期六

WebSocket - Tomcat v8.0+ - 簡單的聊天室

WebSocket可以在伺服器和瀏覽器之間建立雙向通訊,今天要來用
WebSocket來實現一個簡單的聊天室做範例,紀錄一下如何使用WebSocket
伺服器採用Tomcat v8.0+、JDK 1.8,IDE 為 NetBeans

需求:

  1. 網頁上面可以輸入名稱登入聊天室。 登入後聊天室會出現登入訊息。
  2. 登入後可在訊息框中輸入訊息並送出,送出後聊天室會出現訊息。
  3. 聊天室出現訊息指的是,任何已登入聊天室的人都會看到訊息。
  4. 登入後的人只能看到登入後有人打的訊息。


先來看一下檔案結構:


2017年2月17日 星期五

正規表示法 - 幫數字字串的每三個位數標上逗號

碰到一個需求是將數字字串的每三個位數標上逗號,
例如:
1234567 ==> 1,234,456

發現原來正規表示法也可以辦到,特別在這邊做個紀錄。

以Javascript為例,以下的方法numberWithCommas()可以將輸入的整數字串每隔3位數標上逗號

function numberWithCommas(x) {
  return x.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

使用的正規表示法為
\B(?=(\d{3})+(?!\d))

先來看看它的圖型化表示

圖型化表示連結


解釋:
我們要匹配的是從低位數數起,每三個位數的位置(是位置,不是字元),但不包括左邊沒數字的位置
例如123456789,匹配的位置有
3跟4這間、6跟7中間,但1左邊不應該要有。



\B   :   非字元邊界,在此列中即左邊不應該有空白

x(?=y)   :   x接著y才匹配成功,並且y不列入匹配成功的字串中,只匹配出x,如果x沒寫,則 匹配出來的為位置

\d{3}  :   3個數字字元

x(?!y)   :   x後面不跟著y才匹配成功

所以組合出來的就是

  1. 左邊不要空格(即不要左邊界) ==>  \B
  2. 比對後面符合我們要的條件的位置  ==> \B(?=)
  3. 條件之一,1到多個的,3個為一組的數字字元  ==>  \B(?=(\d{3})+)
  4. 條件之二,條件之一的最後面不可是數字字元  ==>  \B(?=(\d{3})+(?!\d))
最後,找到位置以後,把位置替換成逗號就行了。



node.js POST 及 GET的讀取

使用Node.js 架Server時,我們常需要從Client端接收GET或POST方法傳來的參數,
有兩個module可以利用,url及querystring

下面是簡單的範例:
(function(){
   //獲取modules
  var http = require('http');   //架Server用
  var url = require('url');      //解析url,包括GET參數
  var querystring = require('querystring');     //可以用來解析POST的參數語句

  //建立Server
  var server = http.createServer(function(request, response) {
    //得到url資訊
    var urlInfo = url.parse(request.url, true);
    //取得url的path
    var pathname = urlInfo.pathname;
   
    response.writeHead(200, {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "http://XXX"  //如果要當做api給Client利如用Ajax呼叫的
                                                                                   //話,要加跨域的Header
    });
    //判斷是不是要處理的path name
    if (pathname === "/somePathName") {
       var GET_data = urlInfo.query;  // 得到GET的參數資訊
       console.log(GET_data);

       //準備得到POST的參數資訊
       var POST_data = '';     //POST_data 用來存POST資訊
       //POST的資訊息會不斷送進來,不斷送的時候觸發data事件,結束觸發end事件,
       // 綁在request上面以接收POST參數資訊
       request.on("data", function(data){
            POST_data += data
       });  
       request.on("end", function(){
           //可以利用querystring module來將POST參數資訊轉成物件
           var postDataObject = querystring.parse(POST_data );
           console.log(postDataObject);
       });
    } else {
      //
    }
  });

  設置Server的Port
  server.listen(3000);

  console.log('Server跑起來了,現在時間是:' + new Date());
})();

這邊要注意的是,POST的資料不是一次性的得到,而是不斷地傳送進Server,所以我們在request上綁定了"data"及"end"事件來監聽POST資料的傳遞狀態,在"data"事件中不斷地組合POST參數資料,當資料傳送後"end"事件會被觸發,這時才會得到完整的POST參數資料。

參考資料:
  1. Node.jS初學者筆記(1)-用GET傳送資料
  2. Node.jS初學者筆記(2)-用POST傳送資料

2017年2月16日 星期四

Node.js 連接 Sql Server -

這邊紀錄如何使用node.js連接Sql Server的範例,

首先我使用的node.js module是mssql,它有npm網站連結Github連結
只要用node.js 打上
npm install mssql
即可安裝

以下是範例程式碼,
var config = {
    user: 'XXX',
    password: 'XXX',
    server: 'XXX', // You can use 'localhost\\instance' to connect to named instance
    port: 0000,
    database: 'XXX'
    /*
    ,options: {
        encrypt: true // Use this if you're on Windows Azure
    }
    */
}
//獲取連線
var connection = new sql.Connection(config); 
connection.connect(function() {
 // Query 範例   
        //建立Request來進行query,query會回傳Promise,
        //call back function裡的recordset是一個陣例,為物件的集合,
        //物件各Key為Query結果的欄位名稱,Value為值
    new sql.Request(connection).query('SELECT * FROM someTable').then(function(recordset) {
  var i = 0;
  //印出各row的各個欄位
  for (i=0; i < recordset.length; i++){
   console.log(recordset[i].column1);
   console.log(recordset[i].column2);
   console.log(recordset[i].column3);
   connection.close();  //關閉連接
  }
 }).catch(function(err) {
  // ... error checks
  console.log(err);
  connection.close();  //關閉連接
 });
});

2017年1月28日 星期六

Ardulink2使用注意事項

之前有個專案使用了Arduino + Ardulink 2,發現了一些需要注意的地方,特別在這邊做個紀錄。


1.對於連續的指命呼叫,可能會送出錯誤的指令給Arduino,例如兩個指令被合併成一個,或指今被截斷,可以在指令中間隔1秒鐘(實測覺得1秒比較穩定)。

2.要從Arduino中送回資料給Java,可以使用alp://cevnt/XXX指令,例如:

Serial.print("alp://cevnt/");
Serial.print(inputString);
Serial.print('\n'); // End of Message
Serial.flush();

Java端可以用CustomListener的方法customEventReceived,ce.getMessage()來取得XXX。

Link.addCustomListener(new CustomListener() {
 @Override
 public void customEventReceived(CustomEvent ce) {                                    
  System.out.println(ce.getMessage());
 }
});

3.要從Java端送資料給Arduino,可以用Link的方法sendCustomMessage(String message)來傳送給Arduino,例如

Link.sendCustomMessage("XXX")

在Arduino中會得到如下指令

alp://cust/XXX

解析後就可以得到送給Arduino的資料