@blog.justoneplanet.info

日々勉強

node.jsでbasic認証を使ってhttps接続する

nodeのバグを踏んだりしてたら4時間かかってしまったのでメモしておく。

■HTTP接続

以下のようにしてHTTPクライアントを生成し、ホストに接続することができる。

var host   = 'www.example.com';
var header = {"Host": host};
var client = http.createClient(
    80,
    host,
    false,
    false
).
client.request(
    "GET",
    "/",
    header
);

createClienetはどうやら廃止される方向らしいので以下のように書きなおす。

var http = require('http');
var host = 'www.example.com';
var req  = http.get(
    {
        "host"  : host,
        "port"  : 80,
        "path"  : "/"
    },
    function(res) {
        console.log("Got response: " + res.statusCode);
    }
);
req.on('error', function(e){
    console.log(e);
})

■Basic認証+HTTP接続

以下のようにしてHTTPクライアントを生成し、ホストに接続することができる。

var username = 'hoge';
var password = 'fuga';
var auth     = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
var host     = 'www.example.com';
var header   = {
    "Host"          : host,
    "Authorization" : auth
};
var client   = http.createClient(
    80,
    host,
    false,
    false
).
client.request(
    "GET",
    "/",
    header
);

createClienetはどうやら廃止される方向らしいので以下のように書きなおす。

var http = require('http');
var host = 'www.example.com';
var req  = http.get(
    {
        "host"  : host,
        "port"  : 80,
        "path"  : "/",
        "auth"  : "username:password"
    },
    function(res) {
        console.log("Got response: " + res.statusCode);
    }
);
req.on('error', function(e){
    console.log(e);
})

■Basic認証+HTTPS接続

失敗例

以下のようにしてHTTPSクライアントを生成しても、ホストに接続することはできない。

var username = 'hoge';
var password = 'fuga';
var auth     = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
var host     = 'www.example.com';
var header   = {
    "Host"          : host,
    "Authorization" : auth
};
var client   = http.createClient(
    443,
    host,
    true,
    false
).
client.request(
    "GET",
    "/",
    header
);

実行してもエラーがでる。

参考

上述のように失敗するのでhttpsモジュールを使う。

■Basic認証+HTTPS接続

以下のようにすることで、HTTPSクライアントでBasic認証を使うことができる。

var https    = require('https');
var host     = 'www.example.com';
var username = 'hoge';
var password = 'fuga';
var auth     = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
var options  = {
    "host"          : host,
    "port"          : 443,
    "path"          : '/',
    "method"        : 'GET',
    "Authorization" : auth
};
var request = https.request(
    options,
    function(response) {
        console.log("statusCode: ", response.statusCode);
        console.log("headers: ", response.headers);
    }
);
request["_headers"]["Authorization"]     = auth;// ...(a)
request["_headerNames"]["Authorization"] = 'Authorization';// ...(b)
request.end();

optionsにAuthorizationをセットするだけではBasic認証が成功しなかった。認証用のヘッダが正しく付加されるように、強引に(a)と(b)を加えた。

参考

■後述

現在のnodeのバージョンでは以下のようにして簡単にhttpsでBasic認証を行うことができた。

var http = require('https');
var host = 'www.example.com';
var req  = http.get(
    {
        "host"  : host,
        "port"  : 443,
        "path"  : "/",
        "auth"  : "username:password"
    },
    function(res) {
        console.log("Got response: " + res.statusCode);
    }
);
req.on('error', function(e){
    console.log(e);
})

参考

iPhone版みんなの顔文字辞典をリリースしました

