Jersey を用いた Web API 作成 - その1

新人研修でJavaを学んでいるが、まだ座学がメインなので、勉強がてら手を動かしてみたいと思ったのが動機。
今の所、本家サイトに沿って進めています。

環境構築

最初に、以下がインスコされているか確認します。(括弧内は執筆当時の僕の環境)

使っているOSは macOS Sierra

プロジェクトの作成

以下のような mvn コマンドを実行します。

mvn archetype:generate -DarchetypeArtifactId=jersey-quickstart-grizzly2 \
-DarchetypeGroupId=org.glassfish.jersey.archetypes -DinteractiveMode=false \
-DgroupId=your.domain -DartifactId=your-service-name -Dpackage=your.domain \
-DarchetypeVersion=2.27

-DgroupId-DartifactId-Dpackage の右辺は自身のドメインやサービス名に置き換えてください。
上記のコマンドを実行すると以下のような構造のプロジェクトが作成されます。

your-service-name
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── your
    │           └── domain
    │               ├── Main.java
    │               └── MyResource.java
    └── test
        └── java
            └── your
                └── domain
                    └── MyResourceTest.java

とりあえず動かしてみる

mvn clean test を実行すると、テスト( your-service-name/test/java/your/domain/MyResourceTest.java に記述されているテスト)が実行され、ビルドが行われます。
ビルドに成功した場合、以下のようなログが出力されます。

[INFO] Scanning for projects...
Downloading from central: https://repo.maven.apache.org/maven2/org/glassfish/jersey/jersey-bom/2.27/jersey-bom-2.27.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/glassfish/jersey/jersey-bom/2.27/jersey-bom-2.27.pom (22 kB at 18 kB/s)
[INFO]
[INFO] ---------------------< your.domain:your-service-name >----------------------
[INFO] Building your-service-name 1.0-SNAPSHOT
[INFO] ----------------------------------[ jar ]-----------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/mojo/exec-maven-plugin/1.2.1/exec-maven-plugin-1.2.1.pom
...(中略)...
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running your.domain.MyResourceTest
4 14, 2018 9:20:03 午前 org.glassfish.grizzly.http.server.NetworkListener start
情報: Started listener bound to [localhost:8080]
4 14, 2018 9:20:03 午前 org.glassfish.grizzly.http.server.HttpServer start
情報: [HttpServer] Started.
4 14, 2018 9:20:03 午前 org.glassfish.grizzly.http.server.NetworkListener shutdownNow
情報: Stopped listener bound to [localhost:8080]
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.109 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 27.850 s
[INFO] Finished at: 2018-04-14T09:20:03+09:00
[INFO] ------------------------------------------------------------------------

上記のようなログが確認できたら mvn exec:java を実行します。
正常に実行できていれば以下のようなログが出力されるはずです。

