2012-10-26

Javascript 與 Java Applet 合作

以前只知道可以用 Javascript 呼叫 Java Applet,但都沒有動機去嘗試,今天有了。

Javascript 不能讀寫本機的檔案,這是可以確定的,另外能不能執行本機的程式(例如 ipconfig),大概也不行吧,而這些 Java 都可以!

合作的原理很簡單,在 Applet 所在的 HTML 裡,Javascript 可以呼叫 Applet 帶來的 Java class,至於為什麼可以呼叫,這個我還不知道,就先擱下吧。

先來寫 Java Applet。
package idv.neil.jsa;

import java.applet.Applet;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.Date;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * @author Neil Chan
 * @date 2012/10/24
 */
@SuppressWarnings("serial")
public class JSAApplet extends Applet {

  /**
   * Javascript 可以呼叫 所有public 變數或 method
   */
  public String message = null;

  public String getMessage() {
    return "我說: " + this.message;
  }

  /**
   * Javascript 可以呼叫其他 class
   */
  public Stepper getStepper() {
    return new Stepper();
  }

  /**
   * 讀寫本機檔案
   */
  public String readText(String filePath) {
    String text;
    try {
      // 因為讀 user home 得有其他權限,這邊先跳過
      // String userHome = System.getProperty("user.home");
      // String filePath = userHome + "/HelloJSA.txt";
      File file = new File(filePath);
      // 不存在就新增
      if (!file.exists()) {
        FileUtils.touch(file);
      }
      // 讀入檔案
      text = FileUtils.readFileToString(file);
      // 更新內容
      if (StringUtils.isBlank(text)) {
        text = new Date().toString();
      }
      else {
        text = text + "\n" + new Date().toString();
      }
      // 回寫檔案
      FileUtils.writeStringToFile(file, text);
    }
    catch (Exception e) {
      text = "Error >>> " + e.getMessage() + "\n";
    }
    return text;
  }

  /**
   * 執行外部程式,以 ipconfig 為例
   */
  public String exec() {
    StringBuilder sb = new StringBuilder();
    try {
      Process pr = Runtime.getRuntime().exec("ipconfig");
      BufferedReader input = new BufferedReader(new InputStreamReader(pr.getInputStream(), "MS950"));
      String line = null;
      while ((line = input.readLine()) != null) {
        sb.append(line);
      }
      int exitVal = pr.waitFor();
      sb.append("Exit code - " + exitVal);
    }
    catch (Exception e) {
      e.printStackTrace();
      sb.append("Error: " + e.getMessage());
    }
    return sb.toString();
  }
}
如果不需要的話,不用做任何 UI,除此之外,就是一般 Java class。

為了展示 Javascript 與 Applet 的合作本事,刻意從 Applet 抽出功能到其他的 class。
public class Stepper {

  private int from = 1;
  private int to = 10;
  private int step = 1;

  /**
   * 設定
   */
  public void setup(int from, int to, int step) {
    this.from = from;
    this.to = to;
    this.step = step;
  }

  /**
   * 加總
   */
  public int sum() {
    Integer[] nums = this.getNums();
    int sum = 0;
    for (int num : nums) {
      sum += num;
    }
    return sum;
  }

  /**
   * 取得陣列,Javascript 不支援取得 object array
   */
  public Integer[] getNums() {
    List<Integer> list = new ArrayList<Integer>();
    for (int i = this.from; i <= this.to; i += this.step) {
      list.add(i);
    }
    return list.toArray(new Integer[] {});
  }

  /**
   * 取得陣列,Javascript 只支援取得 primitive array
   */
  public int[] getPrimitiveNums() {
    List<Integer> list = new ArrayList<Integer>();
    for (int i = this.from; i <= this.to; i += this.step) {
      list.add(i);
    }
    int[] is = new int[list.size()];
    for (int i = 0; i < list.size(); i++) {
      is[i] = list.get(i);
    }
    return is;
  }
}
事後測試發現,只有 Chrome 不支援 Object array,Firefox 與 IE9 都支援。

再加入一個完全沒關聯的 class。
public class DateHelper {

  /**
   * Javascript 可以存取靜態變數
   */
  public static String LABEL = "日期:";

  public String getDate() {
    return LABEL + " " + new SimpleDateFormat("yyyy/MM/dd").format(new Date());
  }

}
Java 部份結束,再用 Maven 打包成 jar 檔就可以了。

網頁部份有三個主角,第一個就是剛產生的 jar 檔,另外除了 html 外,還要有一個 jnlp。