ついに出しました。3プラットフォーム対応ですψ(´ڡ`❤)

■ダウンロード

こちらから無料でダウンロードできます。

■機能

マイ顔文字

いわゆるお気に入りです。登録してシェアすることができます。

みんなの顔文字

みんなが登録した顔文字を見ることができます。好きなモノをロングタップしてマイ顔文字に保存できます。

人気の顔文字

みんなが使ってる顔文字の人気ランキングです。知らない顔文字があったらロングタップして保存しましょう。

カテゴリ

カテゴリです。マイ顔文字の分類に使ってください。

■Chrome版

こちらをご覧ください。

■android版

こちらをご覧ください。

Objective-CでURLエンコードする

■実装

Util.h

#import <Foundation/Foundation.h>
@interface Util : NSObject {
}
+ (NSString *)urlencode:(NSString *)text;
+ (NSString *)urldecode:(NSString *)text;
@end

Util.m

#import "Util.h"
@implementation Util
//encode
+ (NSString *)urlencode:(NSString *)text
{
    CFStringRef strRef = CFURLCreateStringByAddingPercentEscapes(
        NULL,
        (CFStringRef)text,
        NULL,
        (CFStringRef)@"!*'();:@&=+$,/?%#[]~",
        kCFStringEncodingUTF8);
    NSString * str = [NSString stringWithString:(NSString *)strRef];
    CFRelease(strRef);
    return str;
}

// decode
+ (NSString *)urldecode:(NSString *)text
{
    CFStringRef strRef = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(
        NULL,
        (CFStringRef) text,
        CFSTR(""),
        kCFStringEncodingUTF8);
    NSString * str = [NSString stringWithString:(NSString *)strRef];
    CFRelease(strRef);
    return str;
}
@end

■テスト

ちゃんと書いておく。

UtilTest.h

#import <GHUnitIOS/GHUnit.h>
@interface UtilTest : GHTestCase {    
}
@end

UtilTest.m

#import "UtilTest.h"
#import "Util.h"
@implementation UtilTest
- (void)test_urlencode
{
    NSString *url1 = [NSString stringWithString:@"%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9"];
    NSString *url2 = [NSString stringWithString:[Util urlencode:@"テストケース"]];
    GHAssertEqualObjects(url1, url2, @"match!");
    
    NSString *url3 = [NSString stringWithString:@"%E9%9A%A3%E4%BA%BA%E3%81%AF%E7%BE%8E%E4%BA%BA"];
    NSString *url4 = [NSString stringWithString:[Util urlencode:@"隣人は美人"]];
    GHAssertEqualObjects(url3, url4, @"match!");
    
    // 記号などはしっかりテストしましょう
    NSString *url5 = [NSString stringWithString:@"%21%22%23%24%25%26%27%28%290%3D%7E%7C%60%7B%7D%2A%2B%3C%3E%3F_"];
    NSString *url6 = [NSString stringWithString:[Util urlencode:@"!\"#$%&'()0=~|`{}*+<>?_"]];
    GHAssertEqualObjects(url5, url6, @"match!");
}

- (void)test_urldecode
{
    NSString *url1 = [NSString stringWithString:[Util urldecode:@"%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9"]];
    NSString *url2 = [NSString stringWithString:@"テストケース"];
    GHAssertEqualObjects(url1, url2, @"match!");
    
    NSString *url3 = [NSString stringWithString:[Util urldecode:@"%E9%9A%A3%E4%BA%BA%E3%81%AF%E7%BE%8E%E4%BA%BA"]];
    NSString *url4 = [NSString stringWithString:@"隣人は美人"];
    GHAssertEqualObjects(url3, url4, @"match!");
    
    // 記号などはしっかりテストしましょう
    NSString *url5 = [NSString stringWithString:[Util urldecode:@"%21%22%23%24%25%26%27%28%290%3D%7E%7C%60%7B%7D%2A%2B%3C%3E%3F_"]];
    NSString *url6 = [NSString stringWithString:@"!\"#$%&'()0=~|`{}*+<>?_"];
    GHAssertEqualObjects(url5, url6, @"match!");
}
@end

つーかNSURLとかを拡張したほうが良かったかな。

OpenGL ESで正六面体を描画する

package info.justoneplanet.android.glsample;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import android.os.Bundle;

