2014年11月29日土曜日

AndroidアプリのIn-app Billing version2からversion3への更新 ③テスト


In-app Billing version3の最終回です。
今回は作ったものがまともに動くかテストする方法を確認します。

テストには二通りの方法があり、ある程度の動きはサンドボックステストで確認し、最終的に実購入テストという流れかと思います。


  • サンドボックステスト
サンドボックステストのために、以下4つの予約済みアイテムIDが用意されています。

購入成功(android.test.purchased)
キャンセル、クレジットカード無効など(android.test.canceled)
払い戻し(android.test.refunded)
存在しないアイテム(android.test.item_unavailable)

android.test.canceledandroid.test.refundedの場合は、RESPONSE_CODEBILLING_RESPONSE_RESULT_USER_CANCELEDあたりが返ってくるのかと思ったのですが、BILLING_RESPONSE_RESULT_OKが返ってきます。

ただし、INAPP_PURCHASE_DATAINAPP_DATA_SIGNATUREはnullです。

android.test.item_unavailableの場合は、onActivityResultに渡されてくるresultCodeRESULT_CANCELEDが返ってきます。

こっちのActivityのRESULT_CANCELEDは0で、RESULT_OKが-1です。In-app Billingで返ってくるレスポンスコードはBILLING_RESPONSE_RESULT_OKが0なのでややこしいです。


ちなみにandroid.test.purchasedで一度購入成功すると、二度と購入出来なくなるのですが、「設定→アプリ→Google Play→データを削除」でデータを削除すると、再度購入出来るようになります。

  • 実購入テスト
サンドボックステストに飽きたら実購入テストに入ります。
実購入テストでは、Google Playと実際に通信して購入処理をテストします。


アルファ版配布用に、google グループで適当にグループを作成し、テスト用のgmailアドレスを招待しておきます。

テスト購入で課金が発生したりしないように、Developer Consoleにログインし、テストアカウントを設定します。
「設定→アカウントの詳細→ライセンス テスト→テスト用のアクセス権がある Gmail アカウント」にテスト用のgmailアドレスを入力します。

テストしたいapkをリリース用のKeyStoreで署名して、アルファ版テストにアップロードします。

「テスターのリストを管理→アプリのアルファ テスト版を提供するユーザー」という画面で先ほど作成したグループを登録すると、オプトインのためのアドレスが表示されます。

テスターが「テスターになる」をタップすると設定は完了で、あとは実際にGoogle Playから実機にインストールして試すだけです。

ただ、アルファ版が有効になるまで結構時間がかかる(半日とか1日とか)ようなので、作ってはアップロード作ってはアップロードという試し方は出来なそうな雰囲気です。

とは言え、やはり課金関連で失敗するとひどいことになるので、十分なテストの上で公開したいところです。

時間を置くと、実際にGoogle Playからダウンロード出来るようになります。

ダウンロードしたアルファ版のアプリで購入ボタンを押すと、しっかりとテスト注文である旨が表示されます。
(アプリ名やカード番号は消しました)

アルファテストで購入完了すると、Googleウォレット販売者コンソールでは以下のように「追加済み」と表示されます。

ということで、苦手意識のあったIn-app Billingですが、version3になって本当にすっきり分かりやすくなりました。

食わず嫌いだった人はこれを機に、是非再チャレンジしてみると良いと思います。


Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ②レスポンスを受け取る


前回はIn-app Billing version3のリクエスト送信方法について触りましたので、今回はそのレスポンス処理方法についてです。

  • レスポンスを受け取る
startIntentSenderForResultのレスポンスはActivityのonActivityResultで受け取ります。

startIntentSenderForResultの第2引数で渡したリクエストコードとの一致を確認した上で、resultCoderesponseCodeが問題ないか確認し、レスポンスの中身を取り出します。

purchaseDataはJSON形式になっていて、その中のproductIdを確認すると、どの商品が購入されたかが分かります。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            if(purchaseData != null) {
                String productId = "";
                try {
                    JSONObject jsonObj = new JSONObject(purchaseData);
                    productId = jsonObj.optString("productId");
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                //必要な処理
            }
        }
    }
}

ちなみに、getIntExtradefaultValueを0にしているサンプルを良く見かけるのですが、0だとBILLING_RESPONSE_RESULT_OKになってしまって微妙な気がしたので-1にしてみました。
なんか理由があるのかもしれませんが。

  • 署名の確認
テストアカウントであればサンドボックステストでも署名が返ってくるはずですが、うまくいかなかったので実購入テストで試しました。

登場人物はonActivityResultに渡されてきたIntentに入っている、INAPP_PURCHASE_DATAINAPP_DATA_SIGNATUREと、公開鍵であるBASE64_ENCODED_PUBLIC_KEYです。

BASE64_ENCODED_PUBLIC_KEYは、Developers Consoleで確認出来る公開鍵です。

INAPP_PURCHASE_DATAはレスポンスのjson文字列で、INAPP_DATA_SIGNATUREはそれを公開鍵で署名したものです。

署名検証にはjava.security.Signatureを利用し、流れとしてはSignaturegetInstanceメソッドで生成し、initVerifyメソッドで公開鍵で初期化。updateメソッドでレスポンスのjson文字列をバイト配列に変換したものを渡しverifyメソッドで検証します。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            String signatureData = data.getStringExtra("INAPP_DATA_SIGNATURE");
            if (purchaseData != null && signatureData != null) {
                if(verify(generatePublicKey(BASE64_ENCODED_PUBLIC_KEY),purchaseData, signatureData)) {
                    //署名OK
                }
            }
        }
    }
}

private final String SIGNATURE_ALGORITHM = "SHA1withRSA";
private final String KEY_FACTORY_ALGORITHM = "RSA";

public boolean verify(PublicKey publicKey, String signedData, String signature) {
    Signature sig;
    try {
        sig = Signature.getInstance(SIGNATURE_ALGORITHM);
        sig.initVerify(publicKey);
        sig.update(signedData.getBytes());
        if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
            return false;
        }
        return true;
    } catch (InvalidKeyException e) {
    } catch (NoSuchAlgorithmException e) {
    } catch (SignatureException e) {
    }
    return false;
}

public PublicKey generatePublicKey(String encodedPublicKey) {
    PublicKey pKey = null;
    try {
        byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
        pKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
    } catch (NoSuchAlgorithmException e) {
    } catch (InvalidKeySpecException e) {
    }
    return pKey;
}

署名のところはもう少し色々と考えなくてはいけないこともありますが、とりあえずは検証出来ました。
次回はここまで作成したもののテスト方法についてです。

Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ①リクエストを送る



GoogleがIn-app Billing Version 2 APIを殺すそうなので、もう長らく更新していなかったAndroidアプリを更新する羽目になっています。

昔version2の頃にアプリ内課金を実装したときは、正直意味が分からないけどサンプル通りに書いたら動いたというレベルだったので、結構苦手意識があります。

ただ、すごくシンプルになったという話も聞くので、これを機にまとめてみようと思います。


  • AIDLファイルの追加
サンプルアプリTrivialDriveに含まれている、Android Definititon Language(AIDL)ファイルを利用し、インターフェースファイルIIAppBillingService.javaを作ります。

Android SDK ManagerでGoogle Play Billing Libraryをインストールします 。


<android-sdk>/extras/google/play_billing/samples/TrivialDrive/からサンプルプロジェクトをインポートします。

TrivialDrivaサンプルの/srcディレクトリからIInAppBillingService.aidlファイルをコピーし、自分のアプリの/srcディレクトリへ貼り付けます。

/genディレクトリ内にIInAppBillingService.javaが自動的に作成されます。

  • AndroidManifestの更新
これはversion2と変わっていないようです。

AndroidManifest.xmlに以下のパーミッションを追加します。

<uses-permission android:name="com.android.vending.BILLING" />

  • ServiceConnectionの作成とIInAppBillingServiceへのバインド
接続時と切断時のコールバックメソッドを持つServiceConnectionを実装し、bindServiceメソッドでバインドします。

ServiceConnectionを実装します。

private IInAppBillingService mIInAppBillingService;
private ServiceConnection mServiceConnection= new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
        mIInAppBillingService = null;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mIInAppBillingService = IInAppBillingService.Stub.asInterface(service);
    }
};

IInAppBillingServiceを使用できるようにするため、ActivityonCreateメソッドでbindServiceメソッドを呼びます。
Intentの引数は、クラス名のIInAppBillingServiceではなく、InAppBillingServiceなのが紛らわしいですね。

Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);