[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< your.domain:your-service-name >----------------------
[INFO] Building your-service-name 1.0-SNAPSHOT
[INFO] ----------------------------------[ jar ]-----------------------------------
[INFO]
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) > validate @ your-service-name >>>
[INFO]
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) < validate @ your-service-name <<<
[INFO]
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ your-service-name ---
Downloading from central: https://repo.maven.apache.org/maven2/junit/junit/3.8.2/junit-3.8.2.pom
Downloaded from central: https://repo.maven.apache.org/maven2/junit/junit/3.8.2/junit-3.8.2.pom (747 B at 699 B/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-plugin-api/2.0/maven-plugin-api-2.0.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-plugin-api/2.0/maven-plugin-api-2.0.pom (601 B at 2.6 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven/2.0/maven-2.0.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven/2.0/maven-2.0.pom (8.8 kB at 35 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-exec/1.1/commons-exec-1.1.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-exec/1.1/commons-exec-1.1.pom (11 kB at 43 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-container-default/1.0-alpha-9/plexus-container-default-1.0-alpha-9.jar
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-exec/1.1/commons-exec-1.1.jar
Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-container-default/1.0-alpha-9/plexus-container-default-1.0-alpha-9.jar (195 kB at 345 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-exec/1.1/commons-exec-1.1.jar (53 kB at 73 kB/s)
4 14, 2018 9:20:19 午前 org.glassfish.grizzly.http.server.NetworkListener start
情報: Started listener bound to [localhost:8080]
4 14, 2018 9:20:19 午前 org.glassfish.grizzly.http.server.HttpServer start
情報: [HttpServer] Started.
Jersey app started with WADL available at http://localhost:8080/myapp/application.wadl
Hit enter to stop it...

上記のようなログが出てきたら http://localhost:8080/myapp/application.wadl へアクセスしてみましょう。
今回はcURLで叩いてみます。すると、以下のようなXMLが取得できました。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://wadl.dev.java.net/2009/02">
    <doc xmlns:jersey="http://jersey.java.net/" jersey:generatedBy="Jersey: 2.27 2018-04-10 07:34:57"/>
    <doc xmlns:jersey="http://jersey.java.net/" jersey:hint="This is simplified WADL with user and core resources only. To get full WADL with extended resources use the query parameter detail. Link: http://localhost:8080/myapp/application.wadl?detail=true"/>
    <grammars/>
    <resources base="http://localhost:8080/myapp/">
        <resource path="myresource">
            <method id="getIt" name="GET">
                <response>
                    <representation mediaType="text/plain"/>
                </response>
            </method>
        </resource>
    </resources>
</application>

また、上記のXMLの resources タグの中を見てみると、 http://localhost:8080/myapp/myresource にアクセスできそうな雰囲気がありますね。
http://localhost:8080/myapp/myresource にアクセスしてみると、 Got it! というプレーンテキストが取得できました。
ひとまず動作を確認することができました。

さいごに

今回は環境構築からプロジェクトの起動までやってみました。
次回はプログラムの中身を見て理解を深めていきたいと思います。

webpack-dev-serverでモジュールの変更が反映されない

webpack-dev-serverのauto-reloadでハマったこと
上の記事と同じくHMRが動かないところで詰まったけど,こちらの記事とは別のやり方で解決できたのでメモとしてまとめようかと思います.

今回,上の記事の解決策ではどうしてダメなのか

上の記事は,最終的には静的なファイル(./index.html)と webpack-dev-server で出力されるファイル(./js/build/bundle.js)を同一ディレクトリ内に配置する(index.html./js/build直下に再配置?)ことで解決しています.
上の記事では,

原因はファイルの場所がよろしくなかったみたい。
serveされる場所に静的ファイルと変更がかかるファイルがないとダメみたいだった。
...中略...
contentBaseを追加して静的ファイルと出力ファイルを同じ階層においたら動いた。

と書かれていますが,今回,以下のようなディレクトリ構成と webpack.config.js でうまく webpack-dev-server の auto-reload を機能させたかったので上の記事とは別の解決策を探る方向になりました.

.
├── dist
│   ├── css
│   ├── index.html   -> ./dist/js/index.jsを呼び出す静的なファイル
│   └── js
│       └── index.js -> Webpackで出力されるファイル
└── src
    ├── component01.js -> ./src/index.jsでimportされるファイル
    ├── component02.js -> ./src/index.jsでimportされるファイル
    └── index.js       -> Webpackのエントリーファイル
// webpack.config.js
const path = require('path');

module.exports = {
  entry: {
    path: path.resolve(__dirname, './src/index.js'),
  },
  output: {
    path: path.resolve(__dirname, './dist/js'),
    filename: 'index.js',
  },

  devtool: 'source-map',
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      query: {
        presets: ['es2017']
      }
    }]
  },
  resolve: {}
};

publicPathを用いる

http://webpack.github.io/docs/webpack-dev-server.html を読むと, webpack.config.jsoutput.publicPath を用いることで解決できそうなのがぼんやりとですがわかりました.
具体的には, webpack.config.jsoutput 部分を以下のように書き換えます.

output: {
  path: path.resolve(__dirname, './dist/js'),
  publicPath: '/js/', // この行を追加
  filename: 'index.js',
}

また, ./dist/index.html から呼び出す際は, <script src="js/index.js"></script> で呼び出せます.
<script src="./js/index.js"></script> ではなぜか動きません.要調査.

最後に

Chrome の Console を見ると以下のようなログが確認できました.

[WDS] App updated. Recompiling...                                             client?cd17:80
[WDS] App hot update...                                                      client?cd17:212
[HMR] Checking for updates on the server...                                 dev-server.js:45
[HMR] Updated modules:                                                log-apply-result.js:20
[HMR]  - 11                                                           log-apply-result.js:22
[HMR]  - 52                                                           log-apply-result.js:22
[HMR] Consider using the NamedModulesPlugin for module names.         log-apply-result.js:28
[HMR] App is up to date.                                                    dev-server.js:27

