2015/08/17

Node.js と MongoDB の開発環境構築メモ

最近流行りの Node.js と MongoDB の組み合わせで開発環境を作ったメモ。

DLとインストール

Node.js

Node.js の本体。

  • https://nodejs.org/download/
    • Installer を選択
    • Binary は Node.exe だけで npm が無い

インストールはインストーラに従えばOK。

Eclipse プラグイン

nodeclips という プラグインをインストールする。
express というフレームワークも一緒に入る。

  • http://dl.bintray.com/nodeclipse/nodeclipse/
    • とりあえず配下全部

プラグイン、インストール後に環境設定が必要。

注:node.exe以外はデフォルトの設定を使うこと。

MongoDB

MongoDB 本体はDLしてインストールするだけ。 起動方法法は後述。

  • https://www.mongodb.org/downloads
    • OS は 2008 R2+ にした

MongoDB の教本(通称:薄い本)のPDFをDLして一読しておく。

プロジェクトの作成

素の Node.js プロジェクトと Expless フレームワークのプロジェクトが作れるが今回は Express プロジェクトを作成する。
Express は Node.js を HTTP サーバとするためのフレームワーク。

  • メニューから 「新規 > プロジェクト > その他 > ノード > Node Express Project」 を選択。
  • テンプレートエンジンは好きなほうを選択
    • jade はHTML省略記法、ejs はJSPライクな感じ