終了処理として、Activityが終了時にonDestroyメソッドでunbindServiceメソッドでアンバインドします。

if (mServiceConnection != null) {
    unbindService(mServiceConnection);
}


なお、このあたりで大きくハマったのですが、bindServiceを呼んでからServiceConnectiononServiceConnectedが呼ばれるまでにはタイムラグがあり、直後にIInAppBillingServiceをいじくろうとするとぬるぽで死にます。

bindService自体はtrueで返ってくるのに、IInAppBillingServiceはnullのまま。

時間を計ってみたところ、50~150ミリ秒後ぐらいでまちまちでしたが、何にしろタイムラグがあります。

直後にスレッド止めたりして待ち合わせをしても、ほぼ同時に返ってくるものの結局ぬるぽ。

まぁ何のためにonServiceConnectedメソッドを実装する必要があるのか考えれば、分かりそうなものでしたが。。

ということで、わざわざコールバックメソッドを実装させられている訳なので、onServiceConnectedからトリガーしてあげればぬるぽにはならない訳です。

  • サポート状況の確認
version3のAPIをサポートしているか確認します。

int response = mIInAppBillingService.isBillingSupported(3, getPackageName(), "inapp");
if(response == BILLING_RESPONSE_RESULT_OK) {
    //OKの場合の処理
}

  • 購入状況の確認
getPurchasesメソッドを呼ぶと、購入済みアイテムの情報を取得出来ます。

RESPONSE_CODEや、INAPP_PURCHASE_ITEM_LISTなど、いくつかの情報を含むBundleが返されるので、必要に応じて取り出し利用します。

Bundle bundle = mIInAppBillingService.getPurchases(3, getPackageName(), "inapp", null);
if(bundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    ArrayList<String> purchases = bundle.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
    for (int i = 0, n = purchases.size(); i < n; i++) {
        //purchases.get(i)の中身によって処理を変える。
    }
}

  • 購入処理
getBuyIntentメソッドを呼んで、返されたBundleからBUY_INTENTキーのPendingIntentを取得します。
取得したPendingIntenstartIntentSenderForResultメソッドを呼び出します。

getBuyIntentメソッドの第4引数はDeveloper Payloadというやつで、空文字でもnullでもレスポンス時には空文字が返ってきます。
本来は任意の文字列を入れて、海賊版対策に使います。

startIntentSenderForResultメソッドの第2引数はコールバックを受けたときにリクエストを識別するための任意のint値です。

Bundle buyIntentBundle = mIInAppBillingService.getBuyIntent(3,
        getPackageName(), itemId, "inapp", "");
if (buyIntentBundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    PendingIntent pendingIntent = buyIntentBundle
        .getParcelable("BUY_INTENT");
    startIntentSenderForResult(pendingIntent.getIntentSender(),
        REQUEST_CODE, new Intent(), Integer.valueOf(0),
        Integer.valueOf(0), Integer.valueOf(0));
}

なお、サポート状況の確認から購入処理まで、IInAppBillingServiceのメソッドを呼ぶ際には、RemoteExceptionが発生するので、適時catchします。

あとはActivityのonActivityResultへレスポンスが来るので、適時処理するだけです。


次回はstartIntentSenderForResultのレスポンス部分、onActivityResultでの処理についてです。


Read More...

2014年11月15日土曜日

AdMobをGoogle Mobile Ads SDKを使って最新のライブラリで更新する



定期的に最新のライブラリへ更新を要求してきて、既存アプリのメンテを余儀なくさせるAdMobについてです。

しばらく見ない間に、Google Admob ads SDKだの、Google Play Services SDKだの、Google Mobile Ads SDKだの、ライブラリも良く分からない状況になっています。

軽く調べたところ、最新はGoogle Play 開発者サービスのGoogle Mobile Adsに内包されているようなので、既存のアプリを切り替えていきます。

かなりの期間放置していたので、何世代か前の状態からの更新になるかと思います。


まずはGoogle Mobile Ads SDKですが、Android3.2以降でのコンパイルが必要だそうです。
つまり、project.propertiestargetがandroid-13以降に設定されている必要があります。

次に、Android 2.3以降のランタイムが必要だそうです。
AndroidManifest.xmlandroid:minSdkVersion9以降に設定されている必要があります。

今回更新するアプリのminSdkVersionは今まで7にしていたので、今回の更新で2.1、2.2ユーザを切り捨てることになります。

悩ましいところですが、こういうことは定期的に発生し、どこかで割り切る必要があります。


では実際に既存アプリを修正していきます。


  • ライブラリの追加

Android SDK ManagerからGoogle Play servicesをインストールします。

完了すると<android-sdk>/extras/google/google_play_services/libproject/配下にプロジェクトが配置されます。

配置されたgoogle-play-services_libライブラリプロジェクトを、FileImportAndroidExisting Android Code into Workspaceでインポートします。

今回更新するプロジェクトを右クリックして、プロパティAndroidライブラリー追加から追加します。

ちなみに、既存アプリに上記の流れでライブラリを追加したところ、以下のエラーが出ました。

Conversion to Dalvik format failed: Unable to execute dex:
Multiple dex files define Lcom/google/ads/AdRequest$ErrorCode;

どうやらライブラリのコンフリクトというやつで、古いGoogleAdMobAdsSdkライブラリがライブラリフォルダに入っていたので削除で解決しました。


  • AndroidManifestの設定

AndroidManifest.xmlをいくつか修正します。

まずは、以下のmeta-dataタグを追加します。

<meta-data android:name="com.google.android.gms.version"
       android:value="@integer/google_play_services_version"/>

はじめは以下のエラーが出ていました。

No resource found that matches the given name (at 'value' with value'@integer/google_play_services_version').

ライブラリのところを見てみると、以下のように×がついています。

結局良く分からないのですが、色々調べながらgoogle-play-servicesのインポートをやり直したりしている内に直りました。

ただ、直った後に別のアプリに同じことをしていたらまた同じエラー出たり、良く分からない動きをします。ネットでもハマっている人は多そうでした。

結局、google-play-servicesのほうではなく、アプリのほうのインポートをやり直すとうまくいきました。

google-play-servicesライブラリを追加した時に、アプリによって絶対パスで表示されたり、相対パスで表示されたりしていたので、アプリ側のインポート時に問題があったのかも知れません。


次に、com.google.android.gms.ads.AdActivityを宣言します。

<activity android:name="com.google.android.gms.ads.AdActivity"
       android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize" />

ちなみに古いコードでは、android:namecom.google.ads.AdActivityだったので、ここだけ変更しました。


最後に、ネットワーク権限を設定します。

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

これは前と同じだったので触っていません。


  • Viewの追加
xmlで設定する場合は、com.google.android.gms.ads.AdViewを追加します。

<com.google.android.gms.ads.AdView android:id="@+id/adView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       ads:adUnitId="MY_AD_UNIT_ID"
       ads:adSize="BANNER"/>

com.google.ads.AdViewだったものを、com.google.android.gms.ads.AdViewに変更するだけです。

ただ、AdViewだけの変更だと、以下のIllegalStateExceptionが出ました。

Ad Size and Ad unit id must be set before loadAd is called

これはXMLネームスペースの設定を変更していないため発生していました。
前はxmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"だったものを、xmlns:ads="http://schemas.android.com/apk/res-auto"に変更します。


また、MY_AD_UNIT_IDの部分が、前はパブリッシャー IDというaから始まる15文字の文字列だったのに対し、現在は広告ユニットIDというca-app-pub-xxxxxxxxxxxxxxxx/nnnnnnnnnnという形式の文字列に置き換わりました。

これはAdMob管理画面内の収益化タブで確認出来ます。


ちなみにいつからあるのか知りませんが、ads:adSizeSMART_BANNERが選択出来るようになっています。

BANNERからSMART_BANNERに変更するだけで、自動的に画面サイズにあわせて真ん中に表示してくれます。

昔は自分で何かしら設定して、センタリングさせたと記憶しています。
これは便利ですね。


  • リソースのルックアップと読み込み

ActivityonCreateに以下コードを書きます。

AdView adView = (AdView)this.findViewById(R.id.adView);
AdRequest adRequest = new AdRequest.Builder().build();
adView.loadAd(adRequest);

自分の昔のコードを見るとAdRequestを直接newしていたのですが、AdRequestの生成方法が変わったようです。


  • テストデバイスの指定

そのまま実機で起動すると、テスト起動時にも普通に広告が表示されてしまうので、テスト用デバイスを指定しておきます。

