@blog.justoneplanet.info

日々勉強

Unicode 6.0の絵文字のソースをコード内で扱える形式に変換する

自分用のメモ。元データをJavaやJavaScript内で扱える形式に変換する。以下のようにnodeで実装した。

var fs = require('fs');
var csv = require('csv');
csv()
.from.stream(fs.createReadStream(__dirname+'/EmojiSources.txt'), {"delimiter" : ";"})
.to.path(__dirname+'/sample.txt')
.transform(function(row){
  var elm = row.slice(0, 1); 
  if (elm[0].indexOf('1F') > -1) {
    var code = parseInt("0x" + elm[0], 16);
    code -= 0x10000;
    var hi = code >> 10; 
    var lo = code & 0x3FF;
    hi |= 0xD800;
    lo |= 0xDC00;
    elm[0] = '\\u' + hi.toString(16) + '\\u' + lo.toString(16);
    elm[1] = String.fromCharCode(hi, lo);
  }
  else {
    elm[1] = String.fromCharCode(parseInt(elm[0], 16));
    elm[0] = '\\u' + elm[0] + ''; 
  }
  return elm;
})
.on('record', function(row,index){
  console.log('#' + index + ' ' + JSON.stringify(row));
})
.on('close', function(count){
  console.log('Number of lines: '+count);
})
.on('error', function(error){
  console.log(error.message);
});

AndroidでKeyboardより手前にViewを表示する

■PopupWindowを使う

// 中身のViewの生成
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View contentView = inflater.inflate(R.layout.content, null);

// popupウィンドウの生成
popupWindow = new PopupWindow(contentView);
popupWindow.setWidth(WindowManager.LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);

以下のコードで表示できる。

View contentView = ((ViewGroup)findViewById(android.R.id.content)).getChildAt(0);
popupWindow.showAtLocation(contentView, Gravity.BOTTOM, 0, 0);

廃案

DialogFragmentを使う

PopupWindowと似たような表示ができると思い試してみたがKeyBoardの上に表示することができなかった。

public static class SampleDialog extends DialogFragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        getDialog().getWindow().setGravity(Gravity.BOTTOM);
        getDialog().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
        return super.onCreateView(inflater, container, savedInstanceState);
    }
}

以下のコードで表示できる。

SampleDialog dialog = new SampleDialog();
dialog.show(getSupportFragmentManager(), "hoge");

SolrでDataImportHandlerを使う

前回の続き。

■インポート元などの設定

solr/conf/data-config.xml

はてなのドキュメントが少し違っていて、以下のようにdocumentノードが無いとエラーになる。

<?xml version="1.0" encoding="UTF-8" ?>
<dataConfig>
  <dataSource
    name="dbname"
    driver="com.mysql.jdbc.Driver"
    url="jdbc:mysql://localhost/dbname"
    user="username"
    password="password"
    batchSize="-1"
    useUnicode="true"
    characterEncoding="utf8"
    useOldUTF8Behavior="true"
    readOnly="true" />
  <document name="items">
    <entity
      name="table"
      dataSource="dbname"
      query="
        SELECT
          `id`,
          `key`,
          `value`
        FROM
          `table`
        WHERE
          `created` &lt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 7 DAY)
        "
      deltaQuery="
        SELECT
          `id`
        FROM
          `table`
        WHERE
          `created` &lt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 7 DAY)
        AND
          `created` &gt; DATE_SUB('${dataimporter.last_index_time}', INTERVAL '7 9' DAY_HOUR)
        "
     deltaImportQuery="
        SELECT
          `id`,
          `key`,
          `value`
        FROM
          `table`
        WHERE
          `id` = '${dataimporter.delta.id}'
        "
      transformer="ClobTransformer,DateFormatTransformer">
    </entity>
  </document>
</dataConfig>

solr/conf/solrconfig.xml

以下の記述を追加する。

  <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
    <lst name="defaults">
     <str name="config">data-config.xml</str>
    </lst>
  </requestHandler>

エラー1

後述のURLでインポートをすると以下のようなエラーが発生する。

Caused by: java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
        at java.net.URLClassLoader$1.run(URLClassLoader.java:217)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:205)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:321)
        at java.net.FactoryURLClassLoader.loadClass(URLClassLoader.java:615)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at org.apache.solr.core.SolrResourceLoader.findClass(SolrResourceLoader.java:378)

