気が向いたらなんか書く

そんな感じです

Thymeleafで遊ぶ

Java用のテンプレートエンジンであるThymeleafで遊んでみた

基本

まず実装から。

次にテンプレート。

Templateを読み込むためのresolverをTemplateEngineに食わせます。テンプレート内の変数などはContextにセットしてengineに与えてやる。基本はこんな感じです。

TemplateResolver

一番キモであるテンプレートファイルを解決するTemplateResolverですが、これの実装は4種類あります。

  • FileTemplateResolver
  • ClassLoaderTemplateResolver
  • UrlTemplateResolver
  • ServletContextTemplateResolver

全てのresolverはITemplateResolverインターフェイスを実装しており、TemplateResolverという親クラスを継承してます。じゃあ何が違うかというと、テンプレートファイルを読み込む手段の違いがあります。

例えばFileTemplateResolverはコンストラクタでFileResourceResolverというリソース取得の為のクラスをセットしますが、この中ではFileクラスを用いて指定されたテンプレートをリソースファイルとして取得しようとします。

対してClassLoaderTemplateResolverはClassLoaderResourceResolver内でクラスローダを利用してリソースファイルを取得します

これの違いが一番わかり易いのは、パスを指定する時に出ます。

└─src
    ├─org
    │  └─misty
    │      └─rc
    │          └─thymeleafsample
    └─template
        └─index.html

こんな感じでテンプレートを置いたとします。

上記サンプルはClassLoaderTemplateResolver(長いな...)を使った例ですが、この場合、ClassLoaderはsrcディレクトリをルートディレクトリとした相対パスで指定できます。

FileTemplateResolverを使う場合はこうなります。

まあ、ちょっと考えれば分かるんですけど、例えば自作ライブラリでJARファイルに固めた時とかどうなるか、とか考えると、どのResolverを使うべきかいろいろ考えますね。

続きは気が向いたら書きます

Viewの拡張による独自属性と埋め込みフォント

TextViewなんかを拡張して、ついでに端末組み込みフォント以外のフォントも使っちゃおう的な話です。あわせて以下の資料も見ると多分大丈夫です

http://developer.android.com/intl/ja/training/custom-views/create-view.html

既存Viewを拡張したカスタムビューをXMLベースで構築する場合、そのコンポーネントの属性を予めresourcesで設定しておく必要があります。

上記のように、利用するカスタムビューに対して設定可能な属性名と属性フォーマットを定義します。

次にLayoutファイル上で、カスタムビューを設定します。ここで必要なのは独自に設定した属性は名前空間を別途明記しておく必要があるということです。

xmlns:app="http://schemas.android.com/apk/res/org.misty.rc.testproject"

上記のようにapp(NamespaceNameはなんでもいいです)と定義することで、その属性値に

app:text

という形でアクセスできるようになります。最後はカスタムビューの実装です。

コンストラクタで渡されたAttributeSetから独自の属性値を引っ張ってきたものをTypedArrayに格納します。配列内の値は

R.styleable.TestView_color

のようにINDEX参照可能なので、引いた値をsetします。サンプルは単純に独自属性の値を既存ビューの属性にセットしてるだけですが、コンストラクタ内で独自属性値を参照しておけば、overrideした関数内でいろんな処理が可能になります。

最後にassetManagerを利用して、assetsディレクトリ内に保存したフォントを利用してビューで表示されるフォントを変更します。createFromAssetを利用して新しいTypefaceを作成し、それをsetすれば変更可能です。

Android4.2のロック画面Widgetと自動更新系の話

AndroidWidgetの話

私、Nexus7持ってるんですが(ドヤァ)、最近JB4.2に更新しました。新しい機能としてまあいろいろあるんですが、その内の一つに「LockScreen Widget」ってのがあります。要するにロック画面上にWidgetを表示するやつです。

で、これがなんというか、ダサいんですよ。sleepから復帰した時に両サイドにWidgetのフレームが一瞬表示されたり、そもそも時計Widgetのフォントが格好悪い。マジでこれですかぁ?って感じなんですよね。

で、これが嫌でとりあえず今まで避けてきた…いや手をつけてこなかったWidgetに手を出してみようかと、そういう話です。

AppWidgetの基本形