一旦画面を表示させると、LogCatに以下のように端末IDのようなものが表示されます。

Use AdRequest.Builder.addTestDevice("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") to get test ads this device.

上記のIDをコピーして、AdRequestを以下のように生成すると、指定した実機ではテスト用のバナー広告が表示されるようになります。

AdRequest request = new AdRequest.Builder()
    .addTestDevice("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
    .build();

こんな感じになります。

ちゃんとセンタリングもされています。


ちなみに、実機でテストを実行するときに、製品版がインストールされてた場合、前はエラーになってた気がしますが、アンインストールするか聞いてくれるようになっています。

色々とイラっとしていた部分が便利になっていたりして、久しぶりに触ると楽しいです。
Read More...

2014年11月9日日曜日

Java EEとmonacoindでWEB Walletを作ってみた④ 動作確認


今回はここまでで作成したMonacoin WEB Walletの動作確認です。


まずはトップ画面を表示します。

ユーザ名が空の状態でログインしてみます。
@NotNullのバリデーションにひかかって、エラーが表示されます。

monacoindが起動していない状態で何か入れてログインしてみます。
当然エラーになります。

ちなみに今回は認証部分は未実装ですが、ログイン部分の判定をfalseにして認証エラーにした場合はこうなります。

monacoindを起動してログインしてみます。
新規の受信用アドレスが作成されて表示されます。
トランザクション履歴はまだないので空です。

表示されたWallet Address宛に送金してみます。
まだ認証されていないのでBalanceは0ですが、トランザクション履歴のところで送金されてきたことが確認出来ます。
また、このタイミングで新しい受信用アドレスが払い出されて表示されます。

しばらく待つと承認されてBalanceに反映されます。
トランザクション履歴のConfirmationsもちゃんと進んでいます。

続いて送金部分ですが、まず送金先アドレスが空の状態でSendボタンをクリックしてみます。
required="true"にひかかってエラーが表示されます。

おかしなアドレス宛に送金してみます。
JSON-RPCで返ってきたエラーが、Sendボタンの横に表示されます。

送金額をゼロにしてみます。
<f:validateDoubleRange minimum="0.00000001"/>にひかかってエラーが表示されます。

送金額を残高以上の金額にしてみます。
JSON-RPCで返ってきたエラーが表示されます。

送金が成功すると、成功したトランザクションIDが表示されます。
二重送信にならないよう、ここで送金先や送金額を消したほうが良さそうですね。

即、PC側のWalletに振り込まれてきました。


しばらく待つと、Confirmationsも問題なく進みました。

さすがBitcoinから延々と使いまわされてきたAPIだけあって、特に違和感なく動作します。

JSFについてはいくつか分からないところもありましたが、慣れたらなかなか効率的にWEBサービスが作れそうな感じです。


Read More...

2014年11月8日土曜日

Java EEとmonacoindでWEB Walletを作ってみた③ Walletの情報を保持するバッキングBeanを作成する


引き続き、MonacoinのWEB Walletを作成していきます。

今回はアカウントにひもづくWalletの情報を保持したり、送金処理をしたりするバッキングBeanです。

  • AccountBean

monacoindから取得したアカウントの情報をリクエストスコープで保持するバッキングBean。
リクエスト毎に最新の情報を取得して画面を更新するメソッドを持つ。
前回作成したUserBeanInjectして、アカウント名などを利用する。
受信用アドレス、残高、トランザクション履歴、送金先アドレス、送信金額を保持し、送金を実行するメソッドを持つ。


それではまずコードです。

package monawebwallet;

import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import javax.inject.Named;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;

@Named(value = "accountBean")
@RequestScoped
@NoArgsConstructor
@Getter
@Setter
public class AccountBean implements Serializable {

    @Inject
    UserBean user;

    String addressStr;
    String balanceStr;

    List<Map<String, String>> transactionMapList;

    String toAddress;
    double amount;

    public void updateAccount() {
        if (user.isLogin()) {
            try {
                addressStr = CoindUtil.getaccountaddress(user.getAccountStr());
                balanceStr = CoindUtil.getbalance(user.getAccountStr());
                transactionMapList = CoindUtil.listtransactions(user.getAccountStr());
            } catch (IOException | CoindException ex) {
                user.loginFormMessage(ex.getMessage());
                clear();
            }
        } else {
            clear();
        }
    }

    public void clear() {
        user.logout();
        addressStr = "";
        balanceStr = "";
        transactionMapList = null;
    }

    public void send() {
        try {
            String txid = CoindUtil.sendfrom(user.getAccountStr(), toAddress, amount);
            user.sendFormMessage("送信成功 : " + txid);
        } catch (IOException | CoindException ex) {
            user.sendFormMessage(ex.getMessage());
        }
    }
}

まず、受信用アドレス、残高、トランザクション履歴を更新するためのupdateAccount()メソッドを作成します。

それぞれJSON-RPCmonacoindから取ってきます。
何かエラーが出たときのために、すべてを消し去るためのclear()メソッドも作成しておきます。


ちなみにこのupdateAccount()ですが、リクエスト毎に毎回実行する初期化処理のようなものですが、@PostConstructを設定しなかったのには理由があります。

どうやら@PostConstructなメソッドが呼ばれるのは、画面を更新した時で、forrmsubmitした時にも呼ばれるのですが、formaction属性に指定したメソッドが呼ばれるまえに、@PostConstructなメソッドが呼ばれるようです。

submitの結果値が変わった場合は良いのですが、action属性のメソッドで値が変わった場合は反映されません。

例えば、ログアウト処理を実行するメソッドをaction属性に指定していた場合、ログアウト処理前の状態で@PostConstructが呼ばれてしまい、一旦無駄なJSON-RPCが飛んでしまう上、ログアウト処理後の状態でもう一度呼ぶ必要があります。

また、ログイン時は認証エラーでも一旦submitされるので、認証出来てない状態でJSON-RPCが飛んでしまいます。

①submit
②バッキングBeanのプロパティへ値反映
③@PostConstructなメソッド実行
④action属性のメソッド実行
⑤action属性のメソッド実行結果反映
⑥描画

ということで、上記⑤と⑥の間に差し込むために、@PostConstructは付けずに、headタグ内のEL式でupdateAccount()を呼んでいます。


最後に送金処理ですが、送金に関する一時的な情報をココで持つのはちょっと抵抗があったのですが、h:commandButtonタグのaction属性に設定したメソッドに、h:inputTextタグのvalueを渡す方法がよく分からなかったのでこうなりました。

これは間違いなくもっと良い方法がありそうですね。

formIdUIComponentを特定して直接取ってくるとかが出来るのかな?

とりあえず今回は動いたから良しとします。


これですべて実装し終わったので、次回は動作確認です。


Read More...

2014年11月7日金曜日

Java EEとmonacoindでWEB Walletを作ってみた② ユーザー情報を保持するバッキングBeanを作成する


引き続き、MonacoinのWEB Walletを作成していきます。

今回はアカウント名を保持するバッキングBeanです。


  • UserBean

ユーザ情報としてアカウント名をセッションスコープで保持するバッキングBean。
ログイン、ログアウト、ログイン状態確認のメソッドを持つ。(認証は未実装)
エラーメッセージ表示用のメソッドもここに作成。

エラーメッセージ表示については、ここに作るのが適正なのかはちょっとよく分からないですが、このバッキングBeanをInjectしてしまえば他からも使えるので、とりあえず。


それではまずコードです。

package monawebwallet;

import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
import javax.validation.constraints.NotNull;
import lombok.Getter;import lombok.Setter;

@Named(value = "userBean")
@SessionScoped
@Setter
@Getter
public class UserBean implements Serializable {

    @NotNull
    String accountStr;

    public boolean isLogin() {
        return accountStr != null && accountStr.length() > 0;
    }

    public void login() {
        if (true) {
            System.out.println("login");
        } else {
            logout();
        }
    }

    public void logout() {
        accountStr = "";
        System.out.println("logout");
    }

    public void loginFormMessage(String message) {
        message("loginForm:account", message);
    }
    
    public void sendFormMessage(String message) {
        message("sendForm:send", message);
    }

    private void message(String formId, String message) {
        FacesContext context = FacesContext.getCurrentInstance();
        UIComponent component = context.getViewRoot().findComponent(formId);
        String clientId = component.getClientId(context);
        context.addMessage(clientId, new FacesMessage(message));
    }
}

まず、この辺はおさらいですが、@NamedでCDI管理とし、EL式から参照するためにuserBeanという名前をつけます。

IDを保持させるので@SessionScopedでセッションスコープを設定します。


バッキングBeanの初期設定が終わったら、ユーザID保持用のaccountStr変数を用意します。
accountStrはnullや空文字をNGとしたいので、BeanValidationのアノテーションを設定します。

ただし、@NotNullを設定しても、デフォルトでは空文字を通してしまいます。
テキストボックスに何も入力されていない状態はnullではなく空文字です。

JSF標準のValidatorではなく、HibernateValidator@NotBlankを使う手もありますが、以下のようにweb.xmlに若干追加するだけで@NotNullでも空文字を蹴るようになります。

<context-param>
  <param-name>
    javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL
  </param-name>
  <param-value>true</param-value>
</context-param>

ただ、h:inputTextタグのrequired属性をtrueにすることで、どうやら同じことが可能なようなので、そちらのほうが良いかもしれません。
(と、後で気づいたけどせっかくなのでメモとして残しておく)


メソッドはlogin()logout()と、ログイン状態を判定するisLogin()を作成します。

現時点では認証ロジックの実装は行わず、login()メソッドを呼ぶと無条件でログイン成功とします。


最後にエラーメッセージ表示用のmessage()メソッドも作成しておきます。

formIdformタグのIDとinputタグなどのIDをコロンでつないだものです。

formIdをキーにしてUIComponentを取得し、getClientId()clientIdを取得します。

これでエラーメッセージをハンドリング出来るようになります。


まだ続きます。

Read More...

2014年11月6日木曜日

Java EEとmonacoindでWEB Walletを作ってみた① JSFでUIを作成する


前回作ったmonacoindユーティリティクラスを利用して、MonacoinのWEB Walletを作成してみます。


まず全体の構成としては、HTMLは1ページでログイン状態によって表示を変え、バッキングBeanはセッションスコープでアカウント名を保持するものと、リクエストスコープでアカウントの情報を保持するもので構成します。


  • index.xhtml

アカウント名を入力してログインボタンを押すと、受信用アドレス、残高、トランザクション履歴、送信先アドレス、送信金額欄、送信ボタンが表示される。
ログイン前はアカウント名入力欄とログインボタン以外非表示。

パスワード欄無し。(今回は実装する気が無いだけ)


それではまずコードです。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core">
    <h:head>
        <title>Monacoin Web Wallet</title>
        #{accountBean.updateAccount()}
    </h:head>
    <h:body>
        <h:outputLink value="/MonaWebWallet/">トップ</h:outputLink>
        <h:form id="loginForm">
            <h:message for="account" /><br />
            <h:inputText id="account" value="#{userBean.accountStr}" rendered="#{! userBean.isLogin()}" />
            <h:commandButton value="Login" action="#{userBean.login()}" rendered="#{! userBean.isLogin()}" />
            <h:commandButton value="Logout" action="#{userBean.logout()}" rendered="#{userBean.isLogin()}" />
            <br />
        </h:form>

        <h:form rendered="#{userBean.isLogin()}">
            Account : <h:outputText value="#{userBean.accountStr}" />
            <br />
            Wallet Address : <h:outputText value="#{accountBean.addressStr}" />
            <br />
            Balance : 
            <h:outputText value="#{accountBean.balanceStr}">
                <f:convertNumber type="number" currencySymbol="#,##0.00000000" />
            </h:outputText>
            <br /><br /><br />
        </h:form>

        <h:form id="sendForm" rendered="#{userBean.isLogin()}">
            Send to : 
            <h:inputText id="sendTo" value="#{accountBean.toAddress}" size="50" maxlength="34" required="true">
                <f:validateLength maximum="34" minimum="34" />
            </h:inputText>
            <h:message for="sendTo" />
            <br />
            Amount : 
            <h:inputText id="sendAmount" value="#{accountBean.amount}" required="true">
                <f:validateDoubleRange minimum="0.00000001"/>
            </h:inputText>
            <h:message for="sendAmount" />
            <br />
            <h:commandButton id="send" value="Send" action="#{accountBean.send()}" rendered="#{userBean.isLogin()}" />
            <h:message for="send" />
            <br /><br />
        </h:form>

        <h:form rendered="#{userBean.isLogin()}">
            Transactions : <br />
            <h:dataTable border="1" var="transactionMap"
                         value="#{accountBean.transactionMapList}" rendered="#{accountBean.transactionMapList != null and ! accountBean.transactionMapList.isEmpty()}">
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="Date"/>
                    </f:facet>
                    <h:outputText value="#{transactionMap.time * 1000}">
                        <f:convertDateTime pattern="yyyy/MM/dd HH:mm:ss" timeZone="JST"/>
                    </h:outputText>
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="Type"/>
                    </f:facet>
                    <h:outputText value="#{transactionMap.category}"/>
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="Address"/>
                    </f:facet>
                    <h:outputText value="#{transactionMap.address}"/>
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="Amount"/>
                    </f:facet>
                    <h:outputText value="#{transactionMap.amount}"/>
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="Confirmations"/>
                    </f:facet>
                    <h:outputText value="#{transactionMap.confirmations}"/>
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="Transaction ID"/>
                    </f:facet>
                    <h:outputText value="#{transactionMap.txid}"/>
                </h:column>
            </h:dataTable>
        </h:form>
    </h:body>
</html>


まず、ヘッダー部分にEL式でバッキングBeanのメソッドが仕込んであり、ここで初期化処理を行っています。

Javascriptで言うところの、bodyタグのonLoradのつもりで使っていますが、ここでやるのが正しいのかは良く分かりません。
何かもっとそれっぽい方法がありそうですが、色々調べたり実験したりしたのですが、これしかうまくいきませんでした。


続いて中身を見ていきますが、基本的に入力が必要なものはh:inputTextタグ、バッキングBean側から表示が必要なものはh:outputTextタグを利用しています。

トランザクション履歴については、h:dataTableタグでListtableに変換して表示します。

それぞれEL式でバッキングBeanのプロパティへバインドします。


バリデーション周りについては、アカウント名入力欄については、バッキングBean側でバリデーションをかけて入力必須にし、HTML側では特に何も設定していません。

送金先アドレスや金額欄については、f:validateLengthタグで文字列の長さ、f:validateDoubleRangeタグで数値の桁数、required属性で入力必須にしています。

入力必須にするだけであれば、後者のほうが楽なのでこっちに統一で良さそうな感じです。


エラーメッセージを表示したい場所については、h:messageタグを設置し、for属性でinput部品と関連付けます。

バリデーションのエラーを表示させたり、バッキングBeanから指定のメッセージを表示させたりします。


最後に表示非表示についてですが、ブロック毎にh:formタグで囲み、rendered属性で表示非表示を切り替えました。
やり方として正しいのか良く分かりませんが、divタグとかではrendered属性が使えないようで、コレしか思いつきませんでした。


これでUIのパーツは揃ったので、次回はバッキングBeanの作成です。


Read More...

2014年11月3日月曜日

Javaでmonacoindユーティリティクラスを作成する② WEB Walletに必要なメソッドの実装



引き続きユーティリティクラスの作成です。

Walletに最低限必要な以下4つのメソッドを実装します。

getaccountaddress
getbalance
listtransactions
sendfrom


まず、JSONRequestクラスですが、メンバ変数はString型のidmethodList<Object>型のparamsです。

idはレスポンスに同じidが含まれて返ってくるだけで、取り合えずは使わないのでハードコードしてしまいます。

methodはそれぞれのメソッド文字列を保持します。

paramsはメソッドにより異なり、sendfromでは送り先アドレスや金額もこのparamsに保持させます。

  • getaccountaddress

アカウント文字列を渡すと、アカウントの最新の受信用アドレスを返します。

public static String getaccountaddress(String accountStr) throws IOException, CoindException {
    String method = "getaccountaddress";

    List<Object> params = new ArrayList<>();
    params.add(accountStr);
    JSONResponse addressRes = connectCoind(createJSONString("1", method, params));

    return addressRes.result.toString();
}

  • getbalance

アカウント文字列を渡すと、アカウントの残高を返します。

public static String getbalance(String accountStr) throws IOException, CoindException {
    String method = "getbalance";
    List<Object> params = new ArrayList<>();
    params.add(accountStr);
    JSONResponse balanceRes = connectCoind(createJSONString("1", method, params));
    return balanceRes.result.toString();
}

  • listtransactions

アカウント文字列を渡すと、トランザクション履歴を返します。
何件目から何件目までという取得の仕方も可能ですが、今回は実装しません。

public static List<Map<String, String>> listtransactions(String accountStr) throws IOException, CoindException {
    String method = "listtransactions";
    List<Object> params = new ArrayList<>();
    params.add(accountStr);
    JSONResponse transactionsRes = connectCoind(createJSONString("1", method, params));
    return (List<Map<String, String>>) transactionsRes.result;
}

  • sendfrom

アカウント文字列、送信先アドレス、金額を渡し、成功するとそのトランザクションIDを返します。
送信先アドレスの無効、金額不足など、エラーの発生しやすいメソッドです。

public static String sendfrom(String accountStr, String toAddress, double amount) throws IOException, CoindException {
    String method = "sendfrom";
    List<Object> params = new ArrayList<>();
    params.add(accountStr);
    params.add(toAddress);
    params.add(amount);
    JSONResponse sendRes = connectCoind(createJSONString("1", method, params));

    return sendRes.result.toString();
}


ちなみにアカウント名を空文字列にすれば、アカウント名を指定しなかったことになって残高などWallet全体の数字になるかと思いきや、空文字列もアカウントとしては有効で、空文字アカウントの残高が返されます。

エラー周辺で結構ハマりましたが、これで一通り必要な機能が実装できました。


Read More...

Javaでmonacoindユーティリティクラスを作成する① JSONを扱う


今回は、JSON-RPCmonacoind操作全般を受け持つユーティリティクラスを作成します。


まずはJSONの扱いですが、2013年のJava EE7の頃に、標準のJSONライブラリが追加されたとか聞いていましたが、あんまりいけてないようです。
POJOにバインドする機能とかもないらしく、自前でマッピングしてあげないといけない?

まぁ実際のところはよく分かりませんが、無難にJacksonを使います。

公式からjackson-corejackson-databindをダウンロードしてライブラリに登録しておきます。


リクエスト用とレスポンス用のPOJOをそれぞれ作成し、Jacksonを使ってinoutstreamと直接変換します。

  • リクエスト用クラス

バージョンは1.0固定で良さそうなのでハードコードしてしまいます。
final宣言しておくと、@AllArgsConstructorアノテーションでもjsonrpc抜きのコンストラクタが作成されます。

paramsにはStringdoubleが入るので、Object型にしておきます。

@AllArgsConstructor
@Getter
@Setter
public class JSONRequest {

    final String jsonrpc = "1.0";
    String id;
    String method;
    List<Object> params;
}

  • レスポンス用クラス

errorは普段nullですが、エラーの場合にはエラー番号やエラー内容がまとめて入ってくるので、MapvalueObject型にしておきます。

@Getter
@Setter
public class JSONResponse {

    Object result;
    Map<String, Object> error;
    String id;
}

また、monacoindとの通信で発生するエラーを格納するExceptionクラスも作成しておきます。

  • monacoind用Exceptionクラス

monacoindが返してきたエラーメッセージを格納出来るようにしておきます。

@NoArgsConstructor
public class CoindException extends Exception {

    public CoindException(String msg) {
        super(msg);
    }
}

前準備は整ったので、ここからはCoindUtilクラスを作成し、必要なメソッドを実装していきます。

  • リクエスト用文字列を作成するメソッド

ObjectMapperクラスのwriteValueAsStringメソッドで、JSONRequestクラスからJSON文字列を作成します。

public static String createJSONString(String id, String method, List<Object> params) throws JsonProcessingException {
    JSONRequest jsonRequest = new JSONRequest(id, method, params);

    return new ObjectMapper().writeValueAsString(jsonRequest);
}

  • JSON文字列を受け取り、monacoindと通信するメソッド

今回はメインPCから実験用PCにアクセスしているので、monacoin.confrpcallowipでローカルホスト以外からのアクセスも許可しています。
ID、PWはmonacoin.confに設定したID、PWです。

リクエストはJSON文字列をOutputStreamPrintWriterでそのまま書き込んで送信します。

何気に、try-with-resource初めて使ったかも。


レスポンスはInputStreamを受け取って、ObjectMapperクラスのreadValueメソッドで、JSONResponseクラスに変換します。

IOExceptionはそのまま上に投げ、monacoindがエラーを返した場合は、先ほど作成したCoindExceptionにメッセージを渡してから上に投げます。

public static JSONResponse connectCoind(String json) throws IOException, CoindException {
  
    final String urlStr = "http://192.168.1.15:9359";
    final String rpcuser = "rpcuser";
    final String rpcpassword = "rpcpassword";

    URL url = new URL(urlStr);
    URLConnection connection = url.openConnection();

    Authenticator.setDefault(new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication(rpcuser, rpcpassword.toCharArray());
        }
    });
    connection.setDoOutput(true);

    connection.setRequestProperty("content-type", "text/plain;");
    OutputStream out = connection.getOutputStream();

    try (PrintWriter pw = new PrintWriter(out)) {
        pw.print(json);
    }

    JSONResponse response;
    try (InputStream in = connection.getInputStream()) {
        ObjectMapper mapper = new ObjectMapper();
        response = mapper.readValue(in, JSONResponse.class);
    } catch (IOException e) {
        try (InputStream es = ((HttpURLConnection) connection).getErrorStream()) {
            ObjectMapper mapper = new ObjectMapper();
            response = mapper.readValue(es, JSONResponse.class);
        }
    }

    Optional<Map<String, Object>> errorOpt = Optional.ofNullable(response.error);
    if (errorOpt.isPresent()) {
        throw new CoindException((String) errorOpt.get().get("message"));
    }

    return response;
}