public class MainActivity extends Activity {
    private GLSurfaceView gLSurfaceView;
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        gLSurfaceView = new GLSurfaceView(this);
        gLSurfaceView.setRenderer(new Renderer());
        setContentView(gLSurfaceView);
    }
    
    @Override
    public void onPause()
    {
        super.onPause();
        gLSurfaceView.onPause();
    }
    
    @Override
    public void onResume()
    {
        super.onResume();
        gLSurfaceView.onResume();
    }
    
    private class Renderer implements GLSurfaceView.Renderer
    {
        private float aspect = 0.0f;
        private int vertices = 0;
        private int indices  = 0;
        private int indicesLength;

        /**
         * onDrawFrame
         * 毎フレーム描画時に実行される
         */
        @Override
        public void onDrawFrame(GL10 gl) {
            gl.glClearColor(0.0f, 0.5f, 0.5f, 1.0f);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
            
            {// カメラの転送
                gl.glMatrixMode(GL10.GL_PROJECTION);
                gl.glLoadIdentity();// 行列の初期化
                GLU.gluPerspective(gl, 45.0f, aspect, 0.01f, 100.0f);// 描画する空間を指定
                GLU.gluLookAt(gl, 0, 5.0f, 5.0f, 0, 0, 0.0f, 0.0f, 1.0f, 0.0f);// 視点の方向を指定
            }
            
            {
                gl.glMatrixMode(GL10.GL_MODELVIEW);
                gl.glRotatef(0.2f, 0, 0.2f, 0);// 物体の回転
            }
            
            // 頂点バッファを関連付けて
            // 物体を描画する
            GL11 gl11 = (GL11) gl;
            gl11.glDrawElements(GL10.GL_TRIANGLES, indicesLength, GL10.GL_UNSIGNED_BYTE, 0);
         }

        /**
         * onSurfaceChanged
         * surfaceのサイズが変更されたりすると実行される
         */
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            aspect = (float) width / (float) height;
            gl.glViewport(0, 0, width, height);
            
            GL11 gl11 = (GL11) gl;
            {
                int[] buffer = new int[2];
                gl11.glGenBuffers(2, buffer, 0);
                
                vertices = buffer[0];
                indices  = buffer[1];
            }
            
            {// 頂点バッファの生成と転送
                final float one = 1.0f;
                
                final float[] vertices = new float[]{
                         one,   one,   one,
                         one,   one,  -one,
                        -one,   one,   one,
                        -one,   one,  -one,
                         one,  -one,   one,
                         one,  -one,  -one,
                        -one,  -one,   one,
                        -one,  -one,  -one,
                };
                
                FloatBuffer fb = ByteBuffer.allocateDirect(vertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
                
                fb.put(vertices);
                fb.position(0);
                
                gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, this.vertices);
                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, fb.capacity() * 4, fb, GL11.GL_STATIC_DRAW);
                
                gl11.glVertexPointer(3, GL10.GL_FLOAT, 0, 0);
            }
            
            {// インデックスバッファの生成と転送
                final byte[] indices = new byte[]{
                        0, 1, 2,
                        2, 1, 3,
                        2, 3, 6,
                        6, 3, 7,
                        6, 7, 4,
                        4, 7, 5,
                        4, 5, 0,
                        0, 5, 1,
                        1, 5, 3,
                        3, 5, 7,
                        0, 2, 4,
                        4, 2, 6,
                };
                ByteBuffer bb = ByteBuffer.allocateDirect(indices.length).order(ByteOrder.nativeOrder());
                bb.put(indices);
                indicesLength = bb.capacity();
                bb.position(0);
                
                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, this.indices);
                gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, bb.capacity(), bb, GL11.GL_STATIC_DRAW);
            }
       }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        }
    }
}

GPU側にあるVertex Buffer Objectに転送することによって、毎回OpenGLのAPIを呼び出して頂点の転送をする必要がなくなるので高速に動作する。モデルの形状が変化する場合などには向かない。

■正六面体を外部ファイル化する

Cube.java

package info.justoneplanet.android.glsample.object;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

public class Cube
{
    public float posX    = 0.0f;
    public float posY    = 0.0f;
    public float posZ    = 0.0f;
    public float rotateY = 0.0f;
    public float scale   = 1.0f;
    
    public float colR = 1.0f;
    public float colG = 1.0f;
    public float colB = 1.0f;
    public float colA = 1.0f;
    
    private int mIndicesLength;
    
    public Cube(int indicesLength)
    {
        mIndicesLength = indicesLength;
    }
    
    public void drawBox(GL10 gl)
    {
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        
        gl.glTranslatef(posX, posY, posZ);
        gl.glRotatef(rotateY, 0, 1, 0);
        gl.glScalef(scale, scale, scale);
        
        gl.glColor4f(colR, colG, colB, colA);
        
        GL11 gl11 = (GL11) gl;
        gl11.glDrawElements(GL10.GL_TRIANGLES, mIndicesLength, GL10.GL_UNSIGNED_BYTE, 0);
    }
}

MainActivity.java

package info.justoneplanet.android.glsample;

import info.justoneplanet.android.glsample.object.Cube;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import android.os.Bundle;