今回はとりあえずお試しで、時刻をTextViewに表示させるだけのWidgetを作ってみます。このへんの話はいろいろググれば出てくるんですが、一番簡単なのはAppWidgetProviderを継承したクラスで完結させちゃうのが一番簡単なんですかね。

とりあえずソースは以下を参照してください。

https://github.com/misty-rc/RonnClockRepeat

ポイント

初期設定

まずAndroidManifest.xmlにreceiverの設定が必要になります

ここにintent-filterとしてandroid.appwidget.action.APPWIDGET_UPDATEを設定してやることで、receiverはWidget更新をintentから認識することができます。meta-dataタグには基本設定を記述したxmlをしてします。

ここで重要なのが、新しく導入されたLockScreenでWidgetを使うためにはandroid:widgetCategory属性に"keyguard"という値を設定する必要があります。keyguardとは要するにロック画面のことですね。またkeyguardを指定した場合、ホーム画面用のレイアウトとは別にkeyguard用のレイアウト指定が必要になります。それがandroid:initialKeyguardLayout属性です。

ぶっちゃけて言ってしまうと、普通のWidgetを作ってappwidget-providerに上記属性を加えてやればロック画面上で使えるようになる…はずです。

コード的な

Widgetを作る場合に利用するAppWidgetProviderを継承したクラスでonUpdateをoverrideし、まずここで必要なことを済ませてしまいます。

まずintentを作成し、そこに更新用のオレオレアクションをセットします。次にgetBroadcastでPendingIntentを用意し、それをAlarmManagerに登録するします。getBroadcastで返されたPendingIntentがAlarmManagerによって一定時間ごとにbroadcastし、それをonReceiveで受けるという感じになります。

今回はAlarmManager.setRepeatingを使ってます。これはintervalの時間毎に処理を繰り返す関数ですが、AlarmManager.setを使い、一定Intervalを自前で用意する方法もあります。ググってよく見かけるのはこの方法ですね。