ここで死ぬほどハマったのが、cURLからだと問題無くJSONでエラーが返ってくる状況で、普通にInputStreamを取り出すと、HTTP response code: 500が返って来たことです。

エラーの内容を見たいのに、500が返ってくるとIOExceptionが投げられてしまい、InputStreamを弄れません。

そして、Google先生との長い対話の末たどり着いたのが、ErrorStreamです。

まず一度IOExceptioncatchして、URLConnectionHttpURLConnectionにキャストした上で、getErrorStream()で改めてInputStreamを取得します。

コイツを弄ってやると、ちゃんとmonacoindが返したエラーが読めました。

エラーメッセージが読めた時は感動しましたが、これ設計としてどうなんでしょうか?常識なの?
なんだか釈然としません。


そして最後にせっかくなのでJava8で導入されたOptional使ってみました。
が、ラムダ式内部からExceptionを投げるのがうまくいかなかったので、普通にif文判定。

わざわざOptionalを使った意味があるかは不明ですが、いいんです。
使いたかっただけだから。


次回は実際にmonacoindとやり取りするメソッドの実装です。

Read More...

2014年9月7日日曜日

NetBeansを使ったJava EEアプリケーション開発


最近のJava Webフレームワークについて調べてみたところ、どうやらJava EEが良いようなので、今回はJava EEに手を出してみます。