作成されたプロジェクトの構成は以下

  • public/** : 静的なHTMLファイル
  • routes/** : JSのロジック
  • views/** : テンプレート
  • app.js : アプリのメイン
  • package.json : アプリの定義ファイル

app.js のメニューから 「実行 > Node Application」 でアプリが起動する。
ブラウザから http://localhost:3000 にアクセスして Welcom ページが表示されればOK。

Node.js と MongoDB の接続

MongoDB サーバの起動

mongod.exe を実行するだけでサーバは起動するが WindowsだとDBフォルダのドライブ指定を明示したほうが良いので起動バッチを作成する。 MongoDBのインストールフォルダに置いておく。

boot-mongod.bat: サーバ起動用

set BASEDIR=%~dp0
cd /d %BASEDIR%
%BASEDIR%\Server\3.1\bin\mongod.exe --dbpath %BASEDIR%\DB

clinet-mongo.bat: シェル起動用

set BASEDIR=%~dp0
cd /d %BASEDIR%
%BASEDIR%\Server\3.1\bin\mongo.exe

クライアントのシェルを起動して繋がればOK。

ドライバの準備

ドライバは Node.js のモジュール 'mongodb' を使用する。
'mongoose' とかも有るようだけどとりあえず標準を使う。

package.json の dependencies に "mongodb" を追加する。

{
  "name": "node-ex-test",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.6",
    "jade": "*",
    "mongodb": "*"     <--これ追加
  }
}

変更後にメニューから「実行 > npm install」を行うとmongodbのドライバがDLされる。
他のモジュールも同じように追加できる。

ドライバのAPIは以下を参照。

接続サンプルコード

MongoDBにコレクション accounts を作成しユーザ登録と認証を行う簡単なサンプル。
POSTのパラメータに user,pass が設定されるAPIとしている。

簡単な仕様:

メソッドURLリクエスト本文レスポンス
登録POST/accounts/registeruser={ユーザ名}&pass={パスワード}200 or 500
認証POST/accounts/loginuser={ユーザ名}&pass={パスワード}200 or 401

routes/accounts.js:

var MASTER_PASSWD = "ce39325bc505e74089f7e176a380370f";
var mongodb = require('mongodb');
var crypto = require('crypto');
var accounts;

mongodb.MongoClient.connect("mongodb://localhost:27017/test", function(err, database) {
    if (err != null) console.error(err);
    accounts = database.collection("accounts");
});

function pass2hash(pass, solt) {
    var hash = crypto.createHash('sha1');
    hash.update(MASTER_PASSWD).update(pass).update(solt);
    return hash.digest('base64');
}

exports.register = function register(req, res) {
    var solt = crypto.createHash('sha1').update(""+ new Date()).digest('base64');
    var hash = pass2hash( req.body.pass, solt);
    accounts.update({name: req.body.name},
        {name: req.body.name, hash: hash, solt:solt},
        {upsert: true},
        function(err, result) {
            if (err == null && result.result.n == 1) {
                console.log(item);
                res.send("OK");
            } else {
                console.error(err);
                res.statusCode = 500;
                res.send("NG");
            }
        }
    );
}

exports.login = function login(req, res) {
    accounts.findOne({name:{$eq: req.body.name}}, function(err,doc){
        if (err != null) console.error(err);
        var isLoginOk = false;
        if (doc != null) {
            var hash = pass2hash(req.body.pass, doc.solt);
            isLoginOk = (hash == doc.hash);
        }
        if (isLoginOk) {
            res.send("OK");
        } else {
            res.statusCode = 401;
            res.send("NG");
        }
    });
}

apps.js:

var express = require('express');
var http = require('http');
var path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
    app.use(express.errorHandler());
}

// サンプルのマッピング設定
var accounts = require('./routes/accounts');
app.post("/accounts/register", accounts.register);
app.post("/accounts/login", accounts.login);

http.createServer(app).listen(app.get('port'), function() {
    console.log('Express server listening on port ' + app.get('port'));
});

実行結果

ユーザ登録を行ってMongoDBのシェルからDBにデータが登録された事を確認する。

> db.accounts.find();
{ "_id" : ObjectId("55cd9b6e8ca01a0a50ecd707"), "name" : "abc", "hash" : "+1j0H0OdNH/HFGXspXi+hx6mtS8=", "solt" : "RzB+u5FvnOvHRXvtZi85iX9URno=" }
{ "_id" : ObjectId("55d011f4ba68640523bd0271"), "name" : "xxx", "hash" : "wsB0iPj/2BtLHGXWmSFqeGjqwn4=", "solt" : "1o2QK3qqdWSgw1cFQgbIj4DnpBA=" }
{ "_id" : ObjectId("55d0126cba68640523bd0272"), "name" : "yyyy", "hash" : "lHj8Xu6HNrrpjn9w6i9FnWYUwdI=", "solt" : "BI8xyULlX/CAj19/3VonYxehG/o=" }
{ "_id" : ObjectId("55d012bbba68640523bd0273"), "name" : "zzz", "hash" : "xeJTRfFbVaKSI2m6QePMv9laB4E=", "solt" : "NG42owW9EC7Ntu8FAp6NWXLwbBs=" }

つでに name に Index を作成しておく。

> db.accounts.ensureIndex({name:1},{unique:true});
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "ok" : 1
}
> db.accounts.getIndexes()
[
        {
                "v" : 1,
                "key" : { "_id" : 1 },
                "name" : "_id_",
                "ns" : "test.accounts"
        },
        {
                "v" : 1,
                "unique" : true,
                "key" : { "name" : 1 },
                "name" : "name_1",
                "ns" : "test.accounts"
        }
]

MongoDBはスキーマレスなのでこういう設定をするタイミングはよくわからない。
_id の役割を完全に理解してないが name の代わりに PKEY として使っても良いのかもしれない。

まとめ

Node.js と MongoDB どちらも JavaScript に慣れている人なら違和感なく入れると思う。
思いの他、学習コストは低いのではないだろうか。
Eclipse でデバッグ実行までできるので開発もしやすそう。

MongoDB の「トランザクションが無い」とか「正規化しない」などの特徴は慣れるまでは難しそうだ。


2014/02/17

AndroidのWebViewをPCのChromeでデバッグ

WebView 上で Web アプリを開発する場合に JavaScript のデバッグが大変そうだなと思いどうすれば良いのか調べてみた。

結論から言うと PC の Chrome に ADB プラグインをインストールすると リモートで Chrome のデバッガが使える事が分かった。

しかも WebView がデバックできるようになったのが 4.4(KitKat) からと言うタイムリーさ。

インストール

以下の Chrome ウェブストアからプラグインをインストールすれば終わりです。
(なぜかウェブストアの検索では出てこないのでリンクを直接叩く必要があります。)

「+無料」のボタンをクリックでインストール開始します。

エミュレータの準備

WebView のデバッグは 4.4(KitKat) からなので andrid-sdk の SDK Manager を起動して 4.4.2(Level-19) 以上の開発環境を一式落とします。

次に AVD Manager を起動して 4.4.2(Level-19) 以上のエミュレータイメージを作成して起動します。

Target: の項目が 4.4.2(Level-19) 以上になっていれば他の項目は何でも良いです。

試してませんが 4.4 の実機があればそちらでもデバッグ可能なようです。

WebViewアプリの準備

アプリのどこかに以下のコードを入れてリモートデバッグを有効にします。

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
}

API Level-19 以前には存在しないメソッドなのでバージョンチェックが必要です。 クラスメソッドなので WebView インスタンスは必要ありません。

コンパイルしなおしてエミュレータで実行しておけば準備完了です。

デバッガの起動

PC の Chrome を起動するとドロイド君のアイコンが有るのでクリックして 「View Inspection Targets」を選択します。

デバッグ可能なブラウザの一覧が表示されるので「inspect」をクリックします。

別ウインドウでデバッガが起動するので後は通常の Chrome と同じようにデバッグできます。

ブレークポイントで止めて見たところ。

所感

JavaScript のデバッグにおいては Chrome のデバッガは圧倒的に使いやすいのでこれは大変ナイスです。

これで Android におけるWebハイブリッドアプリを開発する為の環境はほぼ揃った用に思えます。


2014/02/13

AndroidでWebViewとNativeのハイブリッドアプリ

最近、WebViewを使ったWebとNativeのハイブリッドアプリが流行っている。

iPhoneとAndroidで同一アプリを開発する場合、Web部分が共通化できるメリットが大きいからだろう。

と言うわけで WebView を使ったハイブリッドアプリの作り方を調べてみた。

iPhoneってどうしてるの?

私は iPhone はやってません。(だって開発するだけで金取られるんだもんw)
とは言え iPhone との互換性を無視しては意味ないのでやり方だけは調べて置きます。

ググると一杯出てきますが、通信のリクエストをフックして Native のコードで WebAPI を擬似的に実装するようです。

JavaScriptとの接続方法としては無理が無く良い方法だと思われます。

一方、Android は...

WebView#addJavascriptInterface() を使って Java のオブジェクトを登録すると JavaScript からそのまま使えると言う直接的な方法です。

が、これ結構致命的なセキュリホールが報告されています。
JavaScript から getClass() 等も呼べてしまうため、 WebView に信頼できないサイトの JavaScript が紛れ込むと何でも出来てしまいます。

スマホの運用条件を考えると PKI でサーバを固め、 アプリも XSS に細心の注意を払う必要がありそうです。
現実的にはかなりハードル高いっすね。

但し、API Level 17 (4.2.2) からは @JavascriptInterface アノテーションが追加されメソッドのアクセス制限ができる用になったので この問題は解決されています。
って、最近過ぎるだろw

それはそれとして使ってみる

最近のブラウザはカメラやバイブレーションの API も最初から持っていて以外に追加機能のネタが無かったりします。

なのでクロスドメイン可能な XMLHttpRequestXS とか追加してさらにセキュリティホールを広げて見たいと思います。

Javaオブジェクトの登録

基本ドキュメントの通りですが登録しているのはファクトリです。 ファクトリから得た Java オブジェクトも JavaScript から使えます。

登録処理抜粋:

private class SampleWebViewClient extends WebViewClient {
    @Override
    public void onPageStarted (WebView webview, String url, Bitmap favicon) {
        // 拡張XMLHttpRequestファクトリの初期化。
        XMLHttpRequestXSFactory factory = getXMLHttpRequestXSFactory();
        factory.setAccessControlList(_accessControlList);
        factory.setWebView(webview);
        webview.addJavascriptInterface(factory, "XMLHttpRequestXSFactory");
    }
    …省略
}

登録されるクラス抜粋:

public class XMLHttpRequestXSFactory {
    private WebView _webview;
    private AccessControlList _accessControlList;
    public XMLHttpRequestXSFactory() { }
    @JavascriptInterface
    public XMLHttpRequestXS getXMLHttpRequestXS() {
        return new XMLHttpRequestXS(this);
    }
    …省略
}

登録名に abc.XMLHttpRequestXSFactory とかしてみましたがダメでした。 navigator.~ もダメです。 グローバルの直下のみに登録できるようです。

登録のタイミングは onCreate() だけで無く WebViewClient#onPageStarted() にも必要なようです。

JavaScript から Java の呼び出し

登録されたJavaオブジェクトの呼び出しはほぼそのままです。 但し、フィールドにはアクセス出来ません。

JavaScript抜粋:

function XMLHttpRequestXS() {
    this._native = XMLHttpRequestXSFactory.getXMLHttpRequestXS();
    …省略
};
XMLHttpRequestXS.prototype = {
    open : function(method, url, async) {
        var error = this._native.open(method, url, async);
        if (error) throw error;
    },
    …省略
}

呼ばれ側Java抜粋:

public class XMLHttpRequestXS {
    …省略
    @JavascriptInterface
    public String open(String type, String url, boolean isAsync) throws Exception {
        Log.d(TAG,"open:"+type+" "+url);
        try {
            _isAsync = isAsync;
            if (GET.equalsIgnoreCase(type)) {
                _request = new HttpGet(url);
            } else {
                _request = new HttpPost(url);
            }
            _factory.checkDomain(_request.getURI());

            String cookie = CookieManager.getInstance().getCookie(url);
            _request.setHeader("Cookie", cookie);
            setReadyState(OPENED);
            return null;
        } catch (Throwable t) {
            Log.e(TAG, t.getMessage(), t);
            setReadyState(ERROR);
            return t.getMessage();
        }
    }
    …省略
}

ここで気になったのはメソッドへの引数です。 ドキュメントには JS -> Java の変換ルールが見つけられませんでした。

実際に試した方の情報では String,int,double,boolean,int[],String[] が受け取れたようです。

とりあえず、プリミティブまでは大丈夫そうな気がします。
JSON変換できるオブジェクトならこんな感じで渡せるようです。

Android.test(JSON.stringify({abc:"ABC", yyy:2}));

@JavascriptInterface
public void test(String jsonStr) throws JSONException {
    JSONObject json = new JSONObject(jsonStr);
    String abc = json.getString("abc");
    int yyy = json.getInt("yyy");
}

Java から JavaScript の呼び出し

ドキュメントの通り以下で呼び出せます。

webview.loadUrl("javascript:スクリプト");

が、JS->Java->JSと呼び出すと例外になります。

02-13 04:35:39.382: W/webview(3503): java.lang.Throwable: Warning: 
A WebView method was called on thread 'WebViewCoreThread'.
All WebView methods must be called on the UI thread.
Future versions of WebView may not support use on other threads.

JavaScript は WebView のスレッドで走っているので UIスレッドから呼べ、 と言うことらしいです。

つまり、JavaScriptから呼び出されたメソッドからコールバックしようとする場合、 HandlerかAsyncTaskを経由する必要があるようです。

Java から JavaScript オブジェクトの生成

どうも無理そうです。
JavaScript自体がJavaで実装されて無いと思われるので難しいのでしょう。

XMLやJSONは文字列で渡して JavaScript 側でパーズしてもらう事になりそうです。

動かしてみる

完成した XMLHttpRequestXS を jQuery.ajax() で実行してみます。

HTML:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script type="text/javascript" src="XMLHttpRequestXS.js" ></script>
    <script type="text/javascript" src="jquery-1.11.0.js" ></script>
    <script type="text/javascript">
jQuery.support.cors = true; // クロスドメインをjQueryでするために必要。

function testJqueryAsync() {
    console.log("testJqueryAsync");
    $.ajax({
        type: "GET", url: "http://www.redmine.org/issues.json",
        dataType: "json",
        success: function(data){
            var issues = data.issues;
            var $table = $("#table");
            for (var i=0; i<issues.length;i++) {
                $table.append($("<tr><td>"+issues[i].id+"</td><td>"+issues[i].subject+"</td></tr>"));
            }
        },
        error: function(xhr, status, error){
            alert("error:"+status+":"+error);
        },
        xhr: function() { // jQueryが使うXHRの差替用API
            return new XMLHttpRequestXS();
        }
    });
}
    </script>
</head>
<body onload="testJqueryAsync()">
    <h3>Ajax result from http://www.redmine.org/issues.json</h3>
    <table id="table" width="100%" border="1" >
        <tr><th>ID</th><th>Subject</th></tr>
    </table>
</body>
</html>

jQuery には XMLHttpRequest 差替え用の API が最初から付いてました。 さすが jQuery です。

実行結果:

外部サイトからちゃんとデータを取って来ています。

所感

一見単純そうに見えてやってみると以外に泥沼w
iPhoneとの共通化は表面を JavaScript の API にしてスタブを2種類用意する感じでしょうか。

課題も多いですが Andorid/iPhone のコード共通化やWebからアプリが更新できるメリットは大きいですね。

PhoneGap や Titanium を使う手もありますが HTML5 の機能拡張が著しいので足りない機能だけ Native で実装すると言う方がフレームワークの縛りが無い分楽かもしれません。

サンプルのソースコード一式は以下のSVNにあります。
注意:XMLHttpRequestXSはXSSのセキュリティホールを持ちます。技術検証以上の利用はしないで下さい。


2013/05/25

EclipseのPluginをSWT Browserで作る

Eclipse の Plugin を作ろうと思ったのだが調べてみると敷居が高い高い。

テンプレのプロジェクトは用意されているがいきなりこんな画面が現れて ぼーぜん とすることになる。 ちなみに下部のタブは其々違う設定画面が現れる。


Plugin開発の チュートリアル も有ることは有るが目次を見ただけで気が遠くなる。
しかも、この先には膨大な javadoc が待っている。

正直諦めそうになったのだが以前、 Eclipse内のライブラリからブラウザを起動してみた事を思い出して 内部ブラウザでPluginを作れないかと考えた。

で、ググったら出て来ましたよ、本家IBMのサイトが。

ブラウザの起動方法と最低限の通信方法の説明だけですが本家が可能性として有りだと言ってます。

          ...

そして、設定画面&Javadocと格闘しつつでっち上げたのがこれです。



HTMLのソースはこれだけです。
Eclipse.* は eclipse 側から呼んで貰うためのJavaScriptです。

<html>
<head>

<script src="./jquery-1.8.2.js"></script>

<script>
var Eclipse = {};
Eclipse.setContent = function(content) {
    var data = JSON.parse(content);
    var form = document.parson;
    form.name.value = data.name;
    form.addr.value = data.addr;
    form.email.value = data.email;
};
Eclipse.getContent = function() {
    var data = {};
    var form = document.parson;
    data.name  = form.name.value  ;
    data.addr  = form.addr.value  ;
    data.email = form.email.value ;
    return JSON.stringify(data);
};
Eclipse.fireEvent = function(type) {
    window.status = type;
    window.status = null;
};

window.onload = function() {
    Eclipse.fireEvent("load");
}
window.onerror = function(err) {
    Eclipse.fireEvent("log,"+err);
}
$(function(){
    $("input").bind("change",function() {
        Eclipse.fireEvent("change");
    });
});
</script>

</head>
<body>
    <h1>Browser plugin sample</h1>

    <form name="parson">
        <table>
            <tr><td>Name:</td><td><input name="name" /></td></tr>
            <tr><td>Addr:</td><td><input name="addr" /></td></tr>
            <tr><td>E-Mail:</td><td><input name="email" /></td></tr>
        </table>
    </form>
</body>
</html>



保存した後にテキストエディタで見るとJSON形式でちゃんと保存されている事がわかります。



Plugin側で作成したソースコードは1本だけです。
継承すべき TextEditor クラスを探すのは苦労しましたがその先はすんなり行きました。

package org.kotemaru.eclipse.browserpluginsample.editors;

import 省略;

public class BrowserEditor extends TextEditor {

    private static final String ENCODING = "utf-8";

    private Browser browser;
    private boolean isDirty = false;

    public BrowserEditor() {
        super();
    }

    @Override
    public void init(IEditorSite site, IEditorInput input)
            throws PartInitException {
        setSite(site);
        setInput(input);
        setPartName(input.getName());
    }


    @Override
    public void createPartControl(Composite parent) {
        try {
            browser = new Browser(parent, SWT.NONE);
            browser.setJavascriptEnabled(true);         
            browser.addStatusTextListener(new MyStatusTextListener());

            URL aboutURL = this.getClass().getResource("/webapps/editor.html");
            URL url = FileLocator.resolve(aboutURL);
            browser.setUrl(url.toString());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private class MyStatusTextListener implements StatusTextListener {
        @Override
        public void changed(StatusTextEvent ev) {
            log("status="+ev.text);
            String[] params = ev.text.split(",");
            String method = params[0];
            if ("load".equals(method)) {
                onLoad(params);
            } else if ("change".equals(method)) {
                onChange(params);
            } else if ("log".equals(method)) {
                log(ev.text);
            }
        }
    }

    private void onLoad(String[] params) {
        StringBuilder sbuf = new StringBuilder();
        try {
            IFileEditorInput input = (IFileEditorInput)getEditorInput();
            InputStream in = input.getFile().getContents();
            try {
                Reader reader = new InputStreamReader(in, ENCODING);
                int n;
                char[] buff = new char[1024];
                while ((n=reader.read(buff))>=0) {
                    sbuf.append(buff,0,n);
                }
            } finally {
                in.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        String data = sbuf.toString();
        data = data.replaceAll("'", "\\'").replaceAll("\n", "\\n");

        browser.execute("Eclipse.setContent('"+data+"')");
    }

    private void onChange(String[] params) {
        setDirty(true);
    }


    @Override
    public void doSave(IProgressMonitor monitor) {
        String content = (String) browser.evaluate("return Eclipse.getContent();");
        try {
            IFile file = ((IFileEditorInput) getEditorInput()).getFile();
            file.setContents(
                new ByteArrayInputStream(content.getBytes(ENCODING)),
                true,  // keep saving, even if IFile is out of sync with the Workspace
                false, // dont keep history
                monitor); // progress monitor
            setDirty(false);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public void setDirty(boolean b) {
        isDirty = b;
        firePropertyChange(EditorPart.PROP_DIRTY); 
    }

    @Override
    public boolean isDirty() {
        return isDirty;
    }


    @Override
    public void dispose() {
        browser.dispose();
        super.dispose();
    }

    @Override
    public void setFocus() {
    }
    @Override
    public boolean isSaveAsAllowed() {
        return false;
    }
    @Override
    public void doSaveAs() {
        // Unsupported
    }

    private void log(String msg) {
        System.out.println(msg); // TODO:ちゃんとログ。
    }

}



本格的なPluginを作るにはまだ色々必要ですが JavaScript でEclipse plugin開発ができる事が分かりました。

このサンプルのSVNリポジトリは以下にあります。


2013/05/06

antでファイル一覧を生成する。

JavaScriptで1クラス=1ファイルでコードを書いていると .js ファイルが沢山できる。

これを一々、<script> タグにして HTML に埋め込むのが以外に面倒くさい。

で、ant の fileset で何とかならないかと思って調べてみたらこうなった。

<taskdef resource="net/sf/antcontrib/antcontrib.properties">
  <classpath>
    <pathelement location="${project.root}/lib/ant-contrib-1.0b3.jar"/>
  </classpath>
</taskdef>

<target name="main">
    <antcall target="makeListFile">
        <param name="includes" value="**/*.js"/>
        <param name="file" value="html.txt"/>
        <param name="prefix" value="&lt;script src='"/>
        <param name="suffix" value="'/>"/>
    </antcall>