以下のコマンドを実行してJDBCドライバを配置する。

wget http://www.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.22.tar.gz/from/http://cdn.mysql.com/
tar xvzf mysql-connector-java-5.1.22.tar.gz
cp mysql-connector-java-5.1.22/mysql-connector-java-5.1.22-bin.jar ./lib/

■インポートの実行

以下のURLにアクセスする。


http://localhost/solr/admin/dataimport.jsp?handler=/dataimport

■差分インポートの実行

ドキュメントを見ると差分インポートの仕方もあるようなのだが、(Jetty経由で叩く?)いまいちよくわからないので、以下のようにcrontabとcurlで実行することにした。

00 * * * * curl "http://localhost/solr/dataimport?clean=false&commit=true&command=delta-import"

参考

緊急地震速報 by Extensionのサーバーを書き直す

そろそろOAuth認証にしておかないとまずいので書きなおすことにした。

■環境構築

必要なパッケージのインストール等をする。

sudo yum -y install openssl-devel gcc-c++ git make
git clone https://github.com/isaacs/nave.git ~/.nave
~/.nave/nave.sh install stable
~/.nave/nave.sh use stable
echo "~/.nave/nave.sh use stable" >> ~/.bash_profile
sudo curl https://npmjs.org/install.sh --insecure | sh

npm -g install forever
npm install twitter
npm install websocket

以下のファイルを編集する。

vim /etc/security/limits.conf

以下の記述を加える。

*               soft    nofile            32768
*               hard    nofile            32768

■実装

以下のようにした。

const PORT = (process.argv[2])? process.argv[2] : 8080;
const FOLLOWING_ID = 16052553;
const CONSUMER_KEY    = "CONSUMER_KEY";
const CONSUMER_SECRET = "CONSUMER_SECRET";
const TOKEN_KEY       = "TOKEN_KEY";
const TOKEN_SECRET    = "TOKEN_SECRET";

var util            = require('util')
  , http            = require('http')
  , twitter         = require('twitter')
  , WebSocketServer = require('websocket').server
;

util.puts('[' + new Date() + ']Listen ' + process.argv[2]);
// build server
var server = http.createServer(function(request, response) {
  util.puts('[' + new Date() + ']Received request for ' + request.url);
  response.writeHead(404);
  response.end();
});
server.listen(PORT, function() {
  util.puts('[' + new Date() + ']Server is listening on port ' + PORT);
});
var wsServer = new WebSocketServer({
  "httpServer"            : server,
  "autoAcceptConnections" : true
});
wsServer.on(
  'connect',
  function(connection){
    util.puts('[' + new Date() + ']Connection accepted.');
    connection.sendUTF('{"status" : "accepted"}');
  }
);
setInterval(function () {wsServer.broadcastUTF("");}, 30 * 1000);

