まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:customize

IBM Cloud から提供されているサービスの1つである AppID は、オンラインサインアップやパスワード忘れなどにも対応したログイン機能全般を簡単にアプリに組み込める形で提供されているサービスです。

自分も何度か使ったことがあります。Node.js 向けに提供されている SDK を使うと、サービスの属性値や認証後のコールバック URL をいくつか設定するだけで、あらかじめ用意された UI を使った認証機能を独自アプリケーション内に組み込んだり、認証後のユーザー情報を簡単に取得することができます。多くのアプリケーションではユーザー情報やユーザー認証が必要になるのですが、いちいちそのアプリケーション向けに開発する手間が省けるため、とても有用に活用しています。


ただ、唯一の難点だったのが「あらかじめ用意された UI」でした。カスタマイズできる要素が非常に少なく(ロゴ画像を1つ指定できるくらい)、アプリケーションのUIを頑張って作っても「ログイン時だけ他のページと違うテイストのUI」になってしまうことが気になっていました。

このUI画面もカスタマイズできるらしい、という噂は聞いていたのですが、軽く調査した限りでは具体的な情報がなくわからず、つまりある程度自分でトライ&エラーしながら調べる必要がありそうな内容だったのですが、ちゃんと調べる時間が取れずに放っておいてしまいました。

この度、自分の興味が再度高まったこともあって、改めてこの App ID UI のカスタマイズについて調べてみました。どうやら実現できそうな目処が付いたので、本ブログエントリの形でその方法を紹介したいと思います。なお以下は Node.js + Express というフレームワーク環境を前提としていますが、SDK の対応言語であればおそらく他の言語やフレームワークでも大丈夫だと思います。


【AppID の準備】
何はともあれ AppID サービスインスタンスを用意して、アプリケーション開発用の属性情報を取得する必要があります。まずは開発前の AppID 側の準備作業を紹介します。

IBM Cloud にログインし、「リソースの作成」から "App ID" を検索して、見つけたらクリックします:
2021030304


ロケーション(東京とか)とプラン(ライトであれば2段階認証が使えないなどの制約がありますが無料です)を選択して「作成」します:
2021030305


少し待つとインスタンスが作成され、App ID が利用可能な状態になります:
2021030306


作成したこの App ID を外部から利用するための属性情報を取得します。「サービス資格情報」タブを選択し、「新規資格情報」ボタンをクリックして、資格情報を1つ作成します。作成後に資格名(「サービス資格情報-1」などになっているはずです)をクリックすると、資格情報が JSON フォーマットで表示されます:
2021030303


資格情報は概ねこのような内容になっています(***** 部は伏せ字)。このうち、以下5つの値は後で利用するのでメモしておくか、コピペできるようにしておきます:
{
  "apikey": "********************",
  "appidServiceEndpoint": "********************",
  "clientId": "********************",
  "discoveryEndpoint": "********************",
  "iam_apikey_description": "Auto-generated for key ********************",
  "iam_apikey_name": "サービス資格情報-1",
  "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",
  "iam_serviceid_crn": "crn:v1:bluemix:public:iam-identity::a/********************::serviceid:ServiceId-********************",
  "managementUrl": "https://jp-tok.appid.cloud.ibm.com/management/v4/********************",
  "oauthServerUrl": "https://jp-tok.appid.cloud.ibm.com/oauth/v4/********************",
  "profilesUrl": "https://jp-tok.appid.cloud.ibm.com",
  "secret": "********************",
  "tenantId": "********************"
}


後で使う値確認方法
regionprofilesUrl などのURL文字列の .appid.cloud.ibm.com の前に付いている部分(上の例だと jp-tok)
tenantIdtenantId キーの値
apiKeyapikey キーの値
secretsecret キーの値
clientIdclientId キーの値


次に AppID 自体を利用可能な状態にしておきます。まずログイン時に利用するにはユーザーが登録されている必要があるので、あらかじめユーザーを登録しておきます。

※ AppID 自体がオンラインサインアップの機能を持っているのでオンラインサインアップでユーザー登録することも可能ですが、その説明は今回のブログの目的ではないのと、ここで説明する内容を完結にしたい目的もあってここでは紹介しません。今回はオンラインサインアップや外部 SNS アカウントを用いたログイン、パスワード忘れ時の対応等、ログインに無関係な機能を OFF にして説明します。もちろんこれらの機能もカスタム UI で同様に提供することは可能ですが、今回の説明では対象外とします。

AppID のダッシュボード画面から「クラウド・ディレクトリー」-「ユーザー」を選択し、「ユーザーの作成」ボタンをクリックします:
2021030303


