カテゴリー
Android

Androidアプリ内にてPAY.JPのカードトークン作成

AndroidアプリにPAY.JPのカードトークン取得機能を組み込む必要があったのですが、PAY.JP側のライブラリではAndroidに対応していませんでした。

Java用ライブラリは用意されているのですが、下記ランタイムエラーが発生するためAndroidでは利用できません。
Caused by: java.lang.NoClassDefFoundError: javax.xml.bind.DatatypeConverter
at jp.pay.net.LivePayjpResponseGetter.getHeaders(LivePayjpResponseGetter.java:85)

既存プロジェクトではvolleyを使っていたので、直接PAY.JPのRestAPIにリクエストを投げることで対応するに至りました。

以下サンプルとなります。

final String apiKey = "pk_test_123456789012345678901234";//PAY.JPの公開APIキー
String url = "https://api.pay.jp/v1/tokens";
Map<String,String> params = new HashMap<>();
params.put("card[number]",number);
params.put("card[name]",name);
params.put("card[cvc]",cvc);
params.put("card[exp_month]",expMonth);
params.put("card[exp_year]",expYear);
JsonObjectRequestHashMapPost request = new JsonObjectRequestHashMapPost(Request.Method.POST, url, params,
        new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject jsonObject) {
                try{
                    //トークン取得して後続処理を呼ぶ
                    token = jsonObject.getString("id");
                }catch(Exception e){
                    //正常にトークンが発行されなかった
                    e.printStackTrace();
                }
            }
        },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                //エラー処理
                switch(volleyError.networkResponse.statusCode){
                    case 400:
                        //不正なパラメータなどのリクエストエラー
                        break;
                    case 401:
                        //APIキーの認証エラー
                        break;
                    case 402:
                        //カード認証・支払いエラー
                        break;
                    case 500:
                        //PAY.JPや決済ネットワークでの障害
                        break;
                    default:
                }
            }
        }
){
    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        Map<String, String> headers = super.getHeaders();
        // Add BASIC AUTH HEADER
        Map<String, String> newHeaders = new HashMap<String, String>();
        newHeaders.putAll(headers);
        // APIキーにコロンを追加
        String userString = apiKey+":";
        // APIキーをBase64でエンコード
        String user = String.format("Basic %s", Base64.encodeToString(userString.getBytes(), Base64.URL_SAFE| Base64.NO_WRAP));
        newHeaders.put("Authorization", user);
        return newHeaders;
    }
};
//Queueに追加(JSON取得処理)
RequestQueue mQueue = Volley.newRequestQueue(this);
mQueue.add(request);

ポイントはオーバーライドしているpublic Map<String, String> getHeadersで、コロン(:)を追加したapiキーをBase64でエンコードし、HttpリクエストヘッダーのAuthorizationとして追加するところ。

 

ちなみにJsonObjectRequestHashMapPostはその名の通り、HashMapをPostするためにRequestを拡張したクラスです。こちらも参考までに。

public class JsonObjectRequestHashMapPost extends Request<JSONObject> {
    private Listener<JSONObject> listener;
    private Map<String, String> params;

    public JsonObjectRequestHashMapPost(String url, Map<String, String> params,
                         Listener<JSONObject> responseListener, ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        this.listener = responseListener;
        this.params = params;
    }

    public JsonObjectRequestHashMapPost(int method, String url, Map<String, String> params,
                         Listener<JSONObject> responseListener, ErrorListener errorListener) {
        super(method, url, errorListener);
        this.listener = responseListener;
        this.params = params;
    }

    protected Map<String, String> getParams()
            throws com.android.volley.AuthFailureError {
        return params;
    };

    @Override
    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        try {
            String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
            return Response.success(new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JSONException je) {
            return Response.error(new ParseError(je));
        }
    }

    @Override
    protected void deliverResponse(JSONObject response) {
        listener.onResponse(response);
    }
}

参考

カテゴリー
未分類

ディスク容量不足の時の調査

Linuxサーバーでディスク容量が逼迫し始めたとき、どのディレクトリが容量を食ってるか調査する手段。

しょっちゅう使う事がないので、いざというときに忘れているから覚え書きとして...。