// build twitter connection
var twit = new twitter({
  "consumer_key"        : CONSUMER_KEY,
  "consumer_secret"     : CONSUMER_SECRET,
  "access_token_key"    : TOKEN_KEY,
  "access_token_secret" : TOKEN_SECRET
});
twit.stream('statuses/filter', {"follow" : FOLLOWING_ID}, function(stream) {
  stream.on('data', function(data) {
    if (data['user']['id'] === FOLLOWING_ID) {
      delete data['source'];
      delete data['contributors'];
      delete data['entities'];
      delete data['favorited'];
      delete data['lang'];
      delete data['truncated'];
      delete data['in_reply_to_status_id'];
      delete data['in_reply_to_status_id_str'];
      delete data['in_reply_to_user_id'];
      delete data['in_reply_to_user_id_str'];
      delete data['in_reply_to_screen_name'];
      delete data['user']['name'];
      delete data['user']['screen_name'];
      delete data['user']['url'];
      delete data['user']['description'];
      delete data['user']['protected'];
      delete data['user']['followers_count'];
      delete data['user']['friends_count'];
      delete data['user']['listed_count'];
      delete data['user']['created_at'];
      delete data['user']['favourites_count'];
      delete data['user']['utc_offset'];
      delete data['user']['time_zone'];
      delete data['user']['geo_enabled'];
      delete data['user']['verified'];
      delete data['user']['statuses_count'];
      delete data['user']['lang'];
      delete data['user']['contributors_enabled'];
      delete data['user']['is_translator'];
      delete data['user']['profile_background_color'];
      delete data['user']['profile_background_image_url'];
      delete data['user']['profile_background_image_url_https'];
      delete data['user']['profile_background_tile'];
      delete data['user']['profile_image_url'];
      delete data['user']['profile_image_url_https'];
      delete data['user']['profile_link_color'];
      delete data['user']['profile_sidebar_border_color'];
      delete data['user']['profile_sidebar_fill_color'];
      delete data['user']['profile_text_color'];
      delete data['user']['profile_use_background_image'];
      delete data['user']['default_profile'];
      delete data['user']['default_profile_image'];
      delete data['user']['following'];
      delete data['user']['follow_request_sent'];
      delete data['user']['notifications'];
      util.puts('[' + new Date() + ']EEW ' + data);
      wsServer.broadcastUTF(JSON.stringify(data));
    }   
  }); 
  stream.on('error' , function(err) {
    util.puts('[' + new Date() + ']Error::Stream' + err);
  }); 
});
setTimeout(
  function () {
    throw new Error('[' + new Date() + ']Error::for reboot');
  },  
  1000 * 60 * 60// 1 hour
);

■起動

以前はnohupとか使ってたが、ちゃんとforeverを使うようにする。

ulimit -n 32768
forever start -a --spinSleepTime=10000 -w --watchDirectory=./ -l ~/.forever/eew80.log server.js 80
forever start -a --spinSleepTime=10000 -w --watchDirectory=./ -l ~/.forever/eew443.log server.js 443
forever start -a --spinSleepTime=10000 -w --watchDirectory=./ -l ~/.forever/eew8080.log server.js 8080

■サーバーの再起動

rebootするとforeverが止まるので起動スクリプトを書く。

/etc/rc.d/init.d/eewd

パスは適宜かえるとする。

 /etc/init.d/functions

NAME=eewd
SOURCE_DIR=/var/www/hogehoge.com/eew
SOURCE_FILE=server.js

user=root
pidfile=/root/.forever/pids/$NAME.pid
logfile=/root/.forever/production.log
forever_dir=/root/.nave/installed/0.8.17/bin

node=/root/.nave/installed/0.8.17/bin/node
forever=/root/.nave/installed/0.8.17/bin/forever
sed=sed

export PATH=$PATH:/root/.nave/installed/0.8.17/bin
export NODE_PATH=$NODE_PATH:/root/.nave/installed/0.8.17/lib/node_modules


start() {
  echo "Starting $NAME node instance: "

  if [ "$foreverid" == "" ]; then
    # Create the log and pid files, making sure that
    # the target use has access to them
    touch $logfile
    chown $user $logfile

    touch $pidfile
    chown $user $pidfile

    # Launch the application
    daemon --user=root \
      $forever start -a -d --spinSleepTime=10000 -w --watchDirectory=$SOURCE_DIR -p $forever_dir --pidfile $pidfile -l $logfile $SOURCE_DIR/$SOURCE_FILE
    RETVAL=$?
  else
    echo "Instance already running"
    RETVAL=0
  fi
}

stop() {
  echo -n "Shutting down $NAME node instance : "
  if [ "$foreverid" != "" ]; then
    $node $SOURCE_DIR/prepareForStop.js
    $forever stop -p $forever_dir $id
  else
    echo "Instance is not running";
  fi
  RETVAL=$?
}

if [ -f $pidfile ]; then
  read pid < $pidfile
else
  pid = ""
fi

if [ "$pid" != "" ]; then
  # Gnarly sed usage to obtain the foreverid.
  sed1="/$pid\]/p"
  sed2="s/.*\[\([0-9]\+\)\].*\s$pid\].*/\1/g"
  foreverid=`$forever list -p $forever_dir | $sed -n $sed1 | $sed $sed2`
else
  foreverid=""
fi

case "$1" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  status)
    status -p ${pidfile}
    ;;
  *)
    echo "Usage:  {start|stop|status}"
    exit 1
    ;;