</target>

<target name="makeListFile">
    <fileset id="fileset" dir=".">
        <include name="${includes}" />
    </fileset>

    <pathconvert property="files" refid="fileset" pathsep=";" dirsep="/" >
        <map from="${basedir}/" to="" />
    </pathconvert>

    <echo file="${file}"></echo>
    <foreach target="addLine" param="name" inheritall="true"
        list="${files}" delimiter=";"
    >
   </foreach>
</target>
<target name="addLine">
        <echo append="true" file="${file}">${prefix}${name}${suffix}
</echo>
</target>

面倒くせー。 そもそも contrib ずっとベータだし。

次に script タグを試して見ようと思って見付けたのがこちらのサンプル。

scriptタスクのマニュアル より

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="MyProject" basedir="." default="main">

  <property name="fs.dir" value="src"/>
  <property name="fs.includes" value="**/*.txt"/>
  <property name="fs.excludes" value="**/*.tmp"/>

  <target name="main">
    <script language="javascript"> <![CDATA[

      // import statements
      // importPackage(java.io);
      importClass(java.io.File);

      // Access to Ant-Properties by their names
      dir      = project.getProperty("fs.dir");
      includes = MyProject.getProperty("fs.includes");
      excludes = self.getProject()  .getProperty("fs.excludes");

      // Create a <fileset dir="" includes=""/>
      fs = project.createDataType("fileset");
      fs.setDir( new File(dir) );
      fs.setIncludes(includes);
      fs.setExcludes(excludes);

      // Get the files (array) of that fileset
      ds = fs.getDirectoryScanner(project);
      srcFiles = ds.getIncludedFiles();

      // iterate over that array
      for (i=0; i<srcFiles.length; i++) {

        // get the values via Java API
        var basedir  = fs.getDir(project);
        var filename = srcFiles[i];
        var file = new File(basedir, filename);
        var size = file.length();

        // create and use a Task via Ant API
        echo = MyProject.createTask("echo");
        echo.setMessage(filename + ": " + size + " byte");
        echo.perform();
      }
    ]]></script>
  </target>