名前(名と姓、表示用)、Eメール(ログインIDとなります)、パスワードを入力して「保存」します。なおオンラインサインアップでアカウントを作成した場合はメール認証が必要ですが、この直接作成する方法でユーザーを作成した場合は保存後すぐに有効なアカウントとして(メール認証なしに)登録されます:
2021030304


ユーザーが登録されました。(今回の検証では1つあれば充分ですが)必要に応じて同じ手順でユーザーを追加してください:
2021030305


最後に App ID 自体の挙動を構成します。今回はオンラインサインアップやパスワード忘れ時の対応などをすべて無効にして、登録済みユーザーのログイン機能だけを提供するように構成します。

最初に認証時のリダイレクト URL を指定します。実はこのリダイレクト URL の指定が必要なのはカスタマイズしない標準のログイン UI を使う場合のみで、独自 UI を使う際には指定の必要がないものですが、今回は比較のためカスタマイズしない場合でのログインも行うのでここでリダイレクト URL も設定しておきます。

ダッシュボード画面から「認証の管理」を選択し、「認証設定」タブ内の「Web リダイレクト URL の追加」からリダイレクト先を追加します。今回のデモでは localhost で動かすことを想定したリダイレクト先が http://localhost:8080/appid/callback となるサンプルを利用するので、この URL を指定して追加します:
2021030306


次に不要な機能を無効にします。まずは SNS サインイン機能を無効にします。ダッシュボード画面で「認証の管理」から「ID プロバイダー」を選び、「クラウド・ディレクトリー」だけを有効にします(他をすべて無効にします):
2021030308


最後にダッシュボードの「設定」を選択し、「ユーザーがアプリケーションをサインアップできるようにする」、「ユーザーがアプリケーションからアカウントを設定できるようにする」を両方とも「いいえ」に設定します:
2021030307


これで App ID 側の準備はすべて完了しました。ではこの App ID を使ったログイン機能を試してみましょう。


【カスタマイズなしの場合】
本エントリは AppID の UI をカスタマイズする方法を紹介することが主目的ですが、比較の意味も含めて、まずはカスタマイズせずに標準UIを使ってログインする場合のアプリケーションサンプルを紹介します。

サンプルはこちらに用意しました。実際に試す場合は Node.js を導入したシステムにダウンロードして展開するか、git clone するなどしてソースコードを手元に用意してください:
https://github.com/dotnsf/appid_customui

2021030309


ソースコードの準備ができたら、展開後のディレクトリで一度 npm install を実行して、依存ライブラリをまとめて導入しておきます:
$ cd appid_customui
$ npm install

そしてテキストエディタで settings.js を開き、2~6行目までの5箇所に上述の資格情報から取得した値を入力します。8行目の exports.redirectUri の値は既定値以外のコールバック URL を設定した場合のみ、その値に変更してください。変更が完了したら保存します:
//. IBM App ID
exports.region = '(上述の region の値)';
exports.tenantId = '(上述の tenantId の値)';
exports.apiKey = '(上述の apikey の値)';
exports.secret = '(上述の secret の値)';
exports.clientId = '(上述の clientId の値)';

exports.redirectUri = 'http://localhost:8080/appid/callback';
exports.oauthServerUrl = 'https://' + exports.region + '.appid.cloud.ibm.com/oauth/v4/' + exports.tenantId;

コードの説明をする前に一度実際に動かして挙動を確認してみましょう。以下のコマンドでアプリケーションを起動します:
$ node app

App ID の初期化などで少し時間がかかりますが、10 秒ほどすると "server starting on 8080 ..." というメッセージが表示され、起動が完了します。このメッセージを確認したらウェブブラウザで以下のアドレスを開きます:
http://localhost:8080/

アプリケーションが正しく起動していれば、App ID が提供するログインページに転送され、以下のような標準のログイン画面が表示されます(ロゴを加える程度のカスタマイズは可能ですが、ちと味気ないですよね。。):
2021030303


ID とパスワードに、先程 App ID のユーザー登録を行った際のメールアドレス及びパスワードを指定して "Sign in" ボタンをクリックします:
2021030304


正しいメールアドレスとパスワードの組み合わせが入力されていればログインが成功します。ログインに成功すると改めて http://localhost:8080/ に転送され、ログインしたユーザーの名前とログアウトボタンだけが表示される画面に遷移します。これがログイン後のトップページとなります(この画面は、もうちょっと見た目がんばることができると思ってはいますが、、自分で自由にデザインできます):
2021030305


このサンプルではトップページ以外に遷移するページはありません。"Logout" ボタンをクリックするとログアウトして再度 http://localhost:8080/ へ遷移します。が、今度はログインしていないので再び標準ログイン画面のページに転送されます。ここまでが App ID 標準ログイン画面を使ったデモの挙動です:
2021030303