esac
exit $RETVAL

これでしばらくテストして上手く行けばリリースする。

AndroidのListViewで選択状態の項目の背景色を変える

意外と面倒だったのでメモしておく。

■実装

res/layout-v11/cell.xml

API level 11以降の場合は以下のように記述すれば終わる。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/activatedBackgroundIndicator"
    android:orientation="vertical">
    <TextView 
        android:id="@+id/name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    <TextView 
        android:id="@+id/text"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

res/layout/cell.xml

上述のlayoutファイルしかないとAPI level 10以下で実行できなくなるので以下のレイアウトファイルを追加する。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView 
        android:id="@+id/name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    <TextView 
        android:id="@+id/text"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

DataListCursorAdapter.java

bindViewでonDataAssignedを実行するようにする。

class DataListCursorAdapter extends CursorAdapter {
    private Listener mListener;
    public static class ViewHolder {
        public TextView name;
        public TextView text;
    }
    @SuppressWarnings("deprecation")
    public DataListCursorAdapter(Context context, Cursor c, Listener listener) {
        super(context, c);
        mListener = listener;
    }
    public DataListCursorAdapter(Context context, Cursor c, Listener listener, boolean autoRequery) {
        super(context, c, false);
        mListener = listener;
    }
    public DataListCursorAdapter(Context context, Cursor c, Listener listener, int flags) {
        super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
        mListener = listener;
    }
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        try {
            ViewHolder holder = (ViewHolder) view.getTag();
            JSONObject json = new JSONObject(cursor.getString(cursor.getColumnIndex(Table.DATA)));
            final Data data = new Data(json);
            mListener.onDataAssigned(data, view);
            
            holder.name.setText(data.name);
            holder.text.setText(data.text);
        }
        catch (JSONException e) {
        }
    }
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.cell, null);
        ViewHolder holder = new ViewHolder();
        holder.name      = (TextView) view.findViewById(R.id.name);
        holder.text      = (TextView) view.findViewById(R.id.text);
        view.setTag(holder);
        return view;
    }
    public interface Listener {
        public void onDataAssigned(Data data, View view);
    }
}

DataListFragment.java

ListViewは画面サイズを敷き詰めるのに十分な数のViewを生成し、スクロールして表示される項目についてはViewの中身を入れ替えるだけで実現しているので省メモリで高速である。一方で選択項目のViewの背景色を変えるとそのViewを再利用した項目の背景色も変わってしまう。従って、以下のようにinterfaceを実装してHoney Comb未満ではデータがbindされた時にリストの背景色を確認・変更するようにする。

    @Override
    public void onDataAssigned(Data data, View view) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            return;
        }
        if (TextUtils.equals(mLastSelectedDataId, data.id)) {
            view.setBackgroundColor(0xfff7aecb);
        }
        else {
            view.setBackgroundColor(0x00000000);
        }
    }

最後にタップしたリストのデータをフィールドに保持するようにする。

    @Override
    public void onItemClick(AdapterView<?> listView, View view, int position, long id) {
        Cursor cursor = mArrayCursorAdapter.getCursor();
        JSONObject json = new JSONObject(cursor.getString(cursor.getColumnIndex(Table.DATA)));
        final Data data = new Data(json);
        if (data != null && TextUtils.equals(mLastSelectedDataId, data.id)) {
            getListView().setItemChecked(position, false);
            mLastSelectedDataId = null;
        }
        else if (data != null) {
            getListView().setItemChecked(position, true);
            mLastSelectedDataId = data.id;
        }
    }

iOSでキーボードの表示と非表示に応じた処理をする

- (void)keyboardDidShow:(NSNotification *)note {
    // something
}
- (void)keyboardDidHide:(NSNotification *)note {
    // something
}
- (id)init {
    self = [super init];
    if (self) {
        // キーボードが表示された時
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
        // キーボードが隠れた時
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
    }
    return self;
}

node.jsでMySQLに接続する

MySQLサーバーから接続を切られた時は、errorイベントが発火する。一方で接続できなかった時はcallback関数の引き数にerrorオブジェクトが格納される。従って以下のように実装してみた。