#du -h –max-depth=1 /

とすると、ルートディレクトリ配下のディレクトリ毎にどれだけ容量を食ってるかが表示される。

#du -h –max-depth=1 /var

とかで下層を掘っていくことが可能。

カテゴリー
未分類

モッツァレラーチーズを作ろう!

モッツァレラチーズ作りに挑戦です!

【用意したもの】

0102eb197903fb56fe28f1c63aaf549920b510bb7f

・低温殺菌牛乳・・・2L
・レンネット粉末・・・耳かき1杯程度の粉末を10ccの水に溶かしておく
・クエン酸粉末・・・小さじ1を20ccの水に溶かしておく

1.牛乳を温めてクエン酸水をまぜる

牛乳を鍋で12℃まで最弱火で加熱したあと、クエン酸水を入れてゆっくりと混ぜ5分ほど待ちます。

2.レンネットを投入

牛乳に少しモロモロっとした塊ができ始めるので、最弱火で7分くらいかけて32℃まで温めます。
牛乳があったまったらレンネット水をいれて混ぜ、また5分程度待ちます。

この時に徐々に牛乳が固まり始めます。

3.牛乳を加熱してカード(かたまり)とホエー(水分)に分離させる

牛乳を45℃まで最弱火で加熱します。この時、事前に固まった牛乳(カード)をナイフやスケッパーでサイの目状に切っておきます。

...って、あれ?あんまり固まってないぞ?

4.カード(牛乳の塊)を水切り

01876358a96daf724841db0a11db8c6a95d4260ef0

カードを鍋からお玉などですくい、キッチンペーパーなどで水切りします

5.カードをこねる

鍋に残ったホエーを85℃まで加熱します。
加熱したホエーに水切りしたカードをお玉ですくって入れ、耐熱手袋を手にはめてアツアツのカードをこねます...こねます...

なんだこれ、ボソボソすぎてこねるどころじゃねぇ...砂場の砂握ってるみたいで、指の間から全部こぼれていくし...。
数回こねたら伸びとツヤが出てくるらしいが...いつまでたってもコナゴナのカードが指の間から出ていくばかり!

6.塩水につける

こねてまとまったら、濃い塩水に1時間程度つけて完成!

6.カッテージチーズの完成!

できたー!おいしいカッテージチーズですよ!

01c8a07dc4a0d836499209b4f1e820cff4335b3ea3

え、モッツァレラチーズ?なにそれ?
ウワーン、また再チャレンジしてやるー!

【敗因?】

おそらく、クエン酸の量が多かったのではないかと思います。

2番の手順であまり固まっていなかったため、参考レシピの指示に従いクエン酸を追加したのですが、その時の分量が多かったため急速に水分が分離してしまい、レンネットによる凝固がうまくいかなかったような気がしています。

もしくはレンネットの量が少なかったか...。

何事も、やってみないと分からないものですね。

次こそは、おいしいモッツアレラチーズができたらいいなぁ!

カテゴリー
未分類

AmazonSNSでKindleFireにPushさせる(ADM)とき実機デバッグで E/ADM: ADM Error INVALID_SENDER – Unable to parse API Key for package [package_name]. Did you forget to embed it? とか言われる場合

Kindle Fire端末向けにPushを組み込もうとしてADMを実装していましたが、Amazonのマニュアルに従いassetsフォルダにapi_key.txtを作り、いざ実機で動作確認しようとしたところ、

E/ADM: ADM Error INVALID_SENDER – Unable to parse API Key for package [package_name]. Did you forget to embed it?
(お前、api_keyファイル埋め込んでなくね?)

って言われて、登録IDすらろくに取得できずに数時間無駄にしました。

ちなみに環境はWindows10上でAndroidStudio2.xを使っての開発です。

以下、原因と修正内容です。

原因:SecurityProfileのSignatureに入力するMD5が、本番リリース用のKeyStoreFileから取得したMD5だった

そもそもですね、恥ずかしながらKeyStoreファイルなんて自分が用意したものしか知らなかったので、それのMD5が必要なんだとばかり思っていたら、USBデバッグとかでは自動的に用意されたKeyStoreファイルが使われてるんですね。知りませんでした。