ここまでの(標準ログイン画面を使ったログインの)挙動をコードでどのように実現しているかを説明しておきます。まず node コマンドで起動した app.js の全容はこのようになっています:
//. app.js
var express = require( 'express' ),
    bodyParser = require( 'body-parser' ),
    ejs = require( 'ejs' ),
    passport = require( 'passport' ),
    session = require( 'express-session' ),
    WebAppStrategy = require( 'ibmcloud-appid' ).WebAppStrategy,
    app = express();

var settings = require( './settings' );

//. setup session
app.use( session({
  secret: 'appid_normalui',
  resave: false,
  saveUninitialized: false
}));

app.use( bodyParser.urlencoded( { extended: true } ) );
app.use( bodyParser.json() );
app.use( express.Router() );
app.use( express.static( __dirname + '/public' ) );

app.set( 'views', __dirname + '/views' );
app.set( 'view engine', 'ejs' );


//. setup passport
app.use( passport.initialize() );
app.use( passport.session() );
passport.use( new WebAppStrategy({
  tenantId: settings.tenantId,
  clientId: settings.clientId,
  secret: settings.secret,
  oauthServerUrl: settings.oauthServerUrl,
  redirectUri: settings.redirectUri
}));
passport.serializeUser( ( user, cb ) => cb( null, user ) );
passport.deserializeUser( ( user, cb ) => cb( null, user ) );


//. login UI(**カスタムログインページでは変更)
app.get( '/login', passport.authenticate( WebAppStrategy.STRATEGY_NAME, {
  successRedirect: '/',
  forceLogin: true,
}));

//. logout
app.get( '/appid/logout', function( req, res ){
  WebAppStrategy.logout( req );
  res.redirect( '/' );
});

//. callback(*カスタムログインページでは不要)
app.get( '/appid/callback', function( req, res, next ){
  next();
}, 
  passport.authenticate( WebAppStrategy.STRATEGY_NAME )
);

//. ログイン済みでないとトップページが見れないようにする
app.all( '/*', function( req, res, next ){
  if( !req.user || !req.user.sub ){
    //. ログイン済みでない場合は強制的にログインページへ
    res.redirect( '/login' );
  }else{
    next();
  }
});

