2013/04/07

JavaのDateの時刻をリセットするメモ

時間ねたが続くが Java で Date から時刻をリセットして日付だけを取り出そうとしてはまったのでメモ。

日付関係の処理はややこしい。タイムゾーンとか夏時間とか閏秒とか訳分からん。

Java の場合は java.util.Calendar が一手に引き受けてくれるのだが こいつがまた曲者ではまった。

結論だけ欲しい人の為の正解、

static Date clearTime(Date date) {
    Calendar cal = new GregorianCalendar();
    cal.setTime(date);
    cal.clear(Calendar.AM_PM);
    cal.clear(Calendar.HOUR);
    cal.clear(Calendar.HOUR_OF_DAY);
    cal.clear(Calendar.MINUTE);
    cal.clear(Calendar.SECOND);
    cal.clear(Calendar.MILLISECOND);
    return cal.getTime();
}

これで、もろもろの面倒を考えずに日付だけが取り出せる。

単純に考えると

    cal.clear(Calendar.HOUR);
    cal.clear(Calendar.MINUTE);
    cal.clear(Calendar.SECOND);
    cal.clear(Calendar.MILLISECOND);

だけで良さそうだがこれだとこうなる。

2013/04/07 14:00:00.000

AM_PMリセットしても

    cal.clear(Calendar.AM_PM);

こうだったりする。

2013/04/07 02:00:00.000

結局、AM_PM をクリアしてから HOURHOUR_OF_DAY の両方をクリアする必要があるという謎な仕様。

普通に Calendar.clearTime() が有っても良いと思うんだが...


2013/03/24

AndroidでWebServerを動かしてみた。

Android でも普通に ServerSocket が使えると言う話を小耳に挟んだので調べて見ると既に jetty が普通に動いているらしい。

これをインストールして試してもあんまり面白く無いので 自前で簡易WebServerを作ってみた。

ソース:

package org.kotemaru.android.webserver;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends Activity {
    private Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try {
            new AcceptThread(8080).start();
        } catch (IOException e) {
            postMessage(e.getMessage());
            Log.e("boot",e.getMessage());
        }
    }

    void postMessage(final String msg) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }
        });
    }

    private class AcceptThread extends Thread {
        private ServerSocket ssock;

        public AcceptThread(int port) throws IOException {
            this.ssock = new ServerSocket(port);
        }

        @Override
        public void run() {
            try {
                postMessage("Server start");
                while (true) {
                    Socket sock = ssock.accept();
                    new ConnectThread(sock).start();
                }
            } catch (IOException e) {
                postMessage(e.getMessage());
                Log.e("AcceptThread",e.getMessage());
            }
        }
    }

    private class ConnectThread extends Thread {
        private Socket sock;

        public ConnectThread(Socket sock) {
            this.sock = sock;
        }
        @Override
        public void run() {
            try {
                Log.i("ConnectThread","From "+sock.getRemoteSocketAddress());

                BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
                BufferedOutputStream out = new BufferedOutputStream(sock.getOutputStream());

                String reqLine = in.readLine();
                String line = in.readLine();
                while (!line.isEmpty()) {
                    line = in.readLine(); // ヘッダーは無視 (^^;
                }

                String[] parts = reqLine.split(" ");
                String path = parts[1].replaceFirst("[?].*$", "");
                File docroot = new File(Environment.getExternalStorageDirectory(),"docroot");

                out.write("HTTP/1.0 200 OK\r\n".getBytes());
                out.write(("Content-type: "+getCType(path)+"\r\n\r\n").getBytes());
                InputStream fin = new FileInputStream(new File(docroot,path));
                try {
                    byte[] buff = new byte[1024];
                    int n;
                    while ((n=fin.read(buff))>0) {
                        out.write(buff,0,n);
                    }
                    out.flush();
                    out.close();
                } finally {
                    fin.close();
                }
            } catch (IOException e) {
                postMessage(e.getMessage());
                Log.e("ConnectThread",e.getMessage());
            } finally {
                try {
                    sock.close();
                } catch (IOException e) {
                    postMessage(e.getMessage());
                    Log.e("ConnectThread",e.getMessage());
                }
            }
        }
        private String getCType(String path) {
            path = path.toLowerCase();
            if (path.endsWith(".html")) return "text/html";
            if (path.endsWith(".jpg")) return "image/jpeg";
            if (path.endsWith(".png")) return "image/png";
            if (path.endsWith(".gif")) return "image/gif";
            if (path.endsWith(".js")) return "application/javascript";
            if (path.endsWith(".css")) return "text/css";
            return "unknown";
        }

    }
}