修正:debug.keystoreからMD5を取得する

ちなみにうちの環境では「C:\Users\[ユーザー]\.android\debug.keystore」に存在していました。これをkeytoolにかけてMD5を取得し、SecurityProfileに登録してAPI keyを取得するとうまくいきました!

keytoolで指定するaliasは「androiddebugkey」。なのでコマンドラインはこんな感じです。

keytool -list -v -alias androiddebugkey -keystore C:\Users\[ユーザー]\.android\debug.keystore

カテゴリー
未分類

vagrant package で仮想環境を移行するとネットワークアダプタが見つからない

表題の通り、Vagrantの仮想環境を別PCに移行するため、移行元にてvagrant package してできたboxファイルを、移行先PCでvagrant add したあと仮想環境を起動すると、見事にeth1、eth2あたりがなくなっていて、ホストPCから仮想サーバーにアクセスできない状態でした。(vagrant sshは使えた)

移行元と移行先で、ネットワークアダプタに割り当てられるMacアドレスが変わるために仮想環境側ではネットワークアダプタが見つからなくなるようで、以下の通り仮想環境側のファイルを削除した後、vagrant hult -> vagrant up で起動するとネットワークアダプタが復活しました。

rm /etc/udev/rules.d/70-persistent-net.rules

rm /etc/sysconfig/network-scripting/ifcfg-eth1

 

こちらを参考にさせていただきました。

http://d.hatena.ne.jp/nagachika/20140121/vagrant_box_with_centos64

というか、そのままですね…。

カテゴリー
未分類

elasticsearchを試す

全文検索したいよね。
でも、100万件とかのデータに対してRDBで「like ‘%xxxx%’」とかしたくないよね。

ということで、全文検索エンジンの登場です。

全文検索エンジンは古くからいろいろありますが、elasticsearchの特徴はその名の通りelasticにスケールアウトが簡単なとこでしょうか。

他の全文検索エンジンで有名なところだとSolrやGroongaなんかもありますが、スケールアウトの面ではelasticsearchが秀でているようです。(僕自身は他のを試したことが無いので詳細は不明ですが…)

1.まずはインストール

CentOS6.3で試しています。

elasticsearchはJavaで出来ているので、jdをインストールします。

yum install java-1.7.0-openjdk-devel

 

次にelasticsearchのリポジトリを追加します。

vi /etc/yum.repos.d/elasticsearch.repo

 

で、以下の内容をコピペ。(elasticsearch1.4.xを対象としています)

[elasticsearch-1.4]
name=Elasticsearch repository for 1.4.x packages
baseurl=http://packages.elasticsearch.org/elasticsearch/1.4/centos
gpgcheck=1
gpgkey=http://packages.elasticsearch.org/GPG-KEY-elasticsearch
enabled=1

 

んでもって、yumでインストール。

yum install elasticsearch

 

1つのノードだけでよければ、elasticsearchのインストールは以上で完了です。

今回は複数ノードを用意して、ドキュメントが各ノードに分散されることを確認したかったので、同じ作業を3台のサーバーに対して行いました。
(実際は勉強用の環境なので、Vagrant+Chefでサクッと3台立ち上げてprovisionしただけですが…)

で、各サーバーは以下のようになりました。

・IP:192.168.33.21(Master & Data ノード)
・IP:192.168.33.22(Data ノード)
・IP:192.168.33.23(Data ノード)

IP192.168.33.21はマスターとして各ノードを管理し、また自ノードでもデータを保持します。
それ以外はマスターに管理される状態で、データだけを保持するノードとする予定ですが、マスターやデータノードの設定は次になります。

今後の作業のために、headプラグインとkuromojiプラグインをインストールしておきます。

headプラグインはクラスターやノード、インデックスの状態を確認するためのプラグイン。なので、マスターにする192.168.33.21にのみインストールしました。

/usr/share/elasticsearch/bin/plugin -install mobz/elasticsearch-head

 

kuromojiは日本語の形態素解析用プラグインです。
今回の実験では使いませんが、日本語の全文検索ではほぼ必須となるでしょう。

/usr/share/elasticsearch/bin/plugin -install elasticsearch/elasticsearch-analysis-kuromoji/2.4.0

 