しかし,タブの自動更新が起こらず,手動でタブの更新をしないと変更が反映されません.
こちらも要調査だけど,これで自分の好きなディレクトリ構成で開発ができるようになりました〜.
やっぱり公式ドキュメントはちゃんと読むべきですね...^^;

Moodleの活動モジュールからJavaScriptで非同期通信を行う方法

はじめに

Moodleの活動モジュールにてボタンがクリックされた際に,Moodleサーバへリクエストを飛ばし,DBにアクセスしたかった.
活動モジュールのディレクトリ内にリクエストを受け取るファイルを設けて,そこに向かって Client JS からリクエストを投げても良かったが,セキュリティやらなんやらを考慮した結果, Moodle が用意してくれている機能を用いて上記を実現しようと思った次第.
Moodleやら活動モジュールの説明は割愛.

開発環境

require([‘core/ajax’])

Moodle では JavaScriptMoodleサーバと非同期で通信するために core/ajax (Moodleのルートディレクトリ/lib/amd/src/ajax.js) というものが用意されている.
そのため,活動モジュール開発者はリクエストパラメーターとコールバック関数だけを書けばいい.
見出しの require(['core/ajax']) という書き方は,公式のドキュメントに記載されている書き方だが, var ajax = require('core/ajax') のようにも書ける.

require(['core/ajax']) を用いる場合,

require(['core/ajax'], function(ajax) {
  let promises = ajax.call([
    methodname: 'hoge', args: {hoge: 'hogehoge', fuga: 'fugafuga'},
    methodname: 'fuga', args: {hoge: 'hogehoge', fuga: 'fugafuga'},
  ]);

  promises[0].done(function(res) {
    console.log(res);
  }).fail(function(err) {
    console.log(err);
  });

  promises[1].done(function(res) {
    console.log(res);
  }).fail(function(err) {
    console.log(err);
  });
});

と書けるため,グローバルを汚染せずに済む.
今回は,ボタンがクリックされた際に上記のような非同期通信を行いたいので, button.addEventListener('click', function(e) { ... }); の … の部分に記述する.

Moodle側のエンドポイント