HelloJSA.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="en-US">
<head>
<title>HelloJSA</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="http://www.java.com/js/deployJava.js"></script>
<script type="text/javascript">
  $(function() {
    /* 放這不能用
    var attributes = {
      id: 'jsaApplet', // 供 javascript 使用
      code: 'idv.neil.jsa.JSAApplet',  
      width: 1, 
      height: 1
    };
    var parameters = {jnlp_href: 'HelloJSA.jnlp'};
    deployJava.runApplet(attributes, parameters, '1.6');
     */
  });
  function callApplet() {
    // 要值
    var from = prompt('輸入起值: ', '1');
    var to = prompt('輸入迄值 : ', '10');
    var step = prompt('輸入間隔 : ', '1');

    // 回傳物件,呼叫另一個 class 做事情
    var stepper = jsaApplet.getStepper();
    stepper.setup(from, to, step);

    // 回傳陣列
    var nums = stepper.getNums();
    alert("Integer陣列 - " + nums.length + ',' + arrayToString(nums));
    var nums = stepper.getPrimitiveNums();
    alert("int陣列 - " + nums.length + ',' + arrayToString(nums));

    // 回傳 primitive
    var sum = stepper.sum();
    alert("加總 - " + sum);

    // 直接存取 applet 的 public 變數
    jsaApplet.message = prompt('輸入訊息: ', '神奇的 Javascript 與 Applet!!');
    // 呼叫 applest 的 public method
    var message = jsaApplet.getMessage();
    alert(message);

    // 使用 Packages 呼叫靜態 class 與 method
    jsaApplet.Packages.java.lang.System.out.println("這樣去哪看?");

    // 使用 Packages 呼叫靜態變數
    jsaApplet.Packages.idv.neil.jsa.DateHelper.label = "今天是 ";

    // 使用 Packages new 一個 class
    var dateHelper = new jsaApplet.Packages.idv.neil.jsa.DateHelper();
    var dateStr = dateHelper.getDate();
    alert(dateStr);
  }
  function arrayToString(nums) {
    var s = '[';
    try {
      for ( var i = 0; i < nums.length; i++) {
        s += (nums[i] + ',');
      }
      for ( var a in nums) {
        s += (nums[a] + ',');
      }
    }
    catch (e) {
      alert(e);
    }
    s += ']';
    return s;
  }
  function ioFile() {
    var filePath = prompt('Enter File Path:', 'd:/HelloJSA.txt');
    alert(jsaApplet.readText(filePath));
  }
  function exec() {
    alert(jsaApplet.exec());
  }
</script>
</head>
<body>
  <script type="text/javascript">
      // 只能放在這!!!不能放在 $(function(){...})
      var attributes = {
        id : 'jsaApplet', // 供 javascript 使用
        code : 'idv.neil.jsa.JSAApplet',
        width : 1,
        height : 1
      };
      // 加上 ?v=xxx 可以防止瀏覽器 cache applet jar
      // 除了這裡,jnlp 裡 jar href 也要加上 ?v=xxx
      var parameters = {
        jnlp_href : 'HelloJSA.jnlp?v=11'
      };
      deployJava.runApplet(attributes, parameters, '1.6');
    </script>
  <h1>HelloJSA Applet</h1>
  <p>
    <a href="javascript:; " onclick="callApplet();">Call applet...</a>
  </p>
  <p>
    <a href="javascript:; " onclick="ioFile();">Read and write file...</a>
  </p>
  <p>
    <a href="javascript:; " onclick="exec();">Ipconfig...</a>
  </p>
</body>
</html>

HelloJSA.jnlp
<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+" codebase="" href="">
    <information>
        <title>HelloJSA</title>
        <vendor>Sun</vendor>
    </information>
    <resources>
        <j2se version="1.6+" href="http://java.sun.com/products/autodl/j2se"/>
        <jar href="HelloJSA.jar?v=11" main="true" />
    </resources>
    <applet-desc 
         name="Hello JSA Applet"
         main-class="idv.neil.jsa.JSAApplet"
         width="1"
         height="1">
     </applet-desc>
     <update check="background"/>
</jnlp>
完工!

開發過程最困擾的就是瀏覽器將 jar 檔 cache,找不到方法清掉,只能用改網址的消極方式,或者使用官方的作法,但這不是使用者可以接受的方法,後來發現,可以在上述兩個地方加上 ?v=xxx 的方式強迫下載新版本的 jar。

最後發生無法解釋的事了,理論上 Java Applet 預設是不能讀取本機的檔案的,除非有使用者的授權,也就是要將 Applet Sign 過才行,Sign Applet 的參考文章在這

精簡來說就是兩個指令:
keytool -genkey -alias HelloJSA -validity 365
jarsigner HelloJSA.jar HelloJSA
兩個指令都可以在 JAVA_HOME/bin 下找到,第一個用來產生 .keystore,放在 USER_HOME 目錄裡,第二個就是 Sign Applet,會用到第一個指令輸入的密碼。

剛開始開發時,因為 Applet 沒 sign 過,所以在讀寫檔案時出現「access denied java.io.filepermission read」的錯誤訊息,sign 過之後就沒事了,但是在做這邊筆記時,想要再看一次錯誤訊息,所以使用未 sign 的 Applet,結果還是可以執行,哇咧,想出錯也不行,看來還是有很多我不知道的秘密在!
---
---
---

沒有留言:

張貼留言