var mysql = require('mysql');
var dbh = mysql.createConnection({
  "host"     : CONF['mysql']['host'],
  "user"     : CONF['mysql']['user'],
  "password" : CONF['mysql']['pass'],
  "database" : CONF['mysql']['db']
});
var handleDisconnect = function(dbh){
  dbh.on('error', function(err) {
    if (!err.fatal) {
      return;
    }
    if (err.code !== 'PROTOCOL_CONNECTION_LOST') {
      throw err;
    }
    console.log('Re-connecting lost connection: ' + err.stack);
    util.puts('mysql::connect');
    dbh = mysql.createConnection(dbh.config);
    handleDisconnect(dbh);
    dbh.connect(dbhConnectHandler);
  });
}
var dbhConnectHandler = function(err){
  if (err) {
    util.puts('mysql::failed to connect[' + err.code + ']');
    setTimeout(
      function(){
        util.puts('mysql::connect');
        dbh = mysql.createConnection(dbh.config);
        handleDisconnect(dbh);
        dbh.connect(dbhConnectHandler);
      },
      1000 * 30// 30 seconds
    );
  }
  else {
    util.puts('mysql::connected');
  }
};
util.puts('mysql::connect');
handleDisconnect(dbh);
dbh.connect(dbhConnectHandler);

PHPとかでMySQLに繋ぐのに慣れてると発想の転換が必要になる。

他のファイルのスクリプトと接続を共有する

expressを使っているとapp.jsでの接続をroutes/index.jsでも使いたい事がある。他にも方法はあると思うが以下のようにしてオブジェクトを共有している。

app.js
require('./share').dbh = dbh;
routes/index.js
var dbh = require('./share').dbh;

その場合は以下のようにして再接続後の接続を共有オブジェクトにセットする必要がある。

var mysql = require('mysql');
var dbh = mysql.createConnection({
  "host"     : CONF['mysql']['host'],
  "user"     : CONF['mysql']['user'],
  "password" : CONF['mysql']['pass'],
  "database" : CONF['mysql']['db']
});
var handleDisconnect = function(dbh){
  dbh.on('error', function(err) {
    if (!err.fatal) {
      return;
    }
    if (err.code !== 'PROTOCOL_CONNECTION_LOST') {
      throw err;
    }
    console.log('Re-connecting lost connection: ' + err.stack);
    util.puts('mysql::connect');
    dbh = mysql.createConnection(dbh.config);
    handleDisconnect(dbh);
    dbh.connect(dbhConnectHandler);
    require('./share').dbh = dbh;// ...(a)
  });
}
var dbhConnectHandler = function(err){
  if (err) {
    util.puts('mysql::failed to connect[' + err.code + ']');
    setTimeout(
      function(){
        util.puts('mysql::connect');
        dbh = mysql.createConnection(dbh.config);
        handleDisconnect(dbh);
        dbh.connect(dbhConnectHandler);
        require('./share').dbh = dbh;// ...(b)
      },
      1000 * 30// 30 seconds
    );
  }
  else {
    util.puts('mysql::connected');
  }
};
util.puts('mysql::connect');
handleDisconnect(dbh);
dbh.connect(dbhConnectHandler);
require('./share').dbh = dbh;

仮に(a)と(b)が存在しない場合、以下の様なroutes/index.js内では古い接続に対してクエリを発行する事になる。

dbh.query("something query", null, function (err) {
  if (err) {/*err*/}
  else {/*success*/}
});

この場合、厄介な事にcallbackが実行されず原因究明が困難となる。

iOSでUIViewのタップを検出する

■実装

以下のようにUITapGestureRecognizerを使う。

UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapped:)];
[view addGestureRecognizer:recognizer];// view.tag == VIEW_HOGE_TAG

viewがタップされると以下のメソッドが実行される。

- (void)onTapped:(UITapGestureRecognizer *)recognizer {
    switch (recognizer.view.tag) {
        case VIEW_HOGE_TAG:
            // hogehoge
            break;
            
        case VIEW_FUGA_TAG:
            // fugafuga
            break;
            
        default:
            break;
    }
}

CakePHP 2でRedisにSessionを保持する

Cake2系用のが無かったのでCake1系用のこれをforkして作ってみた。

■導入

iRedis

Vendorに配置する

cakephp-redis-session