</project>

もっと面倒くせー orz

結局、自前でスクリプトを呼ぶTask書くのが早いんじゃね、と思って作ったのがこれ。

package jstask;

import java.io.File;
import java.util.*;
import org.apache.tools.ant.*;
import org.apache.tools.ant.types.*;
import org.apache.tools.ant.types.resources.FileResource;
import org.mozilla.javascript.*;

public class JsTask extends Task  {
    private List<ResourceCollection> rclist = new ArrayList<ResourceCollection>();
    private List<Parameter> params = new ArrayList<Parameter>();
    private String text;

    public void add(ResourceCollection rc) {
        rclist.add(rc);
    }
    public void addParam(Parameter param) {
        params.add(param);
    }
    public void addText(String str) {
        text = str;
    }

    @Override
    public void execute() throws BuildException {
        Context cx = Context.enter();
        try {
            Scriptable scope = cx.initStandardObjects();
            scope.put("task", scope, this);
            scope.put("fileset", scope, toFileList(rclist));
            for (Parameter param : params) {
                scope.put(param.getName(), scope, param.getValue());
            }
            cx.evaluateString(scope, text, "ant.js", 0, null);
        } finally {
            Context.exit();
        }
    }   

    private List<File> toFileList(List<ResourceCollection> rclist) {
        List<File> filelist = new ArrayList<File>(); 
        for (ResourceCollection rc : rclist) {
            for (Iterator<?> ite = rc.iterator(); ite.hasNext();) {
                Resource resource = (Resource) ite.next();
                if (resource instanceof FileResource) {
                    File file = ((FileResource) resource).getFile();
                    filelist.add(file);
                }
            }
        }
        return filelist;
    }
}