2.設定

CentOSではインストール直後は以下のディレクトリ構成となりました。

/etc/elasticsearch/ ・・・設定ファイルなど
/usr/share/elasticsearch/ ・・・実行ファイルやプラグインなど

設定ファイルは/etc/elasticsearch/elasticsearch.ymlなので、各ノードのファイルを編集していきます。

・IP:192.168.33.21での設定

ノードの名前を”apple”としてみました。また、shardは全部で3、今回はレプリケーションを行わない設定とします。そして連携するサーバー群(elasticsearchの用語ではclusterと言う)にも名前を付けて”tac”としておきます。

clusterの属するサーバーをお互いが見つける仕組みはdiscoveryで指定します。今回はunicastによるdiscoveryを指定しました。

cluster.name: tac
node.name: "apple"
index.number_of_shards: 3
index.number_of_replicas: 0
network.host: 192.168.33.21
discovery.zen.ping.multicast.enabled: false #コメントアウトを外す
discovery.zen.ping.unicast.hosts: ["192.168.33.21"] #コメントアウトを外してマスターノードのIPを指定

 

・IP:192.168.33.22での設定

ノードの名前を”banana”とし、データノードとして設定します。

cluster.name: tac
node.name: "banana"
node.master: false
node.data: true
network.host: 192.168.33.22
discovery.zen.ping.multicast.enabled: false #コメントアウトを外す
discovery.zen.ping.unicast.hosts: ["192.168.33.21"] #コメントアウトを外してマスターノードのIPを指定

 

・IP:192.168.33.23での設定

こちらはノードの名前を”cinamon”とし、bananaノード同様データノードとして設定します。

node.name: "cinamon"
node.master: false
node.data: true
network.host: 192.168.33.23
discovery.zen.ping.multicast.enabled: false #コメントアウトを外す
discovery.zen.ping.unicast.hosts: ["192.168.33.21"] #コメントアウトを外してマスターノードのIPを指定

 

ここまで設定できたら、各サーバーのelasticsearchを再起動します。

service elasticsearch restart

 

appleノードのサーバで以下のコマンドを実行し、設定が正しく行われているかを確認します。

curl 192.168.33.21:9200/_cluster/health?pretty

 

結果は以下のようになりました。

{
"cluster_name" : "tac",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 3,
"number_of_data_nodes" : 3,
"active_primary_shards" : 3,
"active_shards" : 3,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0
}

 

elasticsearchはデータの作成や更新、削除、サーバーへの各種実行など、すべてRestFullなリクエストによって行い、データのやり取りはJSONが用いられます。

上記結果ではstatusがgreenとなっていればOKです。yellowとなっている場合は、shardやreplicaの数の設定が間違っている可能性があります。number_of_nodes、number_of_data_nodes、active_primary_shardsとactive_shardsが3となっており、正しく設定が反映されていますね。

今回の実験では、設定は以上となります。

3.スキーマの登録とデータの作成

elasticsearchはRDBと異なり、スキーマレスなデータを扱うことができます。今回は各ノードに対して自動的にドキュメントが分散配置されることを確認したいので、データ構造を指定せずにデータを入れる箱だけ用意します。

以下のコマンドを実行すると、shopという名前のインデックス(RDBではテーブルに相当する)が作成されます。

curl -XPOST '192.168.33.21:9200/shop'

 

では、実際に適当なデータをshopインデックスに登録してみます。データの登録は以下のコマンドを実行します。
※今回はデータ登録に関するコマンドの説明は割愛します。

curl -XPUT http://192.168.33.21:9200/shop/main/1 -d '{"title" : "This is the first document."}'
curl -XPUT http://192.168.33.21:9200/shop/main/2 -d '{"title" : "This is the second document."}'
curl -XPUT http://192.168.33.21:9200/shop/main/3 -d '{"title" : "This is the third document."}'

 

登録したデータを確認するには、次のコマンドを実施します。

curl -XGET http://192.168.33.21:9200/shop/main/1

 

すると以下のJSONが返されます。

{"_index":"shop","_type":"main","_id":"1","_version":1,"found":true,"_source":{"title":"This is the first document."}}

 