app/Model/DataSource/Session/に配置する

core.php

core.phpに以下のように記述する。

Configure::write('Session.handler', array('engine' => 'RedisSession'));
Configure::write('RedisSession.hostname', 'some.host.name');
Configure::write('RedisSession.port', 1337);//port
Configure::write('RedisSession.password', 'your password');// password
Configure::write('RedisSession.database', 0);// database number

■コード

オリジナルのコードはパスワードやDB番号が設定できなかったので改良してある。

App::import('Vendor', 'iRedis/iredis');
App::uses('DatabaseSession', 'Model/Datasource/Session');
/**
 * Redis Session Store Class
 */
class RedisSession extends DatabaseSession implements CakeSessionHandlerInterface {
    private static $store;
    private static $timeout;
    
    public function __construct() {
        parent::__construct();
        $timeout = Configure::read('Session.timeout');
        if (empty($timeout)) {
            $timeout = 60 * 24 * 90;
        }
        self::$timeout = $timeout;
    }

    /**
     * open
     * connect to Redis
     * authorize
     * select database
     */
    public function open() {
        $host = Configure::read('RedisSession.hostname');
        $port = Configure::read('RedisSession.port');
        $password = Configure::read('RedisSession.password');
        $database = Configure::read('RedisSession.database');

        if ($host !== null && $port !== null) {
            $redis = new iRedisForRedisSession(array('hostname' => $host, 'port' => $port));
        }
        else {
            $redis = new iRedisForRedisSession();
        }
        if (!empty($password)) {
            $redis->auth($password);
        }
        if (!empty($database)) {
            $redis->select($database);
        }

        self::$store = $redis;
    }

    /**
     * close
     * disconnect from Redis
     * @return type 
     */
    public function close() {
        self::$store->disconnect();
        return true;
    }

    /**
     * read
     * @param type $id
     * @return type 
     * - Return whatever is stored in key
     */
    public function read($id) {
        return self::$store->get($id);
    }

    /**
     * write
     * @param type $id
     * @param type $data
     * @return type 
     * - SETEX data with timeout calculated in open()
     */
    public function write($id, $data) {
        self::$store->setex($id, self::$timeout, $data);
        return true;
    }

    /**
     * destroy
     * @param type $id
     * @return type 
     * - DEL the key from store
     */
    public function destroy($id) {
        self::$store->del($id);
        return true;
    }

    /**
     * gc
     * @param type $expires
     * @return type 
     * not needed as SETEX automatically removes itself after timeout
     */
    public function gc($expires = null) {
        return true;
    }
}
if (!class_exists('iRedisForRedisSession')) {
    class iRedisForRedisSession extends iRedis {
        function __destruct() {
            // don't disconnect yet
        }
        function disconnect() {
            parent::__destruct();
        }
    }
}

iRedisのコードを読んでみると面白いのだがマジックメソッド__callを使って各コマンドをcallしている。

AndroidでIsolatedContextを使ってみる

■コード

以下のようにしてIsolatedContextのサブクラスを定義する。

public class HogeActivityTest extends ActivityInstrumentationTestCase2<Kaomoji> {
    private Kaomoji mActivity;
    private static final MockContentResolver RESOLVER = new MockContentResolver();
    private static class TestIsolatedContext extends IsolatedContext {
        private Context mContext;
        public TestIsolatedContext(ContentResolver resolver, Context targetContext) {
            super(resolver, targetContext);
            mContext = targetContext;
        }
        @Override
        public SharedPreferences getSharedPreferences(String name, int mode) {
            return mContext.getSharedPreferences("prefix_" + name, mode);
        }
    }
}

必要なメソッドはOverrideして実装する。以下のようにしてインスタンス化して、production環境に影響しない事を確かめる。

TestIsolatedContext context = new TestIsolatedContext(RESOLVER, mActivity);
SharedPreferences preferences = context.getSharedPreferences(Constants.KEY_USER_DATA, Context.MODE_PRIVATE);
String str = preferences.getString(Constants.KEY_USER_STRING, null);
assertEquals(str, null);// production側で文字列が保存されているのに関係しない

参考

ちょうどIsolatedContextを使ってるテストがなかったので何となく間に合わせ的なコードになってしまった。