public class MainActivity extends Activity {
    private GLSurfaceView gLSurfaceView;
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        gLSurfaceView = new GLSurfaceView(this);
        gLSurfaceView.setRenderer(new Renderer());
        setContentView(gLSurfaceView);
    }
    
    @Override
    public void onPause()
    {
        super.onPause();
        gLSurfaceView.onPause();
    }
    
    @Override
    public void onResume()
    {
        super.onResume();
        gLSurfaceView.onResume();
    }
    
    private class Renderer implements GLSurfaceView.Renderer
    {
        private float aspect = 0.0f;
        private int vertices = 0;
        private int indices  = 0;
        private int indicesLength;

        /**
         * onDrawFrame
         * 毎フレーム描画時に実行される
         */
        @Override
        public void onDrawFrame(GL10 gl) {
            gl.glClearColor(0.0f, 0.5f, 0.5f, 1.0f);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
            
            {// カメラの転送
                gl.glMatrixMode(GL10.GL_PROJECTION);
                gl.glLoadIdentity();// 行列の初期化
                GLU.gluPerspective(gl, 45.0f, aspect, 0.01f, 100.0f);// 描画する空間を指定
                GLU.gluLookAt(gl, 0, 5.0f, 5.0f, 0, 0, 0.0f, 0.0f, 1.0f, 0.0f);// 視点の方向を指定
            }
            
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            
            {
                cube0.posX = -0.5f;
                cube0.rotateY += 1.0f;
                cube0.colR = 1.0f;
                cube0.colG = 0.0f;
                cube0.colB = 1.0f;
                cube0.drawBox(gl);
            }
            {
                cube1.posX = 0.5f;
                cube1.rotateY -= 0.5f;
                cube1.colR = 0.0f;
                cube1.colG = 0.0f;
                cube1.colB = 1.0f;
                cube1.drawBox(gl);
            }
         }

        /**
         * onSurfaceChanged
         * surfaceのサイズが変更されたりすると実行される
         */
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            aspect = (float) width / (float) height;
            gl.glViewport(0, 0, width, height);
            
            GL11 gl11 = (GL11) gl;
            {
                int[] buffer = new int[2];
                gl11.glGenBuffers(2, buffer, 0);
                
                vertices = buffer[0];
                indices  = buffer[1];
            }
            
            {// 頂点バッファの生成と転送
                final float one = 1.0f;
                
                final float[] vertices = new float[]{
                         one,   one,   one,
                         one,   one,  -one,
                        -one,   one,   one,
                        -one,   one,  -one,
                         one,  -one,   one,
                         one,  -one,  -one,
                        -one,  -one,   one,
                        -one,  -one,  -one,
                };
                
                FloatBuffer fb = ByteBuffer.allocateDirect(vertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
                
                fb.put(vertices);
                fb.position(0);
                
                gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, this.vertices);
                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, fb.capacity() * 4, fb, GL11.GL_STATIC_DRAW);
                
                gl11.glVertexPointer(3, GL10.GL_FLOAT, 0, 0);
            }
            
            {// インデックスバッファの生成と転送
                final byte[] indices = new byte[]{
                        0, 1, 2,
                        2, 1, 3,
                        2, 3, 6,
                        6, 3, 7,
                        6, 7, 4,
                        4, 7, 5,
                        4, 5, 0,
                        0, 5, 1,
                        1, 5, 3,
                        3, 5, 7,
                        0, 2, 4,
                        4, 2, 6,
                };
                ByteBuffer bb = ByteBuffer.allocateDirect(indices.length).order(ByteOrder.nativeOrder());
                bb.put(indices);
                indicesLength = bb.capacity();
                bb.position(0);
                
                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, this.indices);
                gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, bb.capacity(), bb, GL11.GL_STATIC_DRAW);
            }
       }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        }
    }
}

■前後関係を修正する

前述のコードでは重なっている部分の表示が適切ではない。従って以下のようにする。

MainActivity.java

public class MainActivity extends Activity {
    private class Renderer implements GLSurfaceView.Renderer
    {
        @Override
        public void onDrawFrame(GL10 gl) {
            gl.glClearColor(0.0f, 0.5f, 0.5f, 1.0f);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);// GL_DEPTH_BUFFER_BITも追加

            // 以下省略
         }

        /**
         * onSurfaceChanged
         * surfaceのサイズが変更されたりすると実行される
         */
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            aspect = (float) width / (float) height;
            gl.glViewport(0, 0, width, height);
            gl.glEnable(GL10.GL_DEPTH_TEST);// 奥行き情報を前後関係に利用する
            