各ノードに対して、登録したドキュメントが分散配置されているでしょうか?headプラグインを用いて確認してみましょう。

Webブラウザを立ち上げて、http://192.168.33.21:9200/_plugin/head/にアクセスすると、現在のcluster情報が表示されます。

head_plugin

apple、banana、cinamonノード(サーバー)が表示され、緑の四角がshardを表しています。

設定でnumber_of_shardsを3にしたので、合計3つのシャードが自動的に各ノードに割り振られました。

shardを表す四角をクリックして各shardの詳細を表示したのが上の画像です。

各shardのnum_docsが1になっており、登録した3件の文書が各shardに1件ずつ割り振られた状態となっています。

データの登録も読み込みも、マスターノードに対してリクエストを投げれば、あとは勝手にうまい具合に処理されることが確認できました。

以上、elasticsearchのインストールから、複数ノードを使ったclusterの構築でした。

カテゴリー
Linux

サーバーダウンの原因調査

WordPressを稼働させているサーバが良く落ちる。

データのバックアップにbackWPupのプラグインを使っているのだが、これが動く前後でMySQLやApacheが死ぬ。

原因調査と対応について、今後のためにもここにメモ代わりに書き残しておく。

  1. messageログを確認
    egrep -i ‘fatal|error|memory|resource’ /var/log/messages
    Oom Killが発生していると一発で判明する。
    うちでは突然OomKillが発生して手あたり次第サービスが終了されてしまい、WordPressの機能不全に陥っておりました。
  2. メモリを食ってる問題のプロセスを特定...したいけど、どうしたらいいんだろう...。いずれかのhttpdがメモリを食いつぶし始めるんじゃないかと思うのだが、サーバーに問題が発生した時点で、慌ててApache、MySQLを再起動させたので、現状この先どう調べればよいのかわからない。
    各サービスを再起動させる前に、psかtopでも発行してればもう少しヒントを得られたのかな?
    ただ、動いてるプロセスからして、ApacheかMySQLのどちらかしか考えにくいし、おそらくはApacheなんだろうなと思うのだが...、何に対するリクエストで、そんなメモリをバカ食いするんだろうか。
    OomKill発生前のアクセスログから地道に調査するしかないのかなぁ...めんどくせぇ。

以後、調査が進めば随時追記していきます。

 

カテゴリー
SQL

MySQLでDumpをリストアするときにメモリ不足のエラーが出たとき

Dumpファイルをリストアしようとすると、メモリ不足のエラーでリストアできなかった。

リストア先のmax_allowed_packetとnet_buffer_lengthが足りていなかった模様。

 

リストア先のサーバパラメータを変更しても良いが、Dump取得時(Dumpファイル作成時)に以下のようなパラメータを設定すれば、Dump内容をリストア先サーバーのメモリに合わせておくことができるのでリストア時のメモリ不足を回避できる。

mysqldump -u [MYSQL_USER] –opt –pass=[LOGIN_PASS] -h[SRC_HOST] –net_buffer_length=16k –max_allowed_packet=134217728 [SRC_DB] [SRC_TABLE] > [./TOFILE.sql(destination)]

カテゴリー
未分類

vagrant up できない!

開発マシンを新しくし、今までのようにVirtualBoxとVagrant、chefをインストールしてvagrant upしたら…

[default] Waiting for VM to boot. This can take a few minutes.

と表示されたまま一向に進まず、そのままssh接続でタイムアウトしてしまいchefが全く実行されない現象にあたりました。

いろいろネットを見て回るとVirtualBoxのGuiモードで起動するとヒントがあるかも、ということでVagantfileを見ると次のようなオプションがコメントアウト状態になってるので、コメントを外してGuiモードを有効にします。

vb.gui = true

この状態でvagrant upするとVirtualBoxのGui画面が立ち上がり、Linuxの起動画面が表示されます。

で、しばらく様子を見ているとOSが立ち上がった後にLogin画面になるのですが、突如VirtualBoxのエラーダイアログが表示されました。

仮想化支援機構(VT-x/AMD-V)を有効化できません。64ビットゲストOSは64ビットCPUを検出できず、起動できません。
ホストマシンのBIOS設定でVT-x/AMD-V を有効化してください。