使用例:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project basedir="." default="test" name="test">
    <taskdef name="js"
        classname="jstask.JsTask"
        classpath="lib/jstask.js;lib/js.jar"
    />
    <target name="test">
        <js>
            <fileset dir="src" includes="**/*.js" />
            <param name="prefix" value="^${basedir}/" />
            <param name="outfile" value="html.txt" />

            <![CDATA[
            var out = new java.io.FileWriter(outfile);
            var ite = fileset.iterator();
            while (ite.hasNext()) {
                var reg = new RegExp(prefix);
                var fname = (""+ite.next()).replace(reg,"");
                out.write("<script src='"+fname+"'></script>\n");
            }
            out.close();
            ]]>
        </js>
    </target>   
</project>

結局これが一番簡単だった...


2013/04/14

HTML5のCanvasで点線が書けないなんて...

ブラウザ上でツールを作っていて気が付いたのだが HTML5 の Canvas は点線、破線が書けない。

そんな馬鹿なと思ったが、仕様上持っておらずほんとに書けない。

Webでググるといかにも力技な解決策を教えて貰えます。

  • http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvasより。
var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
if (CP.lineTo) {
    CP.dashedLine = function(x, y, x2, y2, da) {
        if (!da) da = [10,5];
        this.save();
        var dx = (x2-x), dy = (y2-y);
        var len = Math.sqrt(dx*dx + dy*dy);
        var rot = Math.atan2(dy, dx);
        this.translate(x, y);
        this.moveTo(0, 0);
        this.rotate(rot);       
        var dc = da.length;
        var di = 0, draw = true;
        x = 0;
        while (len > x) {
            x += da[di++ % dc];
            if (x > len) x = len;
            draw ? this.lineTo(x, 0): this.moveTo(x, 0);
            draw = !draw;
        }       
        this.restore();
    }
}