起動が Activity から行われている事以外はほんとにただの WebServer の実装。

DocumentRoot は内部ストレージの「docroot/」としたので コンテンツは Nexus7 をUSBで繋いでPCからコピペすればOK。

Wifi設定で Nexus7 のIPアドレスを調べてブラウザから直接URLを叩くとちゃんと表示された!

尚、AndroidManifest.xml には以下の設定が必要。

<uses-permission android:name="android.permission.INTERNET" />

但し、これを設定しても port=80 のサーバはパーミッションエラーとなる。 たぶん、root権が必要。

...

モバイル用途の端末だとサーバ化はあんまり意味無いっていうかセキュリティ上の問題がありそうだけど、 スティック型Androidの使い道は色々広がりそう。

安定動作しそうな スティック型Android 出ないかなぁ


2013/03/11

markdown のサポート

ブログのWiki対応で最初は標準化を謳っている Wiki Creole にしようと思って いたんだけど仕様が曖昧で実装も良さげな物が無い。

で、調べてたら markdown って言う Wiki っぽい仕様が有って Git-HUB でも採用しているらしい。

基本的に元の文書のままでも読める事を前提としている仕様との事。

ちょっと使ってみた感じ。

  • 長所:
    • 文中にソースコードが埋め込まれる事を考慮している。
    • HTMLのタグもそのまま使える。
  • 短所:
    • テーブルが書けない。(<table>タグで書く)
    • 文節内での改行制御ができない(<br/>タグで書く)

Git-HUB が採用しているだけ有って技術系の文書は非常に書きやすい。
短所は逃げ道が用意して有りどうにもならないと言う状況は無い。


markdown の Java 実装は markdownj と言うのが有った。
置き場所が散らかっていて分かりづらいので整理。

使い方が何処にも書いて無くて困ったのだがソース内のコメントを読んだらこれだけだったw。

MarkdownProcessor markdown = new MarkdownProcessor();
String htmlText = markdown.markdown(markdownText);


記法ざっくりまとめ

ヘッダー
## ヘッダー2 
### ヘッダー3
#### ヘッダー4

ヘッダー2

ヘッダー3

ヘッダー4


リスト
- リスト
    - ネスト2
        - ネスト3
    - zzz

  • リスト
    • ネスト2
      • ネスト3
    • zzz