Java EEについて調べていると、EclipseよりNetBeansで説明されていることが断然多いです。
単にJavaでの開発というとシェア的にはEclipseに分があるものの、Java EEやるならNetBeansって事でしょうか。

今まではEclipse派でしたが、NetBeansはインストールしたらすぐ使える的なことも良く書いてあるので、今回はNetBeansを試してみようと思います。


まずは、NetBeansのインストールからと言いたいところですが、NetBeansのサイトからダウンロードしてインストーラの指示通りインストールするだけでした。

インストールは割愛し、プロジェクトの作成、バッキングBeanの作成、JSFページの作成について、基本的な部分を試してみます。

今回は、ユーザー名を入力してログインボタンを押すと、ログインしたユーザー名を表示し、ログアウトボタンを押すとユーザー名が消えるだけのサイトを作成します。

  • プロジェクトの作成

新規プロジェクト⇒Java Web⇒WEBアプリケーション

プロジェクト名の入力

Java EEのバージョンやアプリケーションサーバの選択

フレームワークの選択

必要なファイルと階層構造が生成されます。

おもむろに実行すると、初期状態のindex.xhmlが開きます。

ここまでは細かい設定いらず、非常に簡単です。


また、コードを触る前にLombokというライブラリを入れておきます。
ボイラープレートコードをアノテーションだけでコンパイル時に生成してくれるかなり使えるライブラリです。

設定はLombokのサイトからlombok.jarをダウンロードして、ライブラリフォルダに登録するだけです。


  • バッキングBeanの作成

新規⇒JavaServer Faces⇒JSF管理対象Bean