            // 以下省略
       }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        }
    }
}

前後関係に奥行き情報が利用されるようになったが重なった部分の表示がギザギザしたりして美しくない。これはZファイティングと呼ばれる現象であり、ハードウェアにより不自然さが異なる。このようなことがないように位置をずらす必要がある。

ASIHTTPRequestを使う

■準備

Reachability

通信処理を行う場合は必ず通信ができる状態か確認する必要がある。

ダウンロードしてReachability.hとReachability.mをプロジェクトに取り込む。

ライブラリ
  • SystemConfiguration.framework

ASIHTTPRequest

ライブラリ
  • CFNetwork.framework
  • MobileCoreService.framework
  • MobileCoreService.framework
  • libxml2.dylib

「Build Settings > Search Paths > Header Search Paths」に「$SDKROOT/usr/include/libxml2」を追記する。

CakePHP2.0でPHPUnitを使う

めも。

■PHPUnitのインストール

以下のコマンドでXAMPP for MacでPHPUnitが使えるようになる。

sudo /Applications/XAMPP/xamppfiles/bin/pear channel-update pear.php.net
sudo /Applications/XAMPP/xamppfiles/bin/pear upgrade pear
sudo /Applications/XAMPP/xamppfiles/bin/pear channel-discover pear.phpunit.de
sudo /Applications/XAMPP/xamppfiles/bin/pear channel-discover pear.symfony-project.com
sudo /Applications/XAMPP/xamppfiles/bin/pear channel-discover components.ez.no
sudo /Applications/XAMPP/xamppfiles/bin/pear install phpunit/PHPUnit

詳しくは以前の記事で。

■Bake

PHPのエラー

お決まりのtimezoneを設定してくれ的なのが出るので以下のようにする。

vim ./lib/Cake/Console/cake.php

require_onceの前に以下の文を一応記述しておく。

ini_set('date.timezone', 'Asia/Tokyo');

データベースの初期設定

以下のコマンドを実行してDBの設定をする。

./lib/Cake/Console/cake.php bake

対話式ですすめる。以下の順序で設定する。

Welcome to CakePHP v2.0.0 Console
---------------------------------------------------------------
App : app
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/
---------------------------------------------------------------
Your database configuration was not found. Take a moment to create one.
---------------------------------------------------------------
Database Configuration:
---------------------------------------------------------------
Name:  
[default] > test
Driver: (Mysql/Postgres/Sqlite/Sqlserver) 
[Mysql] > 
Persistent Connection? (y/n) 
[n] > 
Database Host:  
[localhost] > 
Port?  
[n] > 
User:  
[root] > 
Password:  
> hogehoge
Database Name:  
[cake] > dbname
Table Prefix?  
[n] > 
Table encoding?  
[n] > 

モデル(クラス)の作成

以下のコマンドを再度実行する。

./lib/Cake/Console/cake.php bake

以下のように進めていく。テーブル設計などは適当なので適宜読み替える。

---------------------------------------------------------------
App : app
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/
---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[F]ixture
[T]est case
[Q]uit
What would you like to Bake? (D/M/V/C/P/F/T/Q) 
> Model
What would you like to Bake? (D/M/V/C/P/F/T/Q) 
> M
---------------------------------------------------------------
Bake Model
Path: /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/Model/
---------------------------------------------------------------
Possible Models based on your current database:
1. Move
Enter a number from the list above,
type in the name of another model, or 'q' to exit  
[q] > 1
A displayField could not be automatically detected
would you like to choose one? (y/n) 
> y
1. id
2. game
3. x
4. y
5. color
Choose a field from the options above:  
> 1
Would you like to supply validation criteria 
for the fields in your model? (y/n) 
[y] > y

バリデーションの設定をしていくと、そのうち以下のようにユニットテスト用ファイルを生成するか聞いてくるので[y]を選択する。

Do you want to bake unit test files anyway? (y/n) 
[y] > y
Creating file /Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/Test/Case/Model/MoveTest.php
Wrote `/Applications/XAMPP/xamppfiles/htdocs/cake.sample.justoneplanet.info/app/Test/Case/Model/MoveTest.php`

■生成されたファイル

app/Test/Case/Model/MoveTestCase.php