リンク各種
- [リンク](http://examle.com)
- <http://examle.com>
- [外部定義リンク][]

[外部定義リンク]: http://examle.com    "タイトル"


画像
![GAEロゴ](http://developers.google.com/appengine/images/appengine-noborder-120x30.gif)

GAEロゴ


インライン
- *強調*
- **とても強調**
- `<escape>`

  • 強調
  • とても強調
  • <escape>


ソースコード
    [xxxx](xxxxx)

<pre>[xxxx](xxxxx)
</pre>

[xxxx](xxxxx)
[xxxx](xxxxx)


引用
> aaaaaa

aaaaaa


区切り線
 ------------------



2013/03/09

自前ブログエンジンにコメント機能

このブログにコメント機能を追加した。
右下の「》この投稿へのコメント」から行けます。

さすがにコメントは動的なページになるので GAE/J の Servlet で作り込み。

単純なので低レベルAPIを直接使って超軽量に実装したがそれでも sipn-up に5秒かかってる。

ms=4932 cpu_ms=1819 cpm_usd=0.000004 loading_request=1

まあ、最初から分かっていたのでコメント部分は JavaScript で取得して静的なページに後から差し込む形態としておいた。
これなら本文を人間が読み終るころにはコメントも表示されているはず。

こんな感じ。

GAE/J の spin-up 問題には泣かされ続けている訳だけど こういう方向で逃げるしか無いんじゃなかろうか。

ソース:(eclipseでチェックアウトしてください)


2013/03/02

GAE/J用ブログエンジンv0.0.1

すこし整理出来たので GAE/J 用のブログエンジンを公開します。
現状はブログエンジンとは名ばかりで実際は静的HTMLの生成エンジンです。

やってる事はこれだけです。



こっち↓の方が分かり易いでしょうか。

contents/の配下に拡張子 .blog でブログの属性と本文を記述すると eclipse のビルダーが自動で html を war/ の下に生成してくれます。

ローカルのGAEサーバを起動すれば http://localhost:8888/ でブログのプレビューが確認できるので後はアップロードするだけです。

基本的にこのブログ用に作った物なので他の人が使えるのか?
ですが、以下のような人が対象です。

  1. 独自ドメインでブログを開設したい人
  2. 巷のプログサービスのお節介な機能がウザイ人
  3. Java PG/SEの人
3.が必須なんですけどね。(^^;

今後、コメントや Wiki 対応していきたいなとは思ってます。

ソース:(eclipseでチェックアウトしてください。両方必要です。)

先に kblog-converter をビルドして kblog/build.properties の設定をすれば動くと思います。たぶん...

以上。


2013/02/23

Blog再び移転

Google の Blogger に移転した訳だがぶっちゃけめちゃくちゃ使いづらい。

HTMLモードにしても勝手にHTMLを書き換えて思った通りに表示されない。
特に、ソースコードの張り付けがどうにもならなずえらい苦労した。

考えた挙げ句、GAE にブログシステムを構築して移転する事にした。

しかし、GAE/Python 版のブログシステムは色々有るようなのだが Java 版が無い。
参考:http://d.hatena.ne.jp/ninoseki/20100924/1285334500

仕方が無いので自分で作った。今、見ているのがそれ。

クライアントPC側で HTML を全て生成して静的コンテンツとして upload しているので GAE の spin-up 問題も影響しない。

コメントとかはさすがに動的コンテンツとしておいおい実装するつもり。

コンテンツの編集や生成、バックアップも全部 Eclipse 上で出来るので Java 屋的には非常に楽になった。

もうちょっと整理出来たら公開します。


2013/02/16

Androidの非同期処理と画面回転

Android-x86-4.4r2 の設定はこちら


Androidで非同期処理を Thread でやろうとするとこんな例外が出て怒られる。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
UI スレッド以外が UI を操作出来ないって事らしい。

普通は Handler か AsyncTask を使えば回避できるのだが、ここで一つ問題が発生する。

Android のデフォルト動作では画面が回転すると Activity の破棄と再構築が行われる。
したがって AsyncTask が処理を終らせて結果を Activity に通知しようとしても既に存在しない可能性がある。

とりあえずの解決法としては AndroidManifest.xml の <activity> タグに以下の属性を追加して回転しても再構築を行わせないようにすることができる。

android:configChanges="orientation|keyboardHidden"

とは言えこれで逃げられないケースも有るのでまじめな対策を考える。

そもそも、Android の Activity はテンポラリな物で何時破棄されてもおかしく無いという位置付けになっている。にもかかわらずアプリケーションは Activity を中心に設計するようになっているので問題が発生しているように思える。

ここのページの方は Activity はあくまで「従」立場で使えとおっしゃってますが現実には色々難しいかと思います。既存のコードも有るし。


問題の本質は「主」である Activity が「従」である AsyncTask の知らない間に入れ替わってしまっていることだ。

朝、出社したら課長の席に知らないおじさんが座っているようなものである。

こういう場合は人事課に現在の所属課の課長が誰なのか問い合わせられれば良いわけだけど Android にはそういう仕掛けが用意されていない。

と言うわけで、Activity を ID で管理するクラスを用意して AsyncTask は ID から必要な時に Activity 問い合わせる方式で実装しみた。

ソースコード


ActivityManager.java:
package org.kotemaru.android.asyncrotate;

import java.util.HashMap;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;

/**
 * Activityの管理クラス。
 * - Activityが destroy/create されても同一IDで継続的にアクセスできる。
 * - インスタンスをApplication.registerActivityLifecycleCallbacks()に設定する事。
 * - Bundle のキー "___ACTIVITY_ID___" を汚染する。
 * @author kotemaru.org
 */
public class ActivityManager implements ActivityLifecycleCallbacks {
    public final String ACTIVITY_ID = "___ACTIVITY_ID___";
   
    /** Application内で一意のActivityのIDカウンタ */
    private Integer nextActivityId = 0;
   
    // マップ
    private HashMap aid2activity = new HashMap();
    private HashMap activity2aid = new HashMap();

   
    /**
     * ActivityからIDの取得。
     * - すでにIDを持っている場合はそれを返す。
     * - IDを持っていない場合はマップに新規登録して返す。
     * @param activity
     * @return Application内で一意のID
     */
    public synchronized String getActivityId(Activity activity) {
        String aid = activity2aid.get(activity);
        if (aid == null) {
            aid = (nextActivityId++).toString();
            aid2activity.put(aid, activity);
            activity2aid.put(activity, aid);
        }
        return aid;
    }
    /**
     * IDからActivityの取得。
     * - 登録されているIDからActivityを引いて返す。
     * - Activity が destroy/create されていてると更新されている。
     * @param aid ActivityのID
     * @return Activity。未登録の場合はnull。
     */
    public synchronized Activity getActivity(String aid) {
        return aid2activity.get(aid);
    }
   
   
    /**
     * Activity.onCreate()のハンドリング。
     * - Bundleに ___ACTIVITY_ID___ を持っていればマップを更新。
     */
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (savedInstanceState == null) return; // First
        String aid = savedInstanceState.getString(ACTIVITY_ID);
        if (aid == null) return; // Not managed.
       
        synchronized (this) {
            aid2activity.put(aid, activity);
            activity2aid.put(activity, aid);
        }
    }
   
    /**
     * Activity.onSaveInstanceState()のハンドリング。
     * - ___ACTIVITY_ID___ にActivityのIDを保存。
     */
    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        String aid = activity2aid.get(activity);
        outState.putString(ACTIVITY_ID, aid);
    }

    /**
     * Activity.onDestroy()のハンドリング
     * - マップからActivityを削除。
     * - Activityインスタンス開放の為、必須。
     */
    @Override
    public synchronized void onActivityDestroyed(Activity activity) {
        String aid = activity2aid.get(activity);
        if (aid == null) return; // Not managed activity.
        aid2activity.put(aid, null);
        activity2aid.remove(activity);
    }
   
   
    @Override
    public void onActivityStarted(Activity activity) {
    }
    @Override
    public void onActivityResumed(Activity activity) {
    }
    @Override
    public void onActivityStopped(Activity activity) {
    }
    @Override
    public void onActivityPaused(Activity activity) {
    }

}

AsyncHelperApplication.java:
package org.kotemaru.android.asyncrotate;

import android.app.Application;

public class AsyncHelperApplication extends Application {
    private ActivityManager activityManager = new ActivityManager();

    @Override
    public void onCreate() {
        // API14 からサポートのActivityのライフサイクルのコールバック設定。
        registerActivityLifecycleCallbacks(activityManager);
    }

    @Override
    public void onTerminate() {
    }

    public ActivityManager getActivityManager() {
        return activityManager;
    }
}
MainActivity.java:
package org.kotemaru.android.asyncrotate;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.widget.TextView;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       
        // 3秒後にメッセージを書き換えるタスクを起動
        TextView m = (TextView) this.findViewById(R.id.message);
        new SlowAsyncTask(this).execute(m.getText()+"{3sec}");
    }
   
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
}