Bean名やパッケージ名の入力

ソース・パッケージフォルダに指定した名前のjavaファイルが生成されます。
表示させると以下のようなコードになっています。

package monawebwallet;

import javax.inject.Named;
import javax.enterprise.context.SessionScoped;
import java.io.Serializable;

@Named(value = "loginBean")
@SessionScoped
public class LoginBean implements Serializable {

    /**
     * Creates a new instance of LoginBean
     */
    public LoginBean() {
    }
   
}

いわゆるJavaBeansですが、@Named@SessionScopedの二つのアノテーションを使用しています。

@Namedアノテーションは決まりごとで、ここで指定した名前(value)でELからアクセス出来ます。

@SessionScopedアノテーションは、バッキングBeanのスコープを定義します。
他にも@RequestScoped@ApplicationScopedなどがありますが、アカウントを保持させるので、ここではセッションスコープにしました。

このコードにアカウント文字列を保持するプロパティを追加し、Lombokの@NoArgsConstructor@Getter@Setterアノテーションを付けます。

package monawebwallet;

import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
import lombok.NoArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Named(value = "userBean")
@SessionScoped
@NoArgsConstructor
@Getter
@Setter
public class UserBean implements Serializable {

    String accountStr;
}

非常にすっきりしていますが、これでデフォルトコンストラクタとゲッター、セッターがコンパイル時に生成されます。


  • JSFページの作成

新規⇒JavaServer Faces⇒JSFページ

ファイル名の入力

Webページフォルダ直下に指定した名前のxhtmlファイルが生成されます。

ちなみにこのページにアクセスするには、http://localhost:8080/MonaWebWallet/login.xhtmlではなく、http://localhost:8080/MonaWebWallet/faces/login.xhtmlです。

デフォルトのindex.xhtmlについては、http://localhost:8080/MonaWebWallet/でアクセス可能ですが、実際にはweb.xmlでウェルカムページとして設定されている、faces/index.xhtmlにアクセスしています。

詳しくはweb.xmlを見て頂きたいのですが、faces配下のファイルがFacesServletの管理対象としてマッピングされています。

これ、faces配下じゃないほうにアクセスしても普通のhtml部分は表示されるため、ちょっとハマりました。
そして、そのままFacesServletが処理する前のソースが見えてしまうのでなんだか微妙なのですが、表示させない方法についてはまた別の機会に。


新規でJSFページを作る際は上記の流れですが、今回はデフォルトで生成されているindex.xhtmlを書き換えて使います。

表示させると以下のようになっています。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        Hello from Facelets
    </h:body>
</html>

<h:body>タグ内を以下のように書き換えます。

<h:form>
    <h:inputText id="account" size="30" value="#{userBean.accountStr}" />
    <h:commandButton id="login" value="ログイン" />
    <h:commandButton id="logout" value="ログアウト" actionListener="#{userBean.setAccountStr('')}" />
</h:form>
<b><h:outputText value="#{userBean.accountStr}" /></b>

#{userBean.accountStr}のように、ELを使用することでバッキングBeanのプロパティにアクセス出来ます。

<h:form>タグの中に<h:inputText>タグを入れて、valueにELでバッキングBeanのプロパティを設定しておくと、フォームをSubmitした際に、プロパティのセッターが呼ばれて値が格納されます。

<h:outputText>タグのvalueに設定すると、プロパティのゲッターが呼ばれて、値が取り出されます。

ログイン前

ログイン後

  • 日本語対応

日本語を入力すると壮大に文字化けしました。

新規⇒Glassfish⇒Glassfish ディスクリプタ

そのまま進めると、「このプロジェクトのデプロイメント構成が見つかりません。デプロイメント・ディスクリプタのバージョンを正しく設定できませんでした。」とエラーが出ましたが、とりあえず放置します。

構成ファイルフォルダの中にsun-web.xmlが生成されるので、以下のように<parameter-encoding>タグを追加します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD GlassFish Application Server 3.0 Servlet 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-web-app_3_0-0.dtd">
<sun-web-app error-url="">
    <class-loader delegate="true"/>
    <jsp-config>
        <property name="keepgenerated" value="true">
            <description>Keep a copy of the generated servlet class' java code.</description>
        </property>
    </jsp-config>
    <parameter-encoding default-charset="UTF-8" />
</sun-web-app>

うまくいきました。

Read More...

2014年8月11日月曜日

サーバーサイドJAVAの環境を整える


Monacoindの基本的な動きは大体分かったので、実際にWEBサービスの形にしてみようと思います。

Linuxもそんなに詳しくないので、色々と試しながらやってみます。

使う言語はJava。

なぜJavaかというと、過去に、Google App Engine for Java(GAE/J)でWEBサービスを作るというような記事を書いて、フレームワークについても調べて、Slim3やべぇなどと騒いだあげくに、何も作らなかったので、贖罪の意味を込めてJavaを使います。

今回は環境作りです。


まずは、Apacheをインストールします。

$ sudo apt-get update
$ sudo apt-get upgrade

$ sudo apt-get install apache2

この時点で既にApacheは起動しているようで、http://localhostにアクセスすると以下が表示されます。

「あなたの予想に反して~」はどこ行っちゃったんでしょう。
It works!なんて言われても、こっちのほうが予想外です。

とりあえず設定ファイルなどは触らずに次行きます。


次は、Tomcatをインストールします。

$ sudo apt-get install tomcat7

これで必要なJavaなどもまとめてインストールされ、tomcat7ユーザが作成されます。

同じくこの時点でTomcatは起動しているようで、http://localhost:8080にアクセスすると以下が表示されます。

続いて、ドキュメントやらWeb Management Interfaceやらexampleやらをインストールします。

$ sudo apt-get install tomcat7-docs tomcat7-admin tomcat7-examples

以下二つはそのまま見れます。

tomcat7-docs

tomcat7-examples

以下二つは認証が必要です。
tomcat7-admin (manager webapp)
tomcat7-admin (host-manager webapp)

tomcat-users.xmlを編集し、manager-guiとadmin-guiへアクセスするためのIDを作成します。

任意のIDとパスワードを設定します。

$ sudo nano /etc/tomcat7/tomcat-users.xml

tomcat-usersタグの中に以下を追加。
<tomcat-users>
    <user username="admin" password="password" roles="manager-gui,admin-gui"/>
</tomcat-users>

保存したら再起動します。
$ sudo service tomcat7 restart

それぞれ、自分で設定したIDとパスワードでログインします。

tomcat7-admin (manager webapp)


tomcat7-admin ( host-manager webapp)


最後にApacheとの連携の設定を行います。

Apache&Tomcatの連携は、静的なコンテンツはApacheに担当させ、動的なコンテンツをTomcatに担当させるという、良く見る構成です。

ただ、Tomcat単体で動かした場合より低速になったりするようですし、そもそもJSPなどでページを作って行くと、Apacheに担当させるような静的なコンテンツは少なくなり、Apacheは無くても良いという説もあります。

Apacheの拡張機能などを使わないのであれば、Tomcat単体運用も選択肢の一つのようですね。

でもとりあえず連携させときます。

tomcatのserver.xmlを開いて、8080ポートを閉じ、8009ポートを開けます。
$ sudo nano /etc/tomcat7/server.xml

以下をコメントアウトします。
<!--
<Connector port="8080" protocol="HTTP/1.1"
    connectionTimeout="20000" URIEncoding="UTF-8" redirectPort="8443" />
-->

以下のコメントアウトを削除します。
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />


AJPモジュールの設定ファイルを作成し、連携の設定を行います。
$ sudo nano /etc/apache2/mods-available/proxy_ajp.conf

以下を入力して保存します。
ProxyPass /tomcat/ ajp://localhost:8009/

AJPモジュールを有効化します。
$ sudo a2enmod proxy_ajp

http://localhost/tomcat/にアクセスすると、ApacheからTomcatに飛ばされて、TomcatのIt works !が表示されます。


これでApacheもTomcatもデフォルトで起動するようになっているので、あとは何か作るだけです。
気長にやります。


Read More...

2014年8月4日月曜日

JSON-RPCを使ってmonacoindを操作する2 WEB Walletを作るには?


Bitcoin API calls 和訳版を参考に、WEB Walletを作るには?という視点でmonacoindで色々やってみました。

流れ的には以下のような感じでしょうか。


getaccountaddressでDeposit用のアドレスを作成。
その際、ユーザ固有のアカウントをアドレスに紐付ける。