作成中のツールが動的に Canvas を使うものだったのでやりたく無いなー と思ってもう少し調べたら同じページにブラウザ毎の対処方がのってました。

var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");

if ( ctx.setLineDash !== undefined )   ctx.setLineDash([5,10]);
if ( ctx.mozDash !== undefined )       ctx.mozDash = [5,10];

Chrome は setLineDash()、Firefox は mozDash を使えば行けるようです。

IE は... サポートする気無いからいいや。

点線の書けない 2D 系のAPIなんで初めてみました。早く仕様に取り込んで欲しいものです。


2012/12/30

CSSのクラスの値をJavaScriptで変更する方法

jQuery に有りそうで無いのでクラスに設定したスタイルを後からJavascript変更する方法をメモ。
function Common(){this.initialize.apply(this, arguments)};
(function(Class){

    /**
     * ルールのキャッシュ
     * @type CSSStyleRule
     */
    var classCssRuleCache = {};

    /**
     * セレクタに一致するルールを取得。
     * <li>検索は重いのキャッシュする。
     * @param {string} selectror セレクタ文字列
     * @returns {CSSStyleRule} ルール
     */
    Class.getCssRule = function(selector) {
        if (classCssRuleCache[selector]) return classCssRuleCache[selector];
        var sheets = document.styleSheets;
        for (var i=0; i<sheets.length; i++) {
            var rules = sheets[i].cssRules;
            if (rules == null) rules = sheets[i].rules; // ForIE
            for (var j=0; j<rules.length; j++) {
                if (selector == rules[j].selectorText) {
                    classCssRuleCache[selector] = rules[j];
                    return rules[j];
                }
            }
        }
        return null;
    }

    /**
     * セレクタに一致するルールを取得。
     * <li>当該セレクタのルールが存在しなければ作成する。
     * @param {string} selectror セレクタ文字列
     * @returns {CSSStyleRule} ルール
     */
    Class.getCssRuleWithDefine = function(selector) {
        var rule = Class.getCssRule(selector);
        if (rule) return rule;

        var sheet = document.styleSheets[0];
        if (sheet.insertRule) {
            sheet.insertRule(selector+"{}", sheet.cssRules.length);
        } else {
            sheet.addRule(selector,"dummy:dummy");//forIE
        }
        return Class.getCssRule(selector);
    }
   
   
    /**
     * セレクタにルールを設定。
     * <li>当該セレクタのルールが存在しなければ作成する。
     * @param {string} selectror セレクタ文字列
     * @param {object} スタイルのマッピング 例:{textAling: "center",…}
     */
    Class.setCssRule = function(selector, style) {
        var rule = Class.getCssRuleWithDefine(selector);
        if (rule == null) return;
        for (var k in style) rule.style[k] = style[k];
    }
   
   
    /**
     * セレクタにルールを important 付きで設定。
     * <li>当該セレクタのルールが存在しなければ作成する。
     * @param {string} selectror セレクタ文字列
     * @param {object} スタイルのマッピング 例:{textAling: "center",…}
     */
    Class.setCssRuleImportant = function(selector, style) {
        var rule = Class.getCssRuleWithDefine(selector);
        if (rule == null) return;
        for (var k in style) rule.style.setProperty(k, style[k], 'important');
    }

})(Common);
使用例:
Common.getCssRule(".MyClass", {textAlign:"center", color:"red"});
だけ。setCssRuleImportant()を使えば !impotant 付きになる。
厳密に言うとクラスのスタイルの言うものは存在しないのでセレクタのスタイルを変更している。つまり第一引数にクラス名を指定する時はドット(.)が必須なので注意。