SlowAsyncTask.java:
package org.kotemaru.android.asyncrotate;

import android.app.Activity;
import android.os.AsyncTask;
import android.widget.TextView;

public class SlowAsyncTask extends AsyncTask {
    private ActivityManager activityManager;
    private String activityId;
   
    public SlowAsyncTask(Activity activity) {
        activityManager = ((AsyncHelperApplication)activity.getApplication()).getActivityManager();
        // Activity の ID を取得して保存。
        activityId = activityManager.getActivityId(activity);
    }
   
    @Override
    protected String doInBackground(String... params) {
        // 時間のかかる非同期処理。
        try {Thread.sleep(3000);} catch (Exception e) { }
        return params[0];
    }

    @Override
    protected void onPostExecute(String result) {
        // Activityは保存しておいたIDから取得する。
        Activity activity = activityManager.getActivity(activityId);
        TextView message = (TextView) activity.findViewById(R.id.message);
        message.setText(result);
    }
}

ソースの解説

とりあえずこれでちゃんと動作している。

API14 からサポートされた ActivityLifecycleCallbacks を利用して Activity の create/destory をフックし入れ替わりを管理している。ID の保存の為に Activity の Bundle を1つ汚染することになるが最小限だろう。

AsyncTask.onPostExecute()が create と destory の間に発生する事を危惧したが起こり得ないようである。onPostExecute() は UI スレッドで呼ばれる事が保証されているので UI の準備の整わない状態で呼ばれないと解釈した。