<?php
/* Move Test cases generated on: 2011-10-21 00:40:13 : 1319125213*/
App::uses('Move', 'Model');

/**
 * Move Test Case
 *
 */
class MoveTestCase extends CakeTestCase {
/**
 * Fixtures
 *
 * @var array
 */
    public $fixtures = array('app.move');

/**
 * setUp method
 *
 * @return void
 */
    public function setUp() {
        parent::setUp();

        $this->Move = ClassRegistry::init('Move');
    }

/**
 * tearDown method
 *
 * @return void
 */
    public function tearDown() {
        unset($this->Move);

        parent::tearDown();
    }

/**
 * こんな感じで書いていけばいいよ的なmethod
 */
    public function testGetById() {
        $this->assertEqual($this->Move->getById(1), 1);
    }
}

app/Model/MoveTestCase.php

<?php
App::uses('AppModel', 'Model');
/**
 * Move Model
 *
 */
class Move extends AppModel {
/**
 * Display field
 *
 * @var string
 */
    public $displayField = 'id';

/**
 * つじつま合わせのメソッド
 */
    public function getById($id) {
        return 1;
    }
}

最後にちゃちゃっとhttp://cake.sample.justoneplanet.info/test.phpにアクセスすればテストが書けているのがわかる。

iOSでSQLiteを使用する

直で扱うのはすごく面倒なのでFMDBというライブラリを使用する。

■ダウンロード

githubからダウンロードする。

gitがないと生きていけない体になってしまったので以下のコマンドでcloneする。

git clone https://github.com/ccgus/fmdb.git

必要なファイル

src配下にある以下の6ファイルが必要なのでコピーしてプロジェクトにインポートする。

  • FMDatabase.h
  • FMDatabase.m
  • FMDatabaseAdditions.h
  • FMDatabaseAdditions.m
  • FMResultSet.h
  • FMResultSet.m

■フレームワーク

以下のフレームワークが必要になる。

  • libsqlite3.dylib

ちなみにテスト用のターゲットが存在している場合にはそちらにもフレームワークを追加しておく。

■実装

設計思想によるがテーブルごとにクラスを作る。

DB.h

以下のようにハンドラ(的なインスタンス)の取得、テーブルの作成、データの取得・登録・更新・削除用のメソッドを用意する。

#import <Foundation/Foundation.h>
@interface HogeDB : NSObject {
    NSString *dbFileName;
}
@property (nonatomic, retain) NSString *dbFileName;
- (FMDatabase *)getDB;
- (void)create;
- (NSMutableArray *)getById:(long)identifer;
- (bool)registerWithName:(NSString *)name;
- (bool)updateById:(long)identifer withName:(NSString *)name;
- (bool)deleteById:(long)identifer
@end

DB.m

以下のようにして各メソッドを実装する。prepared statementなどはライブラリでカバーされている(日本語可)。

#import "HogeDB.h"
#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"

#define DB_FILE @"info.justoneplanet.iphone.database.sqlite"// データベースファイル名
#define KEY_TABLE_HOGE_VERSION @"key_table_hoge_version"

@implementation HogeDB

- (id)init
{
    self = [super init];
    if (self != nil) {
        [self setDbFileName:DB_FILE];
    }
    return self;
}

// テスト用に別DBファイルを指定できるようにする
- (id)initWithDBFile:(NSString *)name
{
    self = [super init];
    if (self != nil) {
        [self setDbFileName:name];
    }
    return self;
}

// データベースハンドラ的なFMDatabaseのインスタンスを取得する
- (FMDatabase *)getDB
{
    NSArray *pathes = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *dir = [pathes objectAtIndex:0];
    NSString *path = [dir stringByAppendingPathComponent:[self dbFileName]];
    FMDatabase *db = [FMDatabase databaseWithPath:path];
    if (![db open]) {
        @throw [NSException exceptionWithName:@"DBOpenException" reason:@"couldn't open specified db dile" userInfo:nil];
    }
    return db;
}