「ハァ?僕、小学校でそんなの習ってません!」

どうやらBIOSから「Intel(R) virtualization technology」をenabledにしないとダメならしく、BIOSの設定を変更するとすんなり起動してchefが動きました。

以前のマシンではこんな設定変更したことなかったので、vagrant upができずに焦りまくり。

ブログに書いた内容は大したことないのですが、原因究明するまですごい時間を無駄にしてしまいました…。

カテゴリー
Scala

Scala 学習日誌(1)

コンパイラによる型推論、アクターによる並列処理、パーサコンビネータ、Javaライブラリを直接取り扱える等、魅力的な言語Scalaを勉強しているのだが、忘れっぽい自分用の学習メモを付けることにした。

個人的な感想としては慣れないうちは、Scalaのコードは非常にわかりにくい。

なぜなら関数定義だけで数多くの省略形が用意されている上、カリー化された関数なんかは、一見Scala組み込みのAPIのようにも見えたりする。

一般的な関数の定義は次のようになる。

def someFunction (arg1:Int, arg2:String) : String = {
  //なんか巻数の処理
}

defに続くのが関数名、その後ろのカッコには引数とその型をコロンで区切って宣言、複数の引数がある場合はカンマで区切る。
カッコの後ろはこの関数の戻り値の型を宣言していて、その後ろのイコールはこの関数に戻り値があることを明示する。
続く波カッコ内に関数の内容を書く…まぁこの辺はアリがち。

関数の内容からコンパイラが戻り値の型を推測できる場合は関数の型宣言を省略できる。

def someFunction (arg1:Int, arg2:String) = {
  if(arg1 < 0) arg2 else "bigger than 0!"
}

 

関数の内容が1文だけの場合は波カッコを省略できる。
そろそろ関数の定義じゃなくて、ただの代入式っぽくなってきたぞ…。

def someFunction (arg1:Int, arg2:String) = if(arg1 > 0) arg2 else "bigger than 0!"

 

また、Class内のgetterメソッドとして、単純に値を返すだけの場合、

def getProperty = classval

とか書ける。
うん、簡潔。でも、もうパッと見は関数って感じじゃないよね。

クラスのメソッドを呼び出す場合なんかも省略形式が用意されている。

例えばmyTestクラスのfirst関数を呼ぶ場合、全部書くと次の通り。

myTest.first(1)

 

でもインスタンス化されたクラスの関数を呼ぶ場合は、途中のピリオドの代わりにスペースでもいいらしい…

myTest first(1)

 

さらに、引数が一つならカッコも省略できて…

myTest first 1

とか。カッコの省略はタイプ数を減らすって意味ではいいと思うけど、ピリオドの代わりにスペースでOKとか何が嬉しいのかよくわかりません…。

ちなみに戻り値はJavaのようにreturn xx としなくても、関数内で最後に実行された式の結果が自動的に戻り値となります。もちろん明示的にreturnを付ける事もできるのですが…そうしないのがScala流なのかな?

おかげで複雑なifを組み上げると、何が戻るのかさっぱり分からない関数が出来上がります。
(その時点でバッドコーディングなんだろうな…)

さらに、高階関数に渡すための「関数リテラル」

//高階関数からxを引数として受取り、奇数はfalse、偶数はtrueを返す関数リテラルで、「x % 2 == 0」が関数の内容になります。

(x:Int) => x % 2 == 0

//また、xの型をIntと推論できる場合は型宣言を省略でき...

(x) => x % 2 == 0

//さらに引数のカッコも省略可能

x => x % 2 == 0

//でもって、関数リテラルはこんな風に高階関数に渡して使います。

List(1,2,3,4,5,6,7,8,9,10).filter(x=>x%2==0)

//結果は下記のような2、4、6、8、10だけのListが出来上がります。
//res0: List[Int] = List(2, 4, 6, 8, 10)

 

省略形って使いこなせれば非常に便利なのですが、やりすぎるとなんのこっちゃわからなくなるんですよねー。

だからperlがキライだったりするものでして、ハイ。

 

さらにこれから、引数の部分適用やカリー化の概念が出てくるので、なかなか一筋縄ではいきませんねー。