2012/10/14

mod_rewrite+mod_jkによるTomcatの文字化け

環境が変わったら Tomcat が文字化けするようになったので 調査してくれと依頼されて調べた。

結論から言ってしまうと mod_rewrite と mod_jk を併用した場合の問題だったのだが 整理された情報が無くてはまったのでまとめとく。

発生条件:
URLパラメータに日本語を使っていて mod_rewrite と mod_jk を経由して Tomcat にリクエストを渡す場合、文字化けが発生する。

原因:
mod_jk の設定に以下の設定が有る場合、 mod_rewriteが日本語をデコードしてしまうので文字化けする。
JkOptions +ForwardURICompat

解決方法:
mod_jk の設定を以下に変更する。但し、mod_jk-1.2.24 以降が必要。
JkOptions +ForwardURIProxy

問題の本質:
  • mod_rewrite と Tomcat がどちらもURL内のエスケープ文字(%xx) を自前で解析したいため矛盾が発生する。
  • mod_jkがどちらを優先するかのデフォルトの設定を2転3転させているため 混乱に拍車がかかっている。

    バージョンとデフォルト動作の整理。
    mod_jk Ver.デフォルト設定 動作
    〜1.2.22 ForwardURICompat mod_jkに渡すURLはmod_rewriteを通す。
    %xxはデコードされる。
    1.2.23 ForwardURICompatUnparsed mod_jkに渡すURLはmod_rewriteを通さない。
    %xxはデコードされない。
    1.2.24〜 ForwardURIProxy mod_jkに渡すURLはmod_rewriteを通す。
    mod_rewriteが操作した所以外は%xxはデコードされない。




  • 最新版の mod_jk はデフォルトの ForwardURIProxy でほぼ問題無いはずなのですが 日本語公式サイトの設定例に ForwardURICompat が指定されていたりするので コピペしちゃう人がいたりしそうです。

    • 内容が古い:http://www.jajakarta.org/tomcat/tomcat-jk2/ja/docs/jk/aphowto.html

    まあ、URLに日本語使うなって言うのが正論なんでしょうけど 現実にはそうもいかないですしね...

    その他の参考サイト:

    • http://tomcat.apache.org/connectors-doc/reference/apache.html
    • http://d.hatena.ne.jp/kusakari/20070616/1181971048
    • http://d.hatena.ne.jp/kusakari/20080325/1206417188

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

    サイト内検索

    登録
    RSS/2.0

    カテゴリ

    最近の投稿【java】

    リンク

    アーカイブ