// テーブルを作成する
// NSDefaultのフラグで既にあるかチェック
// またバージョン番号をNSDefaultに保存して
// 今後のバージョンアップの際のALTER等に備える
- (void)create
{
    FMDatabase *db = [self getDB];
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    int version = [ud integerForKey:KEY_TABLE_HOGE_VERSION];
    if(version == 0){
        NSDictionary *tables = [NSDictionary  dictionaryWithObjectsAndKeys:
            @"CREATE TABLE `hoge`( `_id` INTEGER PRIMARY KEY AUTOINCREMENT, `id_category` INTEGER, `created` INTEGER, `face` TEXT, `tag`  TEXT);", @"hoge",
            nil];
        
        for (NSString *table in tables) {// CREATE TABLE系を全て実行
            if (![db tableExists:table]) {// テーブルが存在していない場合
                if (![db executeUpdate:[tables objectForKey:table], nil]) {
                    NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
                }
            }
        }
        [ud setInteger:1 forKey:KEY_TABLE_HOGE_VERSION];
        [ud synchronize];
    }
    [db close];
}

// IDで取得する
- (NSMutableArray *)getById:(long) identifer
{
    FMDatabase *db      = [self getDB];
    FMResultSet *result = [db executeQuery:@"SELECT * FROM `hoge` WHERE `_id` = ?", [NSNumber numberWithLong:identifer]];
    NSMutableArray *ary = [NSMutableArray arrayWithObjects: nil];
    while ([result next]) {
        [ary addObject:[result resultDict]];
    }
    [result close];
    [db close];
    return ary;
}

// 登録する
- (bool)registerWithName:(NSString *)name
{
    FMDatabase *db = [self getDB];
    long long int unixtime = (long long int)[[NSDate date] timeIntervalSince1970];
    if([db executeUpdate:@"INSERT INTO `hoge`(`name`, `created`) VALUES(?, ?)", name, [NSNumber numberWithLongLong:unixtime]]){
        [db close];
        return YES;
    }
    else{// 登録に失敗した場合
        NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
        [db close];
        return NO;
    }
}

// idを条件にして更新
- (bool)updateById:(long)identifer withName:(NSString *)name
{
    FMDatabase *db = [self getDB];
    if ([db executeUpdate:@"UPDATE `hoge` SET `name` = ? WHERE `_id` = ?", name, [NSNumber numberWithLong:identifer]]) {
        [db close];
        return YES;
    }
    else{// 更新に失敗した場合の処理
        NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
        [db close];
        return NO;
    }
}

// idを条件にして削除する
- (bool)deleteById:(long) identifer
{
    FMDatabase *db = [self getDB];
    if([db executeUpdate:@"DELETE FROM `hoge` WHERE `_id` = ?", [NSNumber numberWithLong:identifer]]){
        [db close];
        return YES;
    }
    else{// 削除に失敗した場合の処理
        NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
        [db close];
        return NO;
    }
}
@end

残念ながらandroidとは違いバージョンアップによるALTERなどは自分でコントロールせねばならない。

■テスト

以下のようにGHUnitを使ってテストケースを記述する。

HogeDBTest.h

実行順序をコントロールするためにメソッド名を連番にした。

#import <GHUnitIOS/GHUnit.h>
@class HogeDB;
@interface HogeDBTest : GHTestCase {
    HogeDB *hoge;
}
@property (nonatomic, retain) HogeDB *hoge;
- (void)test1_getById;
- (void)test3_registerWithName;
- (void)test4_updateById;
- (void)test5_deleteById;
@end

HogeDBTest.m

#import "HogeDBTest.h"
#import "HogeDB.h"

#define DB_FILE @"info.justoneplanet.hoge.i.test.sqlite"

@implementation HogeDBTest
@synthesize hoge;
- (void)setUpClass
{
    hoge = [[HogeDB alloc] initWithDBFile:DB_FILE];
    [hoge create];
}
- (void)tearDownClass
{
    [hoge del];
    [hoge release];
}
- (void)test1_getById
{
    NSMutableArray *data = [hoge getByIdCategory:1];
    GHAssertEqualStrings(
                         [[data objectAtIndex:([data count] - 1)] objectForKey:@"name"],
                         @"Emily",
                         @"");
    GHAssertEqualStrings(
                         [[data objectAtIndex:0] objectForKey:@"name"],
                         @"John",
                         @"");
}
- (void)test2_registerWithName
{
    [hoge registerWithName:@"Nick"];
    NSMutableArray *data = [hoge getAll];
    GHAssertEqualStrings(
                         [[data objectAtIndex:0] objectForKey:@"name"],
                         @"Nick",
                         @"");
}
- (void)test3_updateById
{
    [hoge updateById:2l withName:@"Mike"];
    GHAssertEqualStrings(
                         [[[hoge getById:2l] objectAtIndex:0] objectForKey:@"name"],
                         @"Mike",
                         @"");
    [hoge updateById:2l withName:@"Ken"];// 元に戻しておく
}
- (void)test4_deleteById
{
    NSMutableArray *data1 = [hoge getAll];
    GHAssertEqualStrings(
                         [[data1 objectAtIndex:0] objectForKey:@"name"],
                         @"Nick",
                         @"");
    [hoge deleteById:[[[data1 objectAtIndex:0] objectForKey:@"_id"] longValue]];
    NSMutableArray *data2 = [hoge getAll];
    GHAssertEqualStrings(
                         [[data2 objectAtIndex:0] objectForKey:@"name"],
                         @"John",
                         @"");
}
@end