既存のものではなく、新しいアドレスを作成するには、getnewaddressで新規作成する。
アカウントのアドレスリストは、getaddressesbyaccountで取得する。

getbalanceでアカウントの残高を確認。
listtransactionsでトランザクション履歴を確認。

sendfromで指定のアドレスへWithdrawする。


では、それぞれのメソッドを確認していきます。


  • getaccountaddress

指定したアカウントに支払いを受けるための、最新のアドレスを返します。
なければ作成、あればそれを返す。

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"1","method":"getaccountaddress","params":["testAc"]}' -H 'content-type:text/plain;' http://127.0.0.1:9359
{"result":"MVPMzNXWwu4db4uwUzFm3NbDLRvpjmfkSC","error":null,"id":"1"}

MVPMzNXWwu4db4uwUzFm3NbDLRvpjmfkSCがtestAcにおけるDeposit用のアドレスとなります。

  • getnewaddress

呼ぶたびに新しいアドレスを返します。(以下は2回呼んだ結果)

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"1","method":"getnewaddress","params":["testAc"]}' -H 'content-type:text/plain;' http://127.0.0.1:9359

{"result":"MUh5HD2VEC1UGZxxF1TRcKKsFCbGfmjEpq","error":null,"id":"1"}
{"result":"MS6WsHkyb9cFrHrxKFmi7bARwwpvNRvhsH","error":null,"id":"1"}

内部的にどうなっているのか、先ほど使ったgetaccountaddressで確認してみます。

{"result":"MVPMzNXWwu4db4uwUzFm3NbDLRvpjmfkSC","error":null,"id":"1"}

受信用アドレスは、新しいアドレス生成前と変わっていません。


  • getaddressesbyaccount

指定したアカウントのアドレスリストを返します。

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"1","method":"getaddressesbyaccount","params":["testAc"]}' -H 'content-type:text/plain;' http://127.0.0.1:9359

{"result":["MUh5HD2VEC1UGZxxF1TRcKKsFCbGfmjEpq","MS6WsHkyb9cFrHrxKFmi7bARwwpvNRvhsH","MVPMzNXWwu4db4uwUzFm3NbDLRvpjmfkSC"],"error":null,"id":"1"}

もともとのアドレスに加え、新規で生成したアドレスがアカウントに紐付いています。


ここで0.01MONAほど入金してみます。

getaddressesbyaccountで確認すると、アドレスが増えています。

{"result":["MLGDvB3UoYEV9FvWkq2xkvFrxh6bPLniQE","MUh5HD2VEC1UGZxxF1TRcKKsFCbGfmjEpq","MS6WsHkyb9cFrHrxKFmi7bARwwpvNRvhsH","MVPMzNXWwu4db4uwUzFm3NbDLRvpjmfkSC"],"error":null,"id":"1"}

getaccountaddressで確認すると、新しいアドレスが入金用アドレスに変わっています。

{"result":"MLGDvB3UoYEV9FvWkq2xkvFrxh6bPLniQE","error":null,"id":"1"}

  • getbalance

指定したアカウントの残高を返します。

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"1","method":"getbalance","params":["testAc"]}' -H 'content-type:text/plain;' http://127.0.0.1:9359

{"result":0.01000000,"error":null,"id":"1"}

  • listtransactions

トランザクション履歴を返します。

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"1","method":"listtransactions","params":["testAc"]}' -H 'content-type:text/plain;' http://127.0.0.1:9359

{"result":[{"account":"testAc","address":"MVPMzNXWwu4db4uwUzFm3NbDLRvpjmfkSC","category":"receive","amount":0.01000000,"confirmations":6,"blockhash":"d4dd988599a7649897a80ed94dd5f9e3642c9be33d76fa067bda3dcf352802b0","blockindex":18,"blocktime":1407077735,"txid":"9ee8386695853db90dd2d37abc46e65874a68db51e4daf2721e1ce1a3441156b","normtxid":"0e36640249a1b732c99311e8babbc85a7db43f7640b136188e5a78853b014d6f","time":1407077288,"timereceived":1407077288}],"error":null,"id":"1"}

categoryreceiveのトランザクション履歴が確認出来ます。


  • sendfrom

指定したアカウントから送金します。

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"1","method":"sendfrom","params":["testAc","MR2j3u5oNWZzAAxeHtSinHUEVNiSbkn8zJ",0.001]}' -H 'content-type:text/plain;' http://127.0.0.1:9359

{"result":"92828b7bf40b85b41f7deeb4e8f90ed2a14ced8efa83875132ca9319967f771f","error":null,"id":"1"}

listtransactionsで再度トランザクション履歴を確認してみます。

{"result":[{"account":"testAc","address":"MVPMzNXWwu4db4uwUzFm3NbDLRvpjmfkSC","category":"receive","amount":0.01000000,"confirmations":7,"blockhash":"d4dd988599a7649897a80ed94dd5f9e3642c9be33d76fa067bda3dcf352802b0","blockindex":18,"blocktime":1407077735,"txid":"9ee8386695853db90dd2d37abc46e65874a68db51e4daf2721e1ce1a3441156b","normtxid":"0e36640249a1b732c99311e8babbc85a7db43f7640b136188e5a78853b014d6f","time":1407077288,"timereceived":1407077288},{"account":"testAc","address":"MR2j3u5oNWZzAAxeHtSinHUEVNiSbkn8zJ","category":"send","amount":-0.00100000,"fee":-0.00100000,"confirmations":0,"txid":"92828b7bf40b85b41f7deeb4e8f90ed2a14ced8efa83875132ca9319967f771f","normtxid":"8a13b422a5c0adbe08b4b410d22457e1570142bfde66068a708ec19eddc8aee4","time":1407078088,"timereceived":1407078088}],"error":null,"id":"1"}

categorysendのトランザクションが増えました。


WEB Walletなんてのは実装としては意外と簡単なんですね。

セキュリティとか考えなければ、ですが。

Read More...

2014年8月3日日曜日

JSON-RPCを使ってmonacoindを操作する


JSON-RPCの動きを検証するために、jqueryなどを使って適当なJavascriptでテストページを作ったのですが、クロスオリジンの問題でうまく動きません。

ポートが異なるだけで別オリジン扱いのようなので、どうしようもないのかな?と思っていたところ、動きをテストするだけならもっと簡単な方法がありました。

cURLという、様々なプロトコルでデータを送受信できるユーティリティを使います。

試しにインストールして使ってみましょう。

$ sudo apt-get install curl
$ curl http://monacoin.org/ja/

これでだーっと標準出力にサイトのソースが表示されます。


では実際にmonacoindを操作してみましょう。

monacoindとJSON-RPCを使って会話をするために必要なIDとPWは、初回起動時にmonacoin.confに設定しているはずです。

そのIDとPWを使ってデフォルトのrpcportにアクセスすると、ちゃんと返事があります。

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"curltext","method":"getinfo","params":[]}' -H 'content-type:text/plain;' http://127.0.0.1:9402

{"result":{"version":80700,"protocolversion":70002,"walletversion":60000,"balance":0.01800000,"blocks":208195,"timeoffset":44,"connections":8,"proxy":"","difficulty":1105.55691097,"testnet":false,"keypoololdest":1405931648,"keypoolsize":104,"paytxfee":0.00000000,"mininput":0.00001000,"errors":""},"error":null,"id":"curltext"}


monacoin.confの他の設定項目についても見てみます。

いくつかある設定項目の内、重要なのはrpcallowipとrpcportです。
server、listenについては設定しなくても返事があるので、qtを使う時に必要な設定なのか、デフォルトが有効な設定のようです。

まずrpcportを設定すると、待ち受けるポートをデフォルトの9402から変更出来ます。

rpcport=9359

monacoin.confで上記のように設定すると、以下のように9359ポートで動いているのが分かります。

$ curl --user 'rpcuserid:rpcpassword' --data-binary '{"jsonrpc":"1.0","id":"curltext","method":"getinfo","params":[]}' -H 'content-type:text/plain;' http://127.0.0.1:9359

{"result":{"version":80700,"protocolversion":70002,"walletversion":60000,"balance":0.01800000,"blocks":208196,"timeoffset":44,"connections":6,"proxy":"","difficulty":938.68052376,"testnet":false,"keypoololdest":1405931648,"keypoolsize":104,"paytxfee":0.00000000,"mininput":0.00001000,"errors":""},"error":null,"id":"curltext"}

次に、rpcallowipを設定すると、別のIPからでもアクセス出来るようになります。