間単に解説すると読み込まれたCSSはブラウザ依存で以下のプロパティに保存されている。

ブラウザプロパティ名
IE8以前document.styleSheets[ ].rules[ ]
その他document.styleSheets[ ].cssRules[ ]

cssRules[ ] の型は CSSStyleRule で以下のプロパティを持つ。

プロパティ名説明
stringselectorTextセレクタ文字列
CSSStyleDeclarationstyleスタイルセット


セレクタ文字列の正規化の方法は不明だったので複雑なセレクタを変更する場合は自力で正規化をする必要があるかもしれない。(タグ名の大文字/小文字や空白の扱いの事)

IE8以前はルールの追加方法も異る。

ブラウザプロパティ名
IE8以前addRule(selector, style[, index])
その他insertRule(rule,index)

あと @import とか対応が必要だけどあんまり使わないから必要になるまで放置かな(^^;


2012/12/14

Redmine チケット ビューア MyMine 公開します

Redmine の UI が使いづらいので自前でビューアを作ってみました。

基本機能は以下になります。

  • チケットを見やすく表示。
  • フィルタ、ソートが簡単。
  • チケットをローカルのフォルダに整理できる。
画面(クリックで大きく)

詳細はマニュアルを見て下さい。

マニュアル:
  - http://mymine.googlecode.com/svn/trunk/mymine/war/mymine/manual.html

ダウンロード:
  - http://code.google.com/p/mymine/downloads/list


プロフィール
20年勤めた会社がリーマンショックで消滅、紆余曲折を経て現在はフリーランスのSE。 失業をきっかけにこのブログを始める。

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿【javascript】

リンク

アーカイブ