ちょっとテストケースが少ないけどこんな感じ。

Google Developer Day 2011のAndroid問題を解いてみる

src/com/google/android/apps/gddquiz/IQuizService.aidlを配置したら、後はサービスを扱う感じ。

package info.justoneplanet.android.gdd2011;

import com.google.android.apps.gddquiz.IQuizService;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class MainActivity extends Activity {
    private IQuizService mService;
    
    /**
     * ServiceConnection
     * サービスへの接続
     */
    private ServiceConnection gddServiceConn = new ServiceConnection(){
        /**
         * onServiceConnected
         * サービスに接続時
         */
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // mService経由でやりとりする
            mService = IQuizService.Stub.asInterface(service);
            Log.e("onServiceConnected", "onServiceConnected");
            try {
                Log.e("code", mService.getCode());
            }
            catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        /**
         * onServiceDisconnected
         * サービスから切断時
         */
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e("onServiceDisconnected", "onServiceDisconnected");
            mService = null;
        }
    };
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // IQuizServiceにbindする
        Intent intent = new Intent(IQuizService.class.getName());
        if(bindService(
                intent,
                gddServiceConn,
                BIND_AUTO_CREATE
        )){
            Log.e("bindService", "connected");
        }
        else{
            Log.e("bindService", "connect - error");
        }
    }
    
    @Override
    public void onDestroy()
    {
        super.onDestroy();
        unbindService(gddServiceConn);
    }
}

Goolge Developer Day 2011のWeb Gameを解いてみる

Extensionとか入れたりするの面倒なので以下のJavaScriptをコンソールに打ち込む。

var ary = [];
for(var i = 0; i < 1024; i++){
    if($('card' + i)){
        $('#card' + i).click();
        ary.push($('#card' + i).css('backgroundColor'));
    }
}
ary.forEach(function(elm, i1, ary){
    for(var i2 = i1 + 1; i2 < ary.length; i2++){
        if(ary[i1] == ary[i2]){
            $('#card' + i1).click();
            $('#card' + i2).click();
        }
    }
});

iOSアプリケーションを開発しているマシンを変える

毎回はまるので書いておく。

■キーチェーンアクセス

「アプリケーション > ユーティリティ > キーチェーンアクセス > 証明書アシスタント > 認証局」で証明書を要求する。入力内容は以下のとおり。

  • メールアドレス
  • 通称

「ディスクに保存をする」を選択しデスクトップに保存する。

■Provisioning Portal

Provisioning Portal > Certificates」を選択し、フォームから保存した証明書をアップロードする。

  1. Distribution Provisioning Profilesを作りなおす
  2. 作りなおしたProvisioning Profilesをダウンロードしダブルクリックでインストールする
  3. XCodeの「Build Setting > Code Signing > Release」で新しいProvisioning Profileを選択する

Wildcard Appとプロビジョニングプロファイル(おまけ)

Provisioning Portal >
App IDs > create
」で以下のように入力するとwild card appが作れる。

description Wildcard App
Bundle Identifier *

Provisioning Portal > Provisioning > Development > NEW Profile」で以下のように入力する。

Profile Name Team Provisioning Profile
App ID Wildcard App

上述により全てのアプリの開発で有効なprovisioning profileが作成できるので、1アプリごとに作る手間が省ける。

androidアプリは(パスワードと)keystoreがあればマシンを変えてもそのまま開発できる。iOSアプリは開発マシンの認証(のようなものが)ある。iPhoneは手間がかかるが管理画面に入れれば新しいマシンで開発を進められる。androidはkeystoreが無くなるとアプリの更新ができなくなる。