以下のコメントアウトした1行目のように設定にすると、ネットワークごとアクセス許可が出来ます。

が、基本的にはlocalhostからのみの接続とすべきでしょう。
そうであれば、デフォルトでlocalhost限定なので、わざわざ設定しなくても良さそうですが、設定しろとよく書いてあるので、念のため。

#rpcallowip=192.168.0.*
rpcallowip=127.0.0.1

ちなみにrpcpasswordを間違えると、以下のようにHTMLでレスポンスがあります。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
<HTML>
<HEAD>
<TITLE>Error</TITLE>
<META HTTP-EQUIV='Content-Type' CONTENT='text/html; charset=ISO-8859-1'>
</HEAD>
<BODY><H1>401 Unauthorized.</H1></BODY>
</HTML>


Read More...

2014年7月24日木曜日

ethereumをビルドしてみる


突然Newsletterが届いたので、ethereumをビルドしてみました。

今どういうフェーズなのか知りませんが、とりあえず何も考えずにビルド。


wikiを見ながら試しました。

とりあえず準備。
sudo apt-get update && sudo apt-get upgrade

まず、必要パッケージのインストール。

sudo apt-get install build-essential g++-4.8 git cmake libgmp-dev \
libboost-all-dev automake unzip libtool libleveldb-dev yasm libminiupnpc-dev \
libreadline-dev scons libncurses5-dev qtbase5-dev qt5-default qtdeclarative5-dev \
libqt5webkit5-dev libcurl4-openssl-dev


Cryptopp 5.6.2が必要だそうですが、libcrypto++-devにはCrypto++ v5.6.1しかないので、ソースからビルドする。

git clone https://github.com/mmoss/cryptopp.git
cd cryptopp
sudo scons --shared --prefix=/usr
cd ..


JSONRPCもソースから。

git clone git://github.com/cinemast/libjson-rpc-cpp.git
cd libjson-rpc-cpp/build
cmake .. && make
sudo make install
sudo ldconfig
cd ..


ethereumのソースを拾って来る。

git clone https://github.com/ethereum/cpp-ethereum
cd cpp-ethereum
git checkout develop


ビルドする。

mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release && make


CUIで起動
cd eth
./eth


GUIで起動
cd alethzero
./alethzero


この辺を見ながら動かしてみたけど、いまいち何が起きてるのか不明。

ま、とりあえずメモってことで。

Read More...

2014年7月21日月曜日

意外と簡単に動いた! monacoindのビルドから実行まで


何か暗号通貨関連でサービスを作りたいと思い立ち、準備運動としてWalletのビルドを前回やったので、次にデーモンのビルドを試してみます。

最近TVで報道されたり、暴騰したりで話題のMonacoinのmonacoindをビルドしてみます。


前回Walletをビルドする際に、必要そうなパッケージはあらかた突っ込んだので、今回は非常にシンプルです。
$ git clone git://github.com/monacoinproject/monacoin.git
$ cd monacoin/src
$ make -f makefile.unix

ビルドし終わったら、「/usr/local/bin」あたりに放り込んで実行します。
$ monacoind
Error: To use monacoind, you must set a rpcpassword in the configuration file:
It is recommended you use the following random password:
rpcuser=monacoinrpc
rpcpassword=EvCj7irBs1egMX33HixQ6Hv481neRF9Jm8qmFwRKdUJy
(you do not need to remember this password)
The username and password MUST NOT be the same.
If the file does not exist, create it with owner-readable-only file permissions.
It is also recommended to set alertnotify so you are notified of problems;
for example: alertnotify=echo %s | mail -s "Monacoin Alert" admin@foo.com

なんか怒られました。

設定ファイルでrpcpasswordを設定しろとのことです。
オーナーのみ読み込み可能なように設定するべきと書いてあります。

ということで、指定どおりファイルを作成し編集する。
$ touch monacoin.conf
$ vi monacoin.conf

#中身は言われた通りに設定してみる。
rpcuser=monacoinrpc
rpcpassword=EvCj7irBs1egMX33HixQ6Hv481neRF9Jm8qmFwRKdUJy

#同じく指示通りオーナーのみ読み込み可能に。
$ chmod 400 monacoin.conf

#実行する。
$ monacoind

初めてmonacoindを実行すると、debug.logがモリモリ増量していきます。
初回の同期処理をしてるんだと思いますが、100M超えたあたりで不安になりました。。

不安なのでdebug.logを監視してみます。
$ tail -f debug.log

height(何ブロック目まで読み込んだか)の値が少しずつ大きくなっていっているので何か動いている風です。
しばらく待って、最新のブロックまで読み込んだ後、再起動したところdebug.logは200kぐらいになりました。

実際のデータはblocksディレクトリの中だと思われます。


では、実際にAPIを叩いて動作しているか確認してみます。

monacoindをデーモンとして起動します。
$ monacoind -daemon
Monacoin server starting

いくつかAPIを呼んでみます。

  • ヘルプ

$ monacoind help
だーっとヘルプが表示されます。


  • monacoindを停止

$ monacoind stop
Monacoin server stopping

#この状態でAPIを呼ぶと当然エラー
$ monacoind help
error: couldn't connect to server


  • 再起動してから残高を表示

$ monacoind -daemon
Monacoin server starting
$ monacoind getbalance
0.00000000


  • 最新のブロック番号を表示

$ monacoind getblockcount
197067


  • 現在のdifficultyを表示

$ monacoind getdifficulty
227.03475154


  • もろもろの情報をまとめて表示

$ monacoind getinfo
{
    "version" : 80700,
    "protocolversion" : 70002,
    "walletversion" : 60000,
    "balance" : 0.00000000,
    "blocks" : 197069,
    "timeoffset" : 26,
    "connections" : 8,
    "proxy" : "",
    "difficulty" : 168.17490151,
    "testnet" : false,
    "keypoololdest" : 1405931648,
    "keypoolsize" : 101,
    "paytxfee" : 0.00000000,
    "mininput" : 0.00001000,
    "errors" : ""
}


  • 受信用アドレスの情報を表示
以下だと取引の発生していないアドレスは表示されません。

$ monacoind listreceivedbyaddress
[
]


  • すべての受信用アドレスの情報を表示
取引の発生していないアドレスも表示されます。

$ monacoind listreceivedbyaddress 0 true
[
    {
        "address" : "MENrRKiTwgXSGxczz62Jk7AFq67pLYpdWV",
        "account" : "",
        "amount" : 0.00000000,
        "confirmations" : 0,
        "txids" : [
        ]
    }
]


  • 取引発生後の受信用アドレス
別のWalletから上記アドレスに0.01MONA送金後、こんどはオプション無しで表示されました。

$ monacoind listreceivedbyaddress
[
    {
        "address" : "MENrRKiTwgXSGxczz62Jk7AFq67pLYpdWV",
        "account" : "",
        "amount" : 0.01000000,
        "confirmations" : 7,
        "txids" : [
            "f1a4f98965f45d69abbced94c94e0daa6c4b8106593b8836f2d3d8dc2244d1e6"
        ]
    }
]


  • 取引発生後のすべての受信用アドレス
取引発生後、内部的に新しいアドレスが追加された模様。

$ monacoind listreceivedbyaddress 0 true
[
    {
        "address" : "MENrRKiTwgXSGxczz62Jk7AFq67pLYpdWV",
        "account" : "",
        "amount" : 0.01000000,
        "confirmations" : 7,
        "txids" : [
            "f1a4f98965f45d69abbced94c94e0daa6c4b8106593b8836f2d3d8dc2244d1e6"
        ]
    },
    {
        "address" : "MNVb8W1dHD4jNGRtCayhiMzojGontESw8r",
        "account" : "",
        "amount" : 0.00000000,
        "confirmations" : 0,
        "txids" : [
        ]
    }
]


  • 送金テスト
0.001MONAを送金してみます。

$ monacoind sendtoaddress MR2j3u5oNWZzAAxeHtSinHUEVNiSbkn8zJ 0.001
02f4ab46964f9a55dd0554a5764725e10513b1ad402b939a96e2273d531c9773

早速Windowsに入れているWalletに0.001MONA届きました。


ちなみに激しく貧乏なのは、1MONAが7円ぐらいに跳ね上がった時に、焦って全部Bitcoinにしてしまったためです。。
まさかあそこから30円突破まで行くとは。。


ということで、なんかサービス作るのでMonacoinで寄付下さいw

MR2j3u5oNWZzAAxeHtSinHUEVNiSbkn8zJ


Read More...