この場合、まずonUpdateで初期表示を行った後で、一定時間後にbroadcastするようにAlarmManagerにsetします。AlarmManager.setは1回こっきりなので、onReceiveしたタイミングで再度setしてやる必要があります。サンプルは適当に探してください(ぇ

Emacs+Ensime環境を使ってPlay Framework2.0で遊ぶ

とりあえず…

EmacsVimが幾ら使い慣れているからって言っても、モノによってはEclipse使ったりIntelliJ使ったりしたほうが当然便利な場合もあるわけで、特にJavaに関してはEmacs+JDEEは既にオワコンなので、わざわざEmacsで書くのも馬鹿らしい感じもするわけですが、それでもEmacsVimを使うのは単なるオナニーであることを認識している人だけが、この高みを望める訳です(はぁ?

取ってくる

Ensimeはgithubで公開されています。ここで思わずcloneとかしたくなっちゃいますが、Ensimeは利用するScalaのバージョンに強く依存しているようで、開発の先端を取ってくるよりちゃんとDownload用として用意しているものを使ったほうが、経験上、安定していていいようです。

https://github.com/aemoncannon/ensime/downloads

現時点ではScalaの最新安定版が2.9.2なので、2.9.2用で枝番が最新のやつを持ってくれば問題ないと思います

ここではensime_2.9.2-0.9.8.1.tar.gzを使います

scala-mode

おっとその前に、EnsimeはScalaに付属しているscala-modeに依存しますので、予めscala-modeが入っていることが前提になります。まぁ、当然入っているとは思うがニャー

え?無い?実はstable版の2.9.2のパッケージには入っていません!説明はここにありますが

Subsequently, we moved our repository to GitHub and changed our distribution packaging infrastructure. At this time, "tool-support" it is not by default part of the 2.9.2 distribution, but it will be made available again in the next releases. In the meantime, you can find the source repository at this address: http://www.scala-lang.org/node/91#tool_support

ということなので、そこから取ってきて入れます。

こんな感じに書いておけば動くでしょう

いれてみる

取ってきたensimeを適当な場所に展開し、設定でload-pathを通しておけばとりあえずOKです。

Play! Frameworkで遊ぶ為に

当然ですがPlayFrameworkが既にInstallされているとします。まぁ、Play自体は展開してPATH通せばいいだけなので、特に問題ないかと思います。とりあえず新しいプロジェクトを作成します。

$ play new testapp
       _            _
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/

play! 2.0.4, http://www.playframework.org

The new application will be created in /home/misty/dev/scala/testapp

What is the application name?
> testapp

Which template do you want to use for this new application?

  1 - Create a simple Scala application
  2 - Create a simple Java application
  3 - Create an empty project

> 1

OK, application testapp is created.

Have fun!

$

はい、できました。現在、defaultのSkeletonでプロジェクトを作成するとこんな感じです。次にensimeで使うためにPlay自体を解析してensime上から補完出来るようにします。その為に、作成したプロジェクトディレクトリ内のproject/plugins.sbtにpluginの設定します。

$ cat plugins.sbt
// Comment to get more information during initialization
logLevel := Level.Warn

// The Typesafe repository
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

// Use the Play sbt plugin for Play projects
addSbtPlugin("play" % "sbt-plugin" % "2.0.4")

addSbtPlugin("org.ensime" % "ensime-sbt-cmd" % "0.1.0")  // <-これ

ここまでしたらplay consoleを起動してensime generateを実行します。

[testapp] $ ensime generate
[info] Gathering project information...
[info] Processing project: ProjectRef(file:/home/misty/Develop/scala/testapp/,testapp)...
[info]  Reading setting: name...

(略)

[info]  Reading setting: class-directory...
[info]  Reading setting: ensime-config...
[info] Wrote configuration to .ensime
[testapp] $ 

これでensimeの利用準備完了です。

Emacs + Ensime

とりあえず先ほど作ったプロジェクトで作成されたソースを開こうとすると

error Not connected. M-x ensime to connect

みたいなことを言われるので

M-x ensime

を起動します。特に問題がなければうまいこと起動して使えるようになるはずです!続きは気が向いたら書きます。

mongoose-authでOAuth認証

今回はeveryauthという認証ロジックのラッパーライブラリではなく、サービス毎に纏めて対応した認証ライブラリを更にmongooseのプラグインとして使えるmongoose-authを使って認証させます(ややこしい)

ようするに個人的なメモです

用意するもの

  • node.js
  • mongoose
  • mongoose-auth

その前に

このmongoose-authは形式としてはeveryauthをmongooseから使うためのpluginという形式であるため、できれば前もってeveryauthの知識はある程度欲しいところです。

実際のところ、READMEにあるサンプルが動かせれば大体できます(おぃ)。ただREADMEに無い部分でわかりにくい部分があるのでそのへんをサンプルで書いてみます

Schemaの拡張

mongoose-authを使うとgithubならgithubモジュール、facebookならfacebookモジュールを使うので、予め用意されたUserSchemaをベースにDBに格納されます

mongoose-auth:lib/modules/github/schema.js
module.exports = {
    github: {
        id: Number
      , login: String
      , gravatarId: String
      , name: String
      , email: String
      , publicRepoCount: Number
      , publicGistCount: Number
      , followingCount: Number
      , followersCount: Number
      , company: String
      , blog: String
      , location: String
      , permission: String
      , createdAt: Date

      // Private data
      , totalPrivateRepoCount: Number
      , collaborators: Number
      , diskUsage: Number
      , ownedPrivateRepoCount: Number
      , privateGistCount: Number
      , plan: {
            name: String
          , collaborators: Number
          , space: Number
          , privateRepos: Number
        }
    }
  , 'github.type': String
};

このままでもまあ最低限必要な情報は確保されているんですが、ここに追加で属性追加したいなぁ、なんて思いますよね。

var mongoose = require('mongoose'),
    mongooseAuth = require('mongoose-auth'),
    Schema = mongoose.Schema,
    ObjectId = mongoose.Schema.ObjectId;

// 共通項目としてdisplayNameという属性を追加する
var UserSchema = new Schema({
    displayName: {type:String, default: null}
});

こうしておくと各モジュールのSchemaで追加した項目がmongoose経由で更新できるようになります。

findOrCreateUserの設定

例えば、普通に利用する場合は以下のようなソースで動きます

    var mongoose = require('mongoose')
      , Schema = mongoose.Schema
      , mongooseAuth = require('mongoose-auth');

    var UserSchema = new Schema({})
      , User;

    // STEP 1: Schema Decoration and Configuration for the Routing
    UserSchema.plugin(mongooseAuth, {
        // Here, we attach your User model to every module
        everymodule: {
          everyauth: {
              User: function () {
                return User;
              }
          }
        }
      , github: {
          everyauth: {
              myHostname: 'http://localhost:3000'
            , appId: 'YOUR APP ID HERE'
            , appSecret: 'YOUR APP SECRET HERE'
            , redirectPath: '/'
          }
        }
    });
...

これはREADMEからの抜粋ですが、UserSchemaに対するpluginとしてgithubの設定項目を記述するだけです。これは実際にはeveryauthのgithubモジュールに対する設定に直結してるだけです。everyauthではfindOrCreateUser関数を実装している訳ではありませんが、everyauthのOAuth2の実装部分でfindOrCreateUserを認証シーケンスのstepとして呼び出しています。

everyauth:lib/modules/oauth2.js
  .get('callbackPath',
       'the callback path that the 3rd party OAuth provider redirects to after an OAuth authorization result - e.g., "/auth/facebook/callback"')
    .step('getCode')
      .description('retrieves a verifier code from the url query')
      .accepts('req res')
      .promises('code')
      .canBreakTo('authCallbackErrorSteps')
    .step('getAccessToken')
      .accepts('code')
      .promises('accessToken extra')
    .step('fetchOAuthUser')
      .accepts('accessToken')
      .promises('oauthUser')
    .step('getSession')
      .accepts('req')
      .promises('session')
    .step('findOrCreateUser') //<- ココ
      //.optional()
      .accepts('session accessToken extra oauthUser')
      .promises('user')
    .step('compile')
      .accepts('accessToken extra oauthUser user')
      .promises('auth')
    .step('addToSession')
      .accepts('session auth')
      .promises(null)
    .step('sendResponse')
      .accepts('res')
      .promises(null)

これを利用してfindOrCreateUser関数を定義として実装してやると認証完了時に自動的にfindOrCreateUserを実行できます。所謂hook的な物です。mongoose-authではdefaultの定義として予めfindOrCreateUser関数が定義されています。

mongoose-auth:lib/modules/github/everyauth.js
// Defaults
module.exports = {
  findOrCreateUser: function (sess, accessTok, accessTokExtra, ghUser) {
    var promise = this.Promise()
      , self = this;
    // TODO Check user in session or request helper first
    //      e.g., req.user or sess.auth.userId
    this.User()().findOne({'github.id': ghUser.id}, function (err, foundUser) {
      if (foundUser)
        return promise.fulfill(foundUser);
      self.User()().createWithGithub(ghUser, accessTok, function (err, createdUser) {
        return promise.fulfill(createdUser);
      });
    });
    return promise;
  }
};

したがってこの挙動を変えたい場合はこいつをoverrideする定義を追加してやれば良い訳です。

UserSchema.plugin(mongooseAuth, {
    everymodule: {
        everyauth: {
            User: function() {
                return User;
            },
            handleLogout: function(req, res) {
                req.logout();
                res.writeHead(303, {'Location': this.logoutRedirectPath()});
                res.end();
            }
        }
    },
    github: {
        everyauth: {
            myHostname: conf.myHostname,
            appId: conf.github.oauth.appId,
            appSecret: conf.github.oauth.appSecret,
            scope: 'user,public_repo,repo,gist',
            redirectPath: '/',
            // ココから
            findOrCreateUser: function(session, accessTok, accessTokExtra, ghUser) {
                var promise = this.Promise(),
                    User = this.User()();
                // mongodbから該当するidの情報を検索
                User.findOne({'github.id': ghUser.id}, function(err, foundUser) {
                    if(err) return promise.fail(err);
                    if(foundUser) return promise.fulfill(foundUser);

                    // 無い場合はUserSchemaを元に作成
                    console.log('CREATE GITHUB USER');
                    User.createWithGithub(ghUser, accessTok, function(err, createUser) {
                        if(err) return promise.fail(err);
                        // Schemaで定義した追加属性に値をセット
                        createUser.displayName = ghUser.name;
                        // 保存
                        createUser.save(function(err, modUser) {
                            if(err) return promise.fail(err);
                            return promise.fulfill(modUser);
                        });
                    });
                });
                return promise;
            }
        }
    }
});

使い道としては、例えば認証後のaccessTokenをsessionに入れておくとか(いいか悪いかは別にして…)、別のschemaを更新したりまあいろいろ使い道はあるかもしれません。