core/ajax の実装を見ると, wwwroot(http://localhost:8888/moodle)/lib/ajax/service.php もしくは service-nologin.php を叩いていることがわかる.
では, ajax.call の引数に与えている methodnameargs は何に用いるのか?
これらは,Moodleが内部でごにょごにょして,最終的に methodname で指定したメソッドに対して args を与えて実行している.(厳密には少し違う)
そのため,活動モジュール開発者はサーバ側での処理を記述しておかなければならない.(当たり前っちゃ当たり前)
サーバ側で実行する処理を記述するためにいじるファイルは, Moodleのルートディレクトリ/mod/モジュール名/db/services.phpMoodleのルートディレクトリ/mod/モジュール名/externallib.php の2つ.

<?php /* Moodleのルートディレクトリ/mod/モジュール名/db/services.php */
$functions = array(
    'hoge' => array(
        'classname'   => 'mod_hoge_external',
        'methodname'  => 'methodname',
        'classpath'   => "mod/モジュール名/externallib.php",
        'description' => 'description',
        'type' => 'write',
        'loginrequired' => false,
    ),
    'fuga' => array(
        'classname'   => 'mod_fuga_external',
        'methodname'  => 'methodname',
        'classpath'   => "mod/モジュール名/externallib.php",
        'description' => 'description',
        'type' => 'write',
        'loginrequired' => false,
    ),
);

$services = array(
    'moodle_hogehoge' => array(
        'functions' => array (),
        // 'requiredcapability' => 'some/capability:specified',
        'restrictedusers' => 0,
        'enabled'=>0,
    )
);

$functions(連想配列) のキーは JavaScript 側の methodname と同じ名前にする必要がある.(JavaScript$functions['hoge']['methodname'] を指定するとエラーが返ってくるので注意)

<?php /* Moodleのルートディレクトリ/mod/モジュール名/externallib.php */
defined('MOODLE_INTERNAL') || die;

class mod_hoge_external extends external_api {
    public static function methodname_is_allowed_from_ajax() {
        return true;
    }

    public static function methodname_parameters() {
        return new external_function_parameters(
            array(
                'hoge' => new external_value(PARAM_TEXT, 'hoge'),
                'fuga' => new external_value(PARAM_TEXT, 'fuga'),
            )
        );
    }

    public static function methodname($hoge, $fuga) {
        return array(
          'hoge' => $hoge,
          'fuga' => $fuga
        );
    }

    public static function methodname_returns() {
        return new external_single_structure(
            array(
                'hoge' => new external_value(PARAM_TEXT, 'hoge'),
                'fuga' => new external_value(PARAM_TEXT, 'fuga')
            ),
            'description'
        );
    }
}
/* mod_fuga_externalクラスも同様に定義する */

今回の場合, externallib.php で最低限定義するクラスやメソッドは,

  • mod_hoge_external(クラス)
  • mod_fuga_external(クラス)
  • methodname_is_allowed_from_ajax(各クラスメソッド)
  • methodname_parameters(各クラスメソッド)
  • methodname_returns(各クラスメソッド)

となる.

活動モジュールのインストール

自作の活動モジュールは,

  1. Moodleのルートディレクトリ/mod 直下に自作した活動モジュールを配置
  2. ブラウザからMoodleへアクセス
  3. 活動モジュールのインストール
  4. コースなどに活動モジュールを配置

Moodleにインストールし,コースにモジュールを配置できる.
活動モジュールがインストールされる際, db/services.php に記述した $functionsmdl_external_functionsテーブル に, $servicesmdl_external_servicesテーブル に保存される.
ひとまず,これでMoodleサーバとの非同期通信ができるようになった.

おわりに

もっと簡単にできると思ってた分しんどかった…(´。_。`)
次は externallib.php で定義するクラスやメソッドについて少し詳しく書こうかな.

~/.ssh/config のHost情報を使ってgit clone

最近久しぶりに秘密鍵と公開鍵を作って,GitサーバにSSH接続しようとしたら,色々と忘れていたので戒めとして書く.

~/.ssh

LinuxMacだと各ユーザーのホームディレクトリに .ssh が隠れているかと思う. これは ssh-keygen で作った秘密鍵と公開鍵が置かれるデフォルトのディレクトリである.

~/.ssh/config

~/.ssh/config は複数サーバーのSSH接続を管理するためのファイル.

~/.ssh/config をエディタで開き,

Host hoge
  User git
  Hostname hoge.com
  Port 10000
  IdentityFile ~/.ssh/hoge_id

のような記述を追加し保存すると,以降, ssh -i ~/.ssh/hoge_id -p 10000 -l git hoge.com とせずとも, ssh hogeSSH接続ができるようになる.

SSHでのgit clone

本題.

Githubであったり,個人のGitサーバなど,リモート上のリポジトリSSHを用いて git clone したい時は, git clone hoge:<リポジトリ名> とすることで,リモート上のリポジトリをcloneできる.(パスワードを求められる場合があるが,これは ssh-keygen で入力したパスワードを入力すればよい)

もし, ~/.ssh/config をいじるのに抵抗がある場合は,この記事のように git コマンドをラップするという方法もある.

qiita.com

ElectronでjQueryを使うための設定

Electron(ver 0.35.4)でjQuery<script src="jquery.js"></script> と記述するだけで使えるようにするためには BrowserWindowインスタンスを生成する時に以下のように webPreferences > nodeIntegrationfalse にすればいいらしい.

mainWindow = new BrowserWindow({
  width: 600,   /* ここらへんは */
  height: 400,  /* てきとうに */

  webPreferences: {
    nodeIntegration: false,
  },
});

Hashの添字でnilが使えるらしい

ここ最近で一番の驚きだったので.

hoge = {nil => 'hogehoge'}
puts hoge[nil]

このプログラムを実行すると hogehoge と出力される.なぜこのような実装になっているのか.あとで時間がある時にでも調べてみたいと思う.

Titanium側からiOSモジュールに文字列を渡すとなぜか__NSArrayMになる

var Hoge = require('module_id');

Hoge.init({...});
Hoge.fuga('hogefugapiyo');
.
.
.
-(void)fuga:(NSString *)string {
  NSLog(@"[INFO] %@", NSStringFromClass([string class]));
}
.
.
.

上記のようなプログラムがあるとして,fugaメソッドに文字列を渡すと[INFO] __NSArrayMとログに出力される.

なぜなのか...