//. トップページ
app.get( '/', function( req, res ){
  //. 正しくユーザー情報が取得できていれば、トップページでユーザー情報を表示する
  if( req.user ){
    res.render( 'index', { user: req.user } );
  }else{
    res.render( 'index', { user: null } );
  }
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

情報量としてはあまり多くありませんが、ログイン後の画面となる views/index.ejs の内容も載せておきます。Logout ボタンをクリックすると GET /appid/logout が実行されるようになっていることと、その下にログインユーザーの名前(user.name)が表示されるテンプレートになっています:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/js/bootstrap.min.js"></script>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="App ID Custom UI"/>
<title>App ID Custom UI</title>
<script>
</script>
<style type="text/css">
</style> 
</head>
<body>

<div class="container">
  <div>
    <a class="btn btn-warning" href="/appid/logout">Logout</a>
  </div>

  <% if( user && user.name ){ %>
  <h2><%= user.name %></h2>
  <% }else{ %>
  <h2>(App ID)</h2>
  <% } %>
</div>

</body>
</html>


具体的な詳しい内容はこのコードを見ていただくとして、この中には以下の4つの HTTP リクエストに対するルーティング(と1つのルール)が含まれています:
#HTTP リクエスト挙動
1GET /loginApp ID標準ログイン画面の表示。ログインが成功すると GET / (#5)の処理へ転送されます。
2GET /appid/logoutログアウト処理。その後 GET / (#5) を実行しようとするが、この時点ではログインしていないので、#4 のルールによって GET /login (#1) へ転送されて標準ログイン画面を表示します。
3GET /appid/callback標準ログイン画面のコールバック関数。正しいログイン組み合わせの情報が送信された場合は next()(処理続行)となり、間違った組み合わせの場合は再度ログイン画面に戻ります。
4ALL /*(ここから下のルーティングすべてに適用するルール)ログインされていない場合は強制的に GET /login (#1) へリダイレクトして標準ログイン画面に移動します。
5GET /index.ejs を使ってトップ画面を表示する。#4 のルールがあるのでログイン後の場合のみ表示します。


先程確認した挙動をもう一度このリストを使って順に説明します。まずブラウザで http://localhost:8080/ にアクセスすると #5 のルーティングが処理される、、ように見えますが、その前に #4 が定義されています。最初はログインしていない状態で実行されるため、まずは GET /login( #1 の処理)へ転送され、App ID の標準ログイン画面が表示されます。

ログイン画面で正しい情報を入力して Sign in ボタンを押すと、ログイン済みになった上で #3 へ移動し、処理が続行(#1)され 、ログイン済みなので改めて #5 (http://localhost:8080/)へ転送されます。

最初と同じ #5 のルーティングが処理されますが、今回はログイン済みのため #4 のルールは適用されません。そのまま #5 の処理が実施され、ログイン後の画面が表示されることになります。この後のオペレーションはすべて「ログイン済み」な状態として行われることになります:
2021030404


ログイン後のトップ画面で logout ボタンを押すと #2 が実行され、ログイン情報が消えた状態になってから再度 http://localhost:8080/ へ転送され(つまり最初に戻り)、標準のログイン画面が表示されます。


ちょっと長くなりましたが、これが App ID の標準ログイン画面を使った時のログイン~ログアウトまでのルーティングとその遷移となります:



【UI をカスタマイズする場合】
標準ログイン画面を使った場合の状態遷移がわかった所で、改めてログイン画面の独自カスタマイズについて紹介します。こちらもまずは一度動かしてみることにしましょう。以下のコマンドでアプリケーションを起動します(先程は app.js を起動しましたが、こちらでは app_customlogin.js を起動します):
$ node app_customlogin

App ID の初期化などで少し時間がかかりますが、10 秒ほどすると "server starting on 8080 ..." というメッセージが表示され、起動が完了します。このメッセージを確認したらウェブブラウザで以下のアドレスを開きます:
http://localhost:8080/

アプリケーションが正しく起動していれば、先程のような App ID 標準ログイン画面とは異なる、以下のような独自のログイン画面が表示されます:
2021030303


App ID にユーザー登録した際の username(メールアドレス)と password(パスワード)の組み合わせを入力して "Login" ボタンをクリックします:
2021030304


正しい組み合わせが入力できていれば、ログイン後の画面(名前が表示されている画面)に遷移します。そして "Logout" ボタンをクリックします:
2021030305


するとログアウトが行われ、あらためて元のカスタマイズされたログイン画面に遷移します:
2021030303


カスタマイズされたログイン画面を使う場合であっても、App ID に登録されたユーザー情報を用いて標準ログイン画面の時と同様の挙動や状態遷移が行われることを確認できました。

ではこちらのコードを確認してみます。起動した app_customlogin.js の内容は以下のようになっています:
//. app_customlogin.js
var express = require( 'express' ),
    bodyParser = require( 'body-parser' ),
    ejs = require( 'ejs' ),
    passport = require( 'passport' ),
    session = require( 'express-session' ),
    WebAppStrategy = require( 'ibmcloud-appid' ).WebAppStrategy,
    app = express();

var settings = require( './settings' );

//. setup session
app.use( session({
  secret: 'appid_customui',
  resave: false,
  saveUninitialized: false
}));

app.use( bodyParser.urlencoded( { extended: true } ) );
app.use( bodyParser.json() );
app.use( express.Router() );
app.use( express.static( __dirname + '/public' ) );

app.set( 'views', __dirname + '/views' );
app.set( 'view engine', 'ejs' );


//. setup passport
app.use( passport.initialize() );
app.use( passport.session() );
passport.use( new WebAppStrategy({
  tenantId: settings.tenantId,
  clientId: settings.clientId,
  secret: settings.secret,
  oauthServerUrl: settings.oauthServerUrl,
  redirectUri: settings.redirectUri
}));
passport.serializeUser( ( user, cb ) => cb( null, user ) );
passport.deserializeUser( ( user, cb ) => cb( null, user ) );


//. login UI(**カスタムログインページで変更)
app.get( '/login', function( req, res ){
  res.render( 'login', {} );
});

//. logout
app.get( '/appid/logout', function( req, res ){
  WebAppStrategy.logout( req );
  res.redirect( '/login' );
});

//. login submit(***カスタムログインページで追加)
app.post( '/appid/login/submit', bodyParser.urlencoded({extended: false}), passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
  successRedirect: '/',
  failureRedirect: '/login',
  failureFlash : true
}));

//. ログイン済みでないとトップページが見れないようにする
app.all( '/*', function( req, res, next ){
  if( !req.user || !req.user.sub ){
    //. ログイン済みでない場合は強制的にログインページへ
    res.redirect( '/login' );
  }else{
    next();
  }
});

//. トップページ
app.get( '/', function( req, res ){
  //. 正しくユーザー情報が取得できていれば、トップページでユーザー情報を表示する
  if( req.user ){
    res.render( 'index', { user: req.user } );
  }else{
    res.render( 'index', { user: null } );
  }
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

↑わざと色を付けています。は app.js から変更した部分、は app.js にはなくて追加した部分です。また app.get( '/appid/callback', function( req, res, next ){ ... で始まるコールバック処理の部分は、カスタムログイン画面を使う場合は不要なので削除しています。実はほぼ変わっていなくて、削除した部分も含めて大きく3箇所しか変わっていないことがわかると思います。

加えて、カスタムログイン画面となる views/login.ejs の内容もここに記載しておきます。index.ejs 同様、Bootstrap を使っていますが、ごくシンプルな内容の HTML になっています。特徴として入力した username と password の値が上で新たに追加した POST /appid/login/submit にポストするように作られていることがわかります:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/js/bootstrap.min.js"></script>
<link href="//stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="App ID Custom UI"/>
<title>App ID Custom UI - Login</title>

<style type="text/css">
</style> 

<script>
</script>

</head>
<body>

<div class="container">
  <form method="POST" action="/appid/login/submit">
    <table class="table table-bordered">
      <tbody>
        <tr><th>username</th><td><input type="text" class="form-control" name="username" id="username" value=""/></td></tr>
        <tr><th>password</th><td><input type="password" class="form-control" name="password" id="password" value=""/></td></tr>
      </tbody>
    </table>
    <input type="submit" class="btn btn-primary" value="Login"/>
  </form>
</div>

</body>
</html>


このカスタムログイン画面を使うケースでも、HTTP リクエストとルーティングの関係を表にしてみました:
#HTTP リクエスト挙動
1GET /loginカスタムログイン画面の表示。ログインが成功すると GET / (#5)の処理へ転送されます。
2GET /appid/logoutログアウト処理。その後 GET / (#5) を実行しようとするが、この時点ではログインしていないので、#4 のルールによって GET /login (#1) へ転送されてカスタムログイン画面を表示します。
3POST /appid/login/submitカスタムログイン画面で username と password を入力して Login ボタンを押した時に実行される関数。正しいログイン組み合わせの情報が送信された場合は GET / (#5) が実行されてトップ画面へ、間違った組み合わせの場合は GET /login (#1) が実行され、再度ログイン画面に戻ります。
4ALL /*(ここから下のルーティングすべてに適用するルール)ログインされていない場合は強制的に GET /login (#1) へリダイレクトしてカスタムログイン画面に移動します。
5GET /index.ejs を使ってトップ画面を表示する。#4 のルールがあるのでログイン後の場合のみ表示します。


まずブラウザで http://localhost:8080/ にアクセスすると App ID の標準ログイン画面を使った時と同様に(最初はログインしていない状態で実行されるため)まずは GET /login( #1 の処理)へ転送されます。その結果、カスタムログイン画面が表示されます。

ログイン画面で正しい情報を入力して Sign in ボタンを押すと、#3 の POST /appid/login/submit が実行されます。情報が正しい場合、ログイン済みになった上で #5 へ移動します。今度はログイン済みなのでログイン後の画面が表示されることになります。この後のオペレーションはすべて「ログイン済み」な状態として行われることになります:


2021030404


というわけで、英語でもあまり情報の見つかられなかった App ID のカスタムログインページ作成方法についてまとめてみました。


なお、本ブログエントリと、ここで紹介したサンプルを作る上で参考にしたのはこの Github リポジトリです。使い方の説明が丁寧にかかれているわけではないのですが、カスタムログインページだけでなく、パスワード忘れ時の対応画面など、一通りの App ID カスタム画面を開発するためのサンプルが含まれている(と思います):
https://github.com/ibm-cloud-security/appid-serversdk-nodejs


先日、IBM Bluemix から提供されている IBM Watson の翻訳 API である "Language Translator" サービスが待望の日本語対応を果たしました。現在、日本語翻訳機能としては英語→日本語、および日本語→英語のテキスト翻訳が可能になっています:
2016122808


この翻訳 API は文章の翻訳に加え、入力されたテキストが何語のテキストなのかを識別する機能も備えています。実際にどんな言語翻訳ができるのか、そのサンプル的なアプリケーションが以下で公開されているので、興味ある方は試してみてください:
https://language-translator-demo.mybluemix.net/

2016122809



さて、この IBM Watson 翻訳 API の特徴の1つが「カスタマイズ機能」です。デフォルトで提供される翻訳機能をベースに、自分なりのカスタマイズを加えることができるようになっています。これは日本語翻訳機能においても有効な機能です。

というわけで、早速カスタマイズを試してみましょう。ただし、このカスタマイズを使うには、Language Translator サービスは標準プランではなく、拡張プランを選択する必要があります。標準プランには無料枠が用意されていますが、拡張プランにはありません。サインアップから 30 日間の無料期間を過ぎていたり、既に有償プランに移行済みの場合は、サービスインスタンスを作成した段階でインスタンス料金が発生しますので、ご注意ください:
2016122801


では「拡張プラン」を選択していることを確認した上でインスタンスを「作成」します:
2016122802


こうして拡張プランの Language Translator サービスインスタンスを作りました。この後、実際に API を使うことになるので「サービス資格情報」の「資格情報の表示」からクレデンシャル情報を調べてメモしておいてください(繰り返しですが、拡張プランの資格情報です。流出すると勝手に使われて課金額がどんどん増えていくことになるので、取扱いに注意してください):
2016122803


次に実際にカスタマイズする内容を TMX(Transaction Memory eXchange) という翻訳メモリデータの標準フォーマットである XML ファイルで用意します。今回は以下の内容の osaka.tmx ファイルを手元で作って用意しました:
<tmx version="1.4">
  <header
    creationtool="MyTool" creationtoolversion="1.00"
    datatype="PlainText" segtype="sentence"
    adminlang="en-ja" srclang="en"
    o-tmf="MyTransMem"/>
  <body>
    <tu>
      <tuv xml:lang="en">
        <seg>Hello</seg>
      </tuv>
      <tuv xml:lang="ja">
        <seg>もうかりまっか</seg>
      </tuv>
    </tu>
    <tu>
      <tuv xml:lang="en">
        <seg>I am fine</seg>
      </tuv>
      <tuv xml:lang="ja">
        <seg>ぼちぼちでんな</seg>
      </tuv>
    </tu>
  </body>
</tmx>

(かなり偏見入ってますが・・)大阪弁のカスタマイズをするためのファイルです。英語の "Hello" を「もうかりまっか」に、"I am fine" を「ぼちぼちでんな」に変換する、という内容です。これを元々の英→日翻訳機能に上書きする形でカスタマイズする、という意味です。なお日→英の翻訳カスタマイズは今回の説明には含めませんが、同様にして(同様の TMX ファイルを別途用意する形で)カスタマイズすることは可能です。


これでカスタマイズの準備はできました。 が、カスタマイズの前に、カスタマイズ前の挙動を確認しておきます(後でカスタマイズ後の挙動と比較するためです)。ここからは curl コマンドを使って実際に API を使ってみるので、必要に応じてここなどから curl コマンドをインストールしておいてください。

まず最初に、Language Translator がデフォルトで用意しているモデル(日英とか、英日とか、英仏とか、・・)の一覧を取得してみます(username と password には上記の資格情報で取得したものを指定します。また黒字が入力コマンド、青字が出力結果です):
$ curl -u "username:password" "https://gateway.watsonplatform.net/language-translator/api/v2/models"

{
  "models":[
    {
      "model_id":"ar-en",
      "source":"ar",
      "target":"en",
      "base_model_id":"",
      "domain":"news",
      "customizable":true,
      "default_model":true,
      "owner":"",
      "status":"available",
      "name":"",
      "train_log":null
    },
      :
    {
      "model_id":"en-ja",
      "source":"en",
      "target":"ja",
      "base_model_id":"",
      "domain":"news",
      "customizable":true,
      "default_model":true,
      "owner":"",
      "status":"available",
      "name":"",
      "train_log":null
    },
      :
    {
      "model_id":"ja-en",
      "source":"ja",
      "target":"en",
      "base_model_id":"",
      "domain":"news",
      "customizable":true,
      "default_model":true,
      "owner":"",
      "status":"available",
      "name":"",
      "train_log":null
    },
      :
  ]
}

モデルの一覧が JSON 配列形式で出力されます。"model_id" の値が実際に翻訳する時やこの後のカスタマイズ時に翻訳の種類を指定する値になります。英日であれば "en-ja" 、日英であれば "ja-en" という値になります。

また各モデルの中に "customizable" という値があります。これは「このモデルをカスタマイズ可能か」を示した値であり、true になっているとカスタマイズ可能であることを示しており、このモデルに上書きする形でのカスタマイズが可能です(英日、日英ともに true です)。また "status" の値は現時点で利用可能な状態になっているかどうかを示しています(これも英日、日英ともに "available" となっているので使える状態になっています)。

ではカスタマイズ前の翻訳機能を試してみましょう。英語の "Hello" を日本語に翻訳してみます:
$ curl -u "username:password" "https://gateway.watsonplatform.net/language-translator/api/v2/translate?model_id=en-ja&text=Hello"

こんにちは

カスタマイズ前(model_id に既存の en-ja を指定)なので、期待通りの結果になりました。

ではここからはカスタマイズを加えてみましょう。上記で用意した osaka.tmx を指定して、以下のコマンドを実行します:
$ curl -u "username:password" -X POST -F base_model_id="en-ja" -F forced_glossary=@osaka.tmx "https://gateway.watsonplatform.net/language-translator/api/v2/models"

{
  "model_id":"be81b082-1292-47ac-badf-0beb83846f66",
  "source":"en",
  "target":"ja",
  "base_model_id":"en-ja",
  "domain":"news",
  "customizable":false,
  "default_model":false,
  "owner":"da0dc7f3-87c6-4d82-b965-79b463266291",
  "status":"dispatching",
  "name":null,
  "train_log":null
}

基本モデル(base_model_id)を英日(en-ja)にして、そこに osaka.tmx で指定した内容を上書きでカスタマイズする、という内容のコマンドです。実行結果には新しい model_id (be81b082-1292-47ac-badf-0beb83846f66)が付与されて返っています。また customizable 値は false になっているので、このモデルをベースに更にカスタマイズ、ということはできません。status は "dispatching" となっているので、この時点ではまだ内部処理中です。

少し待ってから再度ステータスを確認し、"available" となっていることを確認できたら翻訳も可能になります。
$ curl -u "username:password" "https://gateway.watsonplatform.net/language-translator/api/v2/models/be81b082-1292-47ac-badf-0beb83846f66"

{
  "model_id":"be81b082-1292-47ac-badf-0beb83846f66",
  "source":"en",
  "target":"ja",
  "base_model_id":"en-ja",
  "domain":"news",
  "customizable":false,
  "default_model":false,
  "owner":"da0dc7f3-87c6-4d82-b965-79b463266291",
  "status":"available",
  "name":null,
  "train_log":null
}

また、この段階でモデル一覧を再確認すると、カスタマイズしたこのモデルも一覧の先頭に含まれて表示されることがわかります:
$ curl -u "username:password" "https://gateway.watsonplatform.net/language-translator/api/v2/models"

{
  "models":[
    {
      "model_id":"be81b082-1292-47ac-badf-0beb83846f66",
      "source":"en",
      "target":"ja",
      "base_model_id":"en-ja",
      "domain":"news",
      "customizable":false,
      "default_model":false,
      "owner":"da0dc7f3-87c6-4d82-b965-79b463266291",
      "status":"available",
      "name":null,
      "train_log":null
    },
    {
      "model_id":"ar-en",
      "source":"ar",
      "target":"en",
      "base_model_id":"",
      "domain":"news",
      "customizable":true,
      "default_model":true,
      "owner":"",
      "status":"available",
      "name":"",
      "train_log":null
    },
      :
    {
      "model_id":"en-ja",
      "source":"en",
      "target":"ja",
      "base_model_id":"",
      "domain":"news",
      "customizable":true,
      "default_model":true,
      "owner":"",
      "status":"available",
      "name":"",
      "train_log":null
    },
      :
    {
      "model_id":"ja-en",
      "source":"ja",
      "target":"en",
      "base_model_id":"",
      "domain":"news",
      "customizable":true,
      "default_model":true,
      "owner":"",
      "status":"available",
      "name":"",
      "train_log":null
    },
      :
  ]
}

ではこのカスタマイズモデルを model_id に指定して、先程と同じ英語を翻訳してみます:
$ curl -u "username:password" "https://gateway.watsonplatform.net/language-translator/api/v2/translate?model_id=be81b082-1292-47ac-badf-0beb83846f66&text=Hello"

もうかりまっか

期待通りのカスタマイズが有効な変換結果になりました。なお "I am fine" でもカスタマイズした結果が翻訳されますが、それ以外の(カスタマイズしていない)テキストについてはベースとなっている英日翻訳が有効になって実行されます:
$ curl -u "username:password" "https://gateway.watsonplatform.net/language-translator/api/v2/translate?model_id=be81b082-1292-47ac-badf-0beb83846f66&text=I%20am%20fine"

ぼちぼちでんな

$ curl -u "username:password" "https://gateway.watsonplatform.net/language-translator/api/v2/translate?model_id=be81b082-1292-47ac-badf-0beb83846f66&text=Good%20evening"

こんばんは

カスタマイズモデルが不要になった場合は以下のコマンドで削除することができます:
$ curl -u "username:password" -X DELETE "https://gateway.watsonplatform.net/language-translator/api/v2/models/be81b082-1292-47ac-badf-0beb83846f66"

{"status":"OK"}

今回紹介したのはごくシンプルなカスタマイズでしたが、より多くの語彙を用意できれば方言や、或いは全く新しい言語の翻訳機能まで実現することができるかもしれません。そんなカスタマイズ機能が IBM Watson の Language Translator API には提供されている、という紹介でした。


なお、Language Translator API の詳しい関数リファレンスはこちらを参照ください:
https://www.ibm.com/watson/developercloud/language-translator/api/v2/

実際に挙動をためせる Watson API Explorer はこちら:
https://watson-api-explorer.mybluemix.net/apis/language-translator-v2#/


#TMX をもう少し楽に作れるツールとかないかな・・・


IBM Notes の起動時に表示されるスプラッシュスクリーンをカスタマイズする方法を紹介します:
2015091801
 (↑これがスプラッシュスクリーン)


やり方は色々あって、「きちんと」対応しようとすると結構面倒なこともあるんですが、てっとり早く作るにはこれかなあ、という方法を紹介します。

まずはビットマップ(Windows BMP)形式のスクリーン画像を用意します。今回はこれを使います:
2015091802
(注 この画像↑自体は PNG 形式なので、保存してもそのままでは使えません。お手元の環境で BMP 形式に変換してから使ってください)

なお、ここで用意する画像のサイズがあまり小さすぎると、パスワードプロンプトメッセージがうまく表示できなかったりするので、適宜調整してください。ちなみに↑上記の画像のサイズは 640x480 です。

この BMP 画像ファイルを適当なフォルダに保存します(C:\IBM\Notes\shojoji.bmp に保存したとします)。そしていったんノーツを閉じて、notes.ini ファイルをテキストエディタで開き、最後に以下の2行を追加します:
  :
  :
SESPlashPath=C:\IBM\Notes\shojoji.bmp
HasNotesOverlay=1

この状態で notes.ini を保存して、改めてノーツを起動するとこんな感じになります。指定した画像がスプラッシュスクリーンになって、パスワード入力待ちになっていることが確認できます:
2015091803


スプラッシュスクリーンをよく見ると分かるのですが、画像左下にパスワードプロンプトメッセージが表示されていたりします。画像サイズによってはここが崩れてしまったり、(今回のように)見難い位置に表示されてしまったりする可能性があるので、そのあたり上手く調整する必要があります。


業務でノーツをお使いの皆様、仕事中の気分転換にどうぞ。

なお、「ちゃんとした方法」はこちらをどうぞ:
http://www-01.ibm.com/support/docview.wss?uid=swg21962056


Tomcat や Jetty などの Java ウェブアプリケーションでエラーが発生した時に表示されるエラーページを独自のものにカスタマイズする方法の紹介です。もちろんエラーが発生しないことが望ましいのですが、万が一エラーが発生してしまった場合に、標準のエラーメッセージが出てしまうと、どのアプリケーションサーバーを使っているのかが分かってしまいます。その結果、そのサーバーのセキュリティホールを狙われる可能性もないわけではありません。あとエラーページにユーモアを交えるような目的でも有用だと思います。

ではその手順の紹介です。 まずアプリケーションの web.xml の最後に以下の青字の情報を追加します:
  :
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

  <error-page>
    <error-code>404</error-code>
    <location>/error/404.html</location>
  </error-page>
  <error-page>
    <error-code>500</error-code>
    <location>/error/500.jsp</location>
  </error-page>
</web-app>

この例ではステータスコードが 404(ページが見つからないエラー)の時と、500(サーバー内部エラー)の時用のページをカスタマイズする前提で、その2つのページに関する情報を追加しています。他のエラー(認証など)についてもカスタマイズする場合は同様に追加してください。この例ではページが見つからない場合は /error/404.html に、Java コード等での内部エラー発生時には /error/500.jsp にそれぞれ飛ばすような指定をしています。もちろん静的な HTML ページでもいいのですが、今回は 500 エラーの時には JSP にして動的に作成してみます。

そしてこれらのページを作っていきます。/error/404.html は 404 エラー、つまり URL が間違っていることになるので、こんな感じの内容で:
<html>
<head>
<title>404</title>
</head>
<body>
<h1>見つからないよ~</h1>
</body>
</html>

一方、/error/500.jsp は 500 エラー、つまり Java の内部エラーなので、スタックトレースも表示するような内容にしてみます。試しにこんな感じにしました。また isErrorPage を true にしています:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="javax.servlet.*" %>
<% request.setCharacterEncoding("utf-8"); %>
<%@ page isErrorPage="true"%>

<html>
<head>
<title>500</title>
</head>
<body>
<h1>Internal な Server の Error だよ~</h1>

<% exception.printStackTrace(new java.io.PrintWriter(out)); %>


</body>
</html>

最後に、わざと 500 エラーを発生させるための JSP ページを作成します:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="javax.servlet.*" %>
<% request.setCharacterEncoding("utf-8"); %>

<%
String[] a = null;
for( int i = 0; i < a.length; i ++ ){ //. ここでぬるぽエラー
}
%>

<html>
<head>
<title>index</title>
</head>
<body>
<h1>Index</h1>

<% exception.printStackTrace(new java.io.PrintWriter(out)); %>


</body>
</html>

この状態で動かしてみます。まず存在しないページの URL を叩くとこんな感じのエラーになります:
2015012706


次に上記で作成したわざと NullPointer エラーがでるページにアクセスするとこんな感じに:
2015012707


できた!
 

このページのトップヘ