承認これくしょん

my black histories

転職ドラフトをやってみた話

はじめに

こちらの記事は転職ドラフト体験談投稿キャンペーンに参加しています。
https://job-draft.jp/articles/251

昨年の 3 月にキャリアについて悩んでいましたが、最終的には転職することを決めました。

今回の転職活動では主に転職ドラフトを利用しました。

特に新技術の業務経験がない、当時のスキルセットで指名をいただけるのか不安でしたが杞憂に終わり、幸い現職に縁あって入社することができました。

このように少なからず利用前後でイメージが変わった部分もありましたので、まだ登録されていない方も選択肢の一つに入れていただければと思います。

転職ドラフトに登録したきっかけ

当時は社歴も長くなってきて、開発する製品のコードベースの理解が進み
これまでの製品や開発におけるコンテキストも把握できていたことから比較的パフォーマンスは安定して出せていたと思います。

一方で業務経験は新卒からずっとサーバーサイド Java に偏っており、 20 代後半に差し掛かった今この状態が続くとキャリアの可能性が狭まっていく不安を覚えるようになりました。

もちろん純粋な技術面だけでなく、ビジネスサイドとの折衝やメンバーのアサイン・育成などプロジェクト運営スキルの必要性も感じていました。

それらを担うリーダーポジションを打診されたこともありましたが、同僚を見る限り業務負荷的にどうしても実務からは離れざるを得ないようで、その節はお断りしました。

こういった経緯もあり、できる技術の幅を広げつつプロジェクト推進力も身につく環境があればと思い転職ドラフトに登録しました。

転職ドラフトでの指名状況

私は 2019 年 5 月開催の第 19 回に参加して、6 社から指名をいただきました。

各社の提示年収に幅はあれど、少なくとも年収ベースで 50 万円アップは見込めるようでした。

また終了間際に指名する企業も多い中、スピーディーに指名してくれる企業は特に好感が持てました。

一人ひとりに個別のメッセージを用意する必要があるため、企業側の負担は間違いなく大きいとはいえ、自社のイメージアップ・他社に先んじて面談スケジュールを確定できるなど、それに見合う恩恵はあるのではないでしょうか。

面接で感じたこと

私は承諾したすべての指名で、初回は選考なしの自社紹介(いわゆるカジュアル面談)をお願いしています。

各社のお話を伺ってから自分の意向に合うか判断し、後日正式な選考に進む流れとしました。

少々時間はかかるものの、カジュアル面談の時点でアンマッチならお互いに選考の負担が省けますし、 私としても相手に共感してもらえそうな経験を棚卸ししてから面接に臨めるというメリットがありました。

選考に入ると、転職ドラフトの売りである情報量満載のレジュメが役立ちました。

基本的なプロフィールは伝わっているため、最初から少々踏み込んだ質問からスタートでき、限られた面接の時間を有効に活用できた実感があります。

補足: 転職ドラフトのレジュメについて

一般的なエンジニア向け職務経歴書では

  • プロジェクトの概要
  • 規模と職務
  • 担当フェーズ
  • 業務内容
  • 開発環境

上記の項目が並んでいますが、それだけではどのような課題があり、そこから何を考えどう行動したのかといったマインドセット面の情報が不足しています。

特に私の場合、このフォーマットに従うとレガシーな技術が目立ち悪印象になるのではという懸念もありました。

一方で転職ドラフトのレジュメは自由記述がメインコンテンツであることから、自分自身でアピールしたいポイントを選ぶことができます。

さらに 800 字以上の記載を推奨している通り、要約する必要はなくむしろ長文推奨というスタンスが取られています。

これはまさに渡りに船で、私はプロジェクトで起きた問題の対処・取り組んだ改善を丁寧に記載することにしました。

それぞれの具体例から「プロダクトに必要と考えたことは技術面に限らず、他のメンバーと協力して進めていく」というポリシーに気づいてもらうことを狙って、エピソードは取捨選択しました。

結果的にはこれが一定成功したようで、チームプレイ重視の現職からも声をかけてもらうことができました。

(余談ですが、入社後に自分のレジュメに対するコメントを見たところ「量が多く読むのが大変」とあり申し訳ない気持ちになりました)

転職ドラフトへの改善要望

  • 私は一つ目のプロジェクトについて 8 割ほど書いただけで審査通過し、少々拍子抜けしたのが正直なところです。
    • 何らかのフィードバックをもらえるものと思っていましたが、特にないまま指名期間に入りました。
    • レジュメの質を保つためのフィルタリング的な側面が強いのかな?と推測しています。
  • 未返信メッセージの通知をユーザー側でクリアできない
    • 企業側が「返信不要」のチェックを入れずに送信すると、ユーザー側で返信しない限り通知は消えません。
    • 特に選考終了時のメッセージが「返信不要」設定なしで送信されると、それが通知に残り続けてノイズになります。

転職ドラフトを使ったことがない方へのメッセージ

私は年収アップを前面に押し出した Web 広告のイメージが強く、最新技術に精通し経験も豊富な「強い人」のためのサービスだと想像していました。

しかし実際にはそうでない人でも、しっかりレジュメでアピールできれば指名は十分もらえると思います。

というのも指名期間中には古の mixi の足跡みたく、レジュメを閲覧した企業が通知されるのですが、 見ている限り多くの企業が参加者全員のレジュメを一通りチェックしている印象があります。 つまり足切りのようなことを実施している企業の方が少数派ではないでしょうか。

何よりも自分の強みを言語化して、読み手に伝わる文章が書けるかが最も重要だと考えます。

記載すべき量が多いため、レジュメ執筆には十分時間を確保して臨んでください。 私もなかなか大変でしたが、一度書けば他フォーマットへの加工もできる量の文章ができますので、決して無駄にはなりません。

少しでも興味があれば、ぜひ参加してみてください。

自社製品開発エンジニアに転職して 3 年経った

転職してからずっとブログをお休みしていましたが、いい機会なので1簡単にこれまでを振り返ってみようと思います。

3 年間でやったこと

1 年目

スクラムチームに所属して、自社製品の次バージョン開発をやっていました。

それなりに大きな規模のコードベースを相手にする改修は、私にとって初めての経験でした。 当時は不安もありましたが、幸いにもスプリントプランニングやペアプロなどで同僚の仕事の進め方を間近で見ることができました。 現在のテクニカルスキルの多くは、そこで学んだことが基礎になっていると思います。

特に「設計思想やモジュールの責務を理解し、適切な場所に修正を加える」具体的なアプローチを知れたのが、私にとって財産になりました。 前職では諸般の事情2から、「できる限り差分を小さくする」場当たり的な改修ばかりで、上記が重要と知っていても実践する機会はありませんでした。 そのため自分自身で interface を定義することもなかったのですが、実際の使用例やそれによる恩恵を理解してから使用頻度が増えました。

2 年目

自社製品をベースとしたクラウドサービスの開発をやっていました。

インフラとして使用する AWS の知識はほとんどありませんでしたが、同僚の支援を受けながら仕様検討を進めました。 ここでは各選択肢についてメリット・デメリットを提示し、関係者を集めて議論により決定するやり方を学びました。 これによる決定は関係者間のレビューを経ているため、独りよがりな結論にはなりにくい印象がありました。 そのため、その方針に基づいて出来上がったタスクは自信をもって進めることができました。

また作業を社外の方にお願いする場合もあり、それが必要と判断した根拠などをミーティングで共有することもありました。 パワポ資料を準備して臨んだりと、いい意味で SE 時代よりもそれらしい仕事をする機会を得られました。

3 年目

前半は昨年度と同様にクラウドサービスを、後半からは 1 年目に担当した自社製品も含めて横断的に活動するようになりました。

年度初めに中途入社された方が私のチーム配属となり、こちらはメンター的な立場で一緒に仕事をするようになりました。 ちょうど私が入社したころに経験したようなペアプロを行い、たくさん会話しながら実装を進めました。

コーディング以外では、以下のような活動を行いました。

  • 前述のクラウドサービスの有識者としてナレッジを esa にまとめる
  • 他チームへの情報共有
  • 流しのコードレビュー 3
  • エンハンス要望の実現可能性と工数調査

所感

「きちんと設計して可読性・保守性の高いコードを書くスキル」はある程度身についたと思います。

特定の技術分野に注力するよりは、まず一通りのことをこなせるよう努力していましたが、これも一定の成果を得られたと思います。

私自身の指向がゼネラリスト寄りなので戦略は間違っていなかったと思いますが、一方で今後のキャリアについて悩むようになりました。 その時必要とされるスキルが市場価値の向上に役立つとは限りませんし、日々進歩していく技術をキャッチアップしていく必要もあります。 いよいよ自分が何を大事にしたいのか……といった価値観が問われているのかもしれませんね。

こんな経緯から kiitok でキャリア相談してみようと Facebook に登録したのですが、翌日アカウントがブロックされて今に至ります。


  1. 平成の終わり、周辺状況の変化に一区切りついたなど

  2. ソースコードの品質、変更箇所をコメントで明示する文化、BP として参画している……など

  3. 必ず一人はレビュアーにアサインされる決まりですが、私が担当でない場合も気になった点はコメントしていました

TypeScriptの言語サービスを使ってサクラエディタで入力補完してみる

TypeScriptプロジェクトにはtypescriptServices.jsが含まれており、コンパイラが持つ機能を外部から利用できます。
その中の言語サービス(Language Service API)を使ってコードの補完候補を取得し、SIでおなじみサクラエディタで表示してみました。
PowerShell + WSH(JScript)で構成されているため、標準的なWindows 7以降の環境で動作するはずです。

全体の構成

下記のts-sakura以下を、サクラエディタプラグインフォルダに置きます。
typeScriptServices.js、lib.d.tsはGitHubから取得できます。
他にも読み込みたい型定義ファイルがあればlib内にしまってください。

ts-sakura
│  plugin.def
│  ts-sakura.js
│
└─server
    │  server.ps1
    │  start.cmd
    │  typescriptServices.js
    │
    └─lib
            lib.d.ts

plugin.def

プラグインの定義ファイルです。

[Plugin]
Id=ts-sakura
Name=ts-sakura
Description=TypeScriptの簡易補完プラグイン
Type=wsh
Version=0.1
Url=http://old-horizon.hateblo.jp/
Author=

[Wsh]
UseCache=0

[Plug]
Complement=ts-sakura.js
Complement.Label=ts-sakura

[Option]
O[1].Section=Option
O[1].Key=Port
O[1].Label=サーバーのポート番号
O[1].Type=Int
O[1].Default=8081

ts-sakura.js

サクラエディタ上で実行されるスクリプトです。
PowerShellのサーバーにソース全文とキャレット位置を送信し、返された補完候補を表示します。

// サーバーのポート番号
var port = Plugin.GetOption("Option","Port");

// 現在入力中の文字列
var currentWord = Complement.GetCurrentWord();

// ソース全文を取得
Editor.SetDrawSwitch(0);
Editor.MoveHistSet();
Editor.SelectAll();
var allLines = Editor.GetSelectedString(0);
Editor.MoveHistPrev();

// キャレット位置までの文字数を取得
var lineCount = Editor.ExpandParameter("$y");
var columnCount = Editor.ExpandParameter("$x");
var position = 0;
for (var i = 1; i < lineCount; i++) {
    position += Editor.GetLineStr(i).length;
}
position += columnCount - 1;

// サーバーから補完候補を取得
var xhr = new ActiveXObject("Msxml2.XMLHTTP.6.0");
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
        if (xhr.status == 200) {
            var response = xhr.responseText;
            if (!response) return;
            var completions = response.split(",");
            for (var i = 0, length = completions.length; i < length; i++) {
                // 先頭一致の候補のみを表示
                if(currentWord == "." || completions[i].toLowerCase().lastIndexOf(currentWord.toLowerCase(), 0) == 0) {
                    try{
                        Complement.AddList(currentWord == "." ? "." + completions[i] : completions[i]);
                    } catch(e) {}
                }
            }
        } else {
            var shell = new ActiveXObject("WScript.Shell");
            shell.Popup("通信に失敗しました。\nサーバーが起動していない可能性があります。", 3, "ts-sakura", 48);
        }
    }
}

try {
    // リクエストを送信
    xhr.Open("POST", "http://localhost:" + port + "/", false);
    xhr.setRequestHeader("x-caretposition", position);
    xhr.send(allLines);
} catch(e) {}

server.ps1

補完候補を返す簡易HTTPサーバーです。
ScriptControlでWSHエンジンを作成し、その中でAPIを呼び出します。

# ポート番号
$port = $args[0]
# カレントディレクトリ
$currentDir = [System.IO.Path]::GetDirectoryName($MyInvocation.InvocationName)

# フルパスを取得
function getFullPath($name) {
    return $([System.IO.Path]::Combine($currentDir, $name)).Replace("\","\\")
}

$jsCode = @"
// 指定ファイルのテキストを取得
function readFile(path) {
    var sr = new ActiveXObject("ADODB.Stream");
    sr.Type = 2;
    sr.charset = "_autodetect_all";
    sr.Open();
    sr.LoadFromFile(path);
    var content = sr.ReadText(-1);
    sr.Close();
    sr = null;
    return content;
}

// eval時のエラー防止
Array.prototype.indexOf = function() {}

// TypeScriptコンパイラを読み込む
eval(readFile("$(getFullPath("typescriptServices.js"))"));

// インターフェースの実装
var LanguageServiceHostImpl = (function() {
    function LanguageServiceHostImpl() {
        this.files = {};
    }
    LanguageServiceHostImpl.prototype.getCompilationSettings = function() {
        return ts.getDefaultCompilerOptions();
    };
    LanguageServiceHostImpl.prototype.getScriptFileNames = function() {
        var fileNames = [];
        for (var fileName in this.files) {
            if (this.files.hasOwnProperty(fileName)) {
                fileNames.push(fileName);
            }
        }
        return fileNames;
    };
    LanguageServiceHostImpl.prototype.getScriptVersion = function(fileName) {
        return this.files[fileName].version.toString();
    };
    LanguageServiceHostImpl.prototype.getScriptIsOpen = function(fileName) {
        return true;
    };
    LanguageServiceHostImpl.prototype.getScriptSnapshot = function(fileName) {
        return this.files[fileName].snapshot;
    };
    LanguageServiceHostImpl.prototype.getCurrentDirectory = function() {
        return "";
    };
    LanguageServiceHostImpl.prototype.getDefaultLibFilename = function(options) {
        return "";
    };
    LanguageServiceHostImpl.prototype.log = function(s) {
    };
    LanguageServiceHostImpl.prototype.addFile = function(fileName, body) {
        var snapshot = ts.ScriptSnapshot.fromString(body);
        snapshot.getChangeRange = function (oldSnapshot) {
            return undefined;
        };
        if (this.files[fileName]) {
            this.files[fileName].version++;
            this.files[fileName].snapshot = snapshot;
        }
        else {
            this.files[fileName] = { snapshot: snapshot, version: 1 };
        }
    };
    return LanguageServiceHostImpl;
})();

var fso = new ActiveXObject("Scripting.FileSystemObject");

// libディレクトリのフルパス
var libDir = fso.BuildPath("$($currentDir.Replace("\","\\"))", "lib");

var host = new LanguageServiceHostImpl();
var service = ts.createLanguageService(host, ts.createDocumentRegistry());

// 型定義ファイルの読み込み
var e = new Enumerator(fso.GetFolder(libDir).Files);
for (; !e.atEnd(); e.moveNext()) {
    var file = e.item();
    if (/\.ts$/.test(file.Name)) {
        host.addFile(file.Name, readFile(file.Path));
    }
}

// タイムスタンプから一時ファイル名を生成
var tempName = new Date().getTime();
while (host.files[tempName + ".ts"]) {
    // ファイル名が衝突した場合はインクリメント
    tempName++;
}
tempName += ".ts";

// 補完候補を取得
function getCompletions(source, position) {
    host.addFile(tempName, source);
    var completions = service.getCompletionsAtPosition(tempName, position);
    var entries = [];
    
    if (completions) {
      for (var i = 0, length = completions.entries.length; i < length; i++) {
          entries.push(completions.entries[i].name);
      }
    }
    // カンマ区切りで一覧を返す
    return entries.join(",");
}
"@

# WSHオブジェクトを生成
$sc = New-Object -Com ScriptControl
$sc.Language = "JScript"
$sc.AddCode($jsCode)

# HTTPサーバーの起動
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.add("http://localhost:" + $port + "/")
$listener.Start()
Write-Host "ポート$($port)で起動しました"

while($listener.IsListening) {
    $context = $listener.GetContext()
    $request = $context.Request
    $response = $context.Response
    
    if ($request.HasEntityBody) {
        $body = $request.InputStream
        $reader = New-Object System.IO.StreamReader($body, $request.ContentEncoding)
        
        # ソース全文とキャレット位置から補完候補を取得
        $completions = $sc.CodeObject.getCompletions($reader.ReadToEnd(), $request.Headers["x-caretposition"])
        
        # 戻り値を出力して返す
        $writer = New-Object System.IO.StreamWriter($response.OutputStream, $request.ContentEncoding)
        $writer.Write($completions)
        $writer.Close()
        $response.OutputStream.Close()
        $response.Close()
    }
}

$listener.Stop()

start.cmd

待ち受けポートを指定してserver.ps1を実行するバッチファイルです。
ScriptControlを使う都合上、32bit版のPowerShellを起動させています。

@echo off
rem ポート番号を指定
set port=8081
if defined ProgramFiles(x86) (
  %SystemRoot%\syswow64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy RemoteSigned -File server.ps1 %port%
) else (
  powershell.exe -ExecutionPolicy RemoteSigned -File server.ps1 %port%
)

使い方

start.cmdとサクラエディタ側の設定でポート番号を合わせ、サーバーを起動すると補完候補が出るようになります。
ただ依存関係の解決をしていないし、不安定で全然ダメですね…実用するならTSServer使うべきでしょう。
JavaScriptの範囲でも少しは型推論が効きます。

参考

Axis2でnasneから録画タイトル一覧を取得する

私がアニメ録画に愛用しているnasneですが、これはUPnPプロトコルで通信するためSOAPAPIが呼び出せます。
そこで先人の皆様の知恵をお借りしつつ、JavaSOAPライブラリであるApache Axis2で録画一覧の情報を取得してみました。

クライアントスタブの生成

UPnPではWSDLに相当するSCPD(Service Control Protocol Document)にSOAPアクションが記載されていますが、フォーマットは結構異なります。
ちなみにnasneの予約関連は以下に定義されています。

http://(nasneのIPアドレス):64230/XSRS.xml

今回はSCPDを見ながらEclipseのエディタでWSDLを作成しています。グラフィカルに編集できるので意外と楽でした。

XSRS.wsdl
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:schemas-xsrs-org:service:X_ScheduledRecording:2" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="XSRS" targetNamespace="urn:schemas-xsrs-org:service:X_ScheduledRecording:2">
  <wsdl:types>
    <xsd:schema targetNamespace="urn:schemas-xsrs-org:service:X_ScheduledRecording:2">
      <xsd:element name="X_CreateRecordSchedule">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="Elements" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_CreateRecordScheduleResponse">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="RecordScheduleID" type="xsd:string" />
            <xsd:element name="Result" type="xsd:string"></xsd:element>
            <xsd:element name="UpdateID" type="xsd:int"></xsd:element>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_GetConflictList">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Elements" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_GetConflictListResponse">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Result" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_DeleteRecordSchedule">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="RecordScheduleID" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_DeleteRecordScheduleResponse">
        <xsd:complexType>
            <xsd:sequence>

            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_GetRecordScheduleList">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="SearchCriteria"
                    type="xsd:string" nillable="true">
                </xsd:element>
                <xsd:element name="Filter" type="xsd:string" nillable="true"></xsd:element>
                <xsd:element name="StartingIndex" type="xsd:int"></xsd:element>
                <xsd:element name="RequestedCount" type="xsd:int"></xsd:element>
                <xsd:element name="SortCriteria" type="xsd:string" nillable="true"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_GetRecordScheduleListResponse">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Result" type="xsd:string"></xsd:element>
                <xsd:element name="NumberReturned" type="xsd:int"></xsd:element>
                <xsd:element name="TotalMatches" type="xsd:int"></xsd:element>
                <xsd:element name="UpdateID" type="xsd:int"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_UpdateRecordSchedule">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Elements" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_UpdateRecordScheduleResponse">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Result" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_GetTitleList">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="SearchCriteria"
                    type="xsd:string" nillable="true">
                </xsd:element>
                <xsd:element name="Filter" type="xsd:string" nillable="true"></xsd:element>
                <xsd:element name="StartingIndex" type="xsd:int"></xsd:element>
                <xsd:element name="RequestedCount" type="xsd:int"></xsd:element>
                <xsd:element name="SortCriteria" type="xsd:string" nillable="true"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_GetTitleListResponse">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Result" type="xsd:string"></xsd:element>
                <xsd:element name="NumberReturned" type="xsd:int"></xsd:element>
                <xsd:element name="TotalMatches" type="xsd:int"></xsd:element>
                <xsd:element name="UpdateID" type="xsd:int"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_DeleteTitle">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="TitleID" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_DeleteTitleResponse">
        <xsd:complexType>
            <xsd:sequence>

            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_UpdateTitle">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Elements" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="X_UpdateTitleResponse">
        <xsd:complexType>
            <xsd:sequence>

                <xsd:element name="Result" type="xsd:string"></xsd:element>
            </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
  </wsdl:types>
  <wsdl:message name="X_CreateRecordScheduleRequest">
    <wsdl:part element="tns:X_CreateRecordSchedule" name="parameters"/>
  </wsdl:message>
  <wsdl:message name="X_CreateRecordScheduleResponse">
    <wsdl:part element="tns:X_CreateRecordScheduleResponse" name="parameters"/>
  </wsdl:message>
  <wsdl:message name="X_GetConflictListRequest">
    <wsdl:part name="parameters" element="tns:X_GetConflictList"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_GetConflictListResponse">
    <wsdl:part name="parameters" element="tns:X_GetConflictListResponse"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_DeleteRecordScheduleRequest">
    <wsdl:part name="parameters" element="tns:X_DeleteRecordSchedule"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_DeleteRecordScheduleResponse">
    <wsdl:part name="parameters" element="tns:X_DeleteRecordScheduleResponse"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_GetRecordScheduleListRequest">
    <wsdl:part name="parameters" element="tns:X_GetRecordScheduleList"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_GetRecordScheduleListResponse">
    <wsdl:part name="parameters" element="tns:X_GetRecordScheduleListResponse"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_UpdateRecordScheduleRequest">
    <wsdl:part name="parameters" element="tns:X_UpdateRecordSchedule"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_UpdateRecordScheduleResponse">
    <wsdl:part name="parameters" element="tns:X_UpdateRecordScheduleResponse"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_GetTitleListRequest">
    <wsdl:part name="parameters" element="tns:X_GetTitleList"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_GetTitleListResponse">
    <wsdl:part name="parameters" element="tns:X_GetTitleListResponse"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_DeleteTitleRequest">
    <wsdl:part name="parameters" element="tns:X_DeleteTitle"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_DeleteTitleResponse">
    <wsdl:part name="parameters" element="tns:X_DeleteTitleResponse"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_UpdateTitleRequest">
    <wsdl:part name="parameters" element="tns:X_UpdateTitle"></wsdl:part>
  </wsdl:message>
  <wsdl:message name="X_UpdateTitleResponse">
    <wsdl:part name="parameters" element="tns:X_UpdateTitleResponse"></wsdl:part>
  </wsdl:message>
  <wsdl:portType name="X_ScheduledRecordingType">
    <wsdl:operation name="X_CreateRecordSchedule">
      <wsdl:input message="tns:X_CreateRecordScheduleRequest"/>
      <wsdl:output message="tns:X_CreateRecordScheduleResponse"/>
    </wsdl:operation>
    <wsdl:operation name="X_GetConflictList">
        <wsdl:input message="tns:X_GetConflictListRequest"></wsdl:input>
        <wsdl:output message="tns:X_GetConflictListResponse"></wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_DeleteRecordSchedule">
        <wsdl:input message="tns:X_DeleteRecordScheduleRequest"></wsdl:input>

    </wsdl:operation>
    <wsdl:operation name="X_GetRecordScheduleList">
        <wsdl:input message="tns:X_GetRecordScheduleListRequest"></wsdl:input>
        <wsdl:output message="tns:X_GetRecordScheduleListResponse"></wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_UpdateRecordSchedule">
        <wsdl:input message="tns:X_UpdateRecordScheduleRequest"></wsdl:input>
        <wsdl:output message="tns:X_UpdateRecordScheduleResponse"></wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_GetTitleList">
        <wsdl:input message="tns:X_GetTitleListRequest"></wsdl:input>
        <wsdl:output message="tns:X_GetTitleListResponse"></wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_DeleteTitle">
        <wsdl:input message="tns:X_DeleteTitleRequest"></wsdl:input>

    </wsdl:operation>
    <wsdl:operation name="X_UpdateTitle">
        <wsdl:input message="tns:X_UpdateTitleRequest"></wsdl:input>
        <wsdl:output message="tns:X_UpdateTitleResponse"></wsdl:output>
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="X_ScheduledRecordingBinding"
    type="tns:X_ScheduledRecordingType">

    <soap:binding style="document"
        transport="http://schemas.xmlsoap.org/soap/http" />
    <wsdl:operation name="X_CreateRecordSchedule">

        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_CreateRecordSchedule" />
        <wsdl:input>

            <soap:body use="literal" />
        </wsdl:input>
        <wsdl:output>

            <soap:body use="literal" />
        </wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_GetConflictList">
        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_GetConflictList" />
        <wsdl:input>
            <soap:body use="literal" />
        </wsdl:input>
        <wsdl:output>
            <soap:body use="literal" />
        </wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_DeleteRecordSchedule">
        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_DeleteRecordSchedule" />
        <wsdl:input>
            <soap:body use="literal" />
        </wsdl:input>
    </wsdl:operation>
    <wsdl:operation name="X_GetRecordScheduleList">
        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_GetRecordScheduleList" />
        <wsdl:input>
            <soap:body use="literal" />
        </wsdl:input>
        <wsdl:output>
            <soap:body use="literal" />
        </wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_UpdateRecordSchedule">
        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_UpdateRecordSchedule" />
        <wsdl:input>
            <soap:body use="literal" />
        </wsdl:input>
        <wsdl:output>
            <soap:body use="literal" />
        </wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_GetTitleList">
        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_GetTitleList" />
        <wsdl:input>
            <soap:body use="literal" />
        </wsdl:input>
        <wsdl:output>
            <soap:body use="literal" />
        </wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="X_DeleteTitle">
        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_DeleteTitle" />
        <wsdl:input>
            <soap:body use="literal" />
        </wsdl:input>
    </wsdl:operation>
    <wsdl:operation name="X_UpdateTitle">
        <soap:operation
            soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_UpdateTitle" />
        <wsdl:input>
            <soap:body use="literal" />
        </wsdl:input>
        <wsdl:output>
            <soap:body use="literal" />
        </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="X_ScheduledRecordingService">
    <wsdl:port binding="tns:X_ScheduledRecordingBinding" name="X_ScheduledRecording">
      <soap:address location="http://192.168.0.2:64230/XSRS"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

そしてAxis2同梱のWSDL2Javaツールを使って、クライアントスタブを生成します。私はWindowsなのでこんな感じです。

wsdl2java.bat -uri XSRS.wsdl -o (出力先ディレクトリ)

あとはこのスタブを呼び出すだけ…のはずでしたが、意外にもハマってしまいました。

</soap:Header>の除去

Axis2で生成したスタブが送信したリクエストを見ると、空のヘッダー要素である</soap:Header>が含まれていました。
しかしnasneはこれを含むリクエストをエラーとして処理してしまいます。
自動生成されたスタブのコードには手を加えず、リクエスト送信前に除去するモジュールを作成して対処します。

src/headerremover/RemovingModule.java

インターフェースを実装していますが中身はありません。

package headerremover;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.description.AxisDescription;
import org.apache.axis2.description.AxisModule;
import org.apache.axis2.modules.Module;
import org.apache.neethi.Assertion;
import org.apache.neethi.Policy;

public class RemovingModule implements Module {

    @Override
    public void applyPolicy(Policy arg0, AxisDescription arg1) throws AxisFault {

    }

    @Override
    public boolean canSupportAssertion(Assertion arg0) {
        return false;
    }

    @Override
    public void engageNotify(AxisDescription arg0) throws AxisFault {

    }

    @Override
    public void init(ConfigurationContext arg0, AxisModule arg1)
            throws AxisFault {

    }

    @Override
    public void shutdown(ConfigurationContext arg0) throws AxisFault {

    }

}
src/headerremover/RemoveHandler.java

こちらに実際の処理である、ヘッダー要素を取り除く処理を書きます。

package headerremover;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.engine.Handler;
import org.apache.axis2.handlers.AbstractHandler;

public class RemoveHandler extends AbstractHandler implements Handler {

    @Override
    public InvocationResponse invoke(MessageContext context) throws AxisFault {
        SOAPEnvelope envelope = context.getEnvelope();
        if (envelope.getHeader() != null) {
            envelope.getHeader().detach();
        }
        return InvocationResponse.CONTINUE;
    }

}
META-INF/module.xml

モジュール名称、それに紐づくクラス、実行タイミングを定義します。
このモジュールでは"OperationOutPhase"を指定したので、処理はリクエスト送信前に実行されます。

<module name="header-remover" class="headerremover.RemovingModule">
    <OutFlow>
        <handler name="OutFlowLogHandler" class="headerremover.RemoveHandler">
            <order phase="OperationOutPhase"/>
        </handler>
    </OutFlow>
</module>

以上を含むjarを作成して、拡張子を.marに変更します。
これをクラスパスに配置すると使用可能なモジュールとして読み込まれます。

レスポンスをJAXBでマッピング

レスポンスはXML形式なので、JAXBでオブジェクトにマッピングすると扱いやすくなります。
TrangでレスポンスからXMLスキーマを生成し、少し手を加えたものがこちらです。

XSRS.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:schemas-xsrs-org:metadata-1-0/x_srs/" xmlns:x_srs="urn:schemas-xsrs-org:metadata-1-0/x_srs/">
  <xs:element name="xsrs">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" ref="x_srs:item"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="item">
    <xs:complexType>
      <xs:sequence>
        <xs:element minOccurs="0" ref="x_srs:title"/>
        <xs:element ref="x_srs:scheduledStartDateTime"/>
        <xs:element ref="x_srs:scheduledDuration"/>
        <xs:element ref="x_srs:scheduledConditionID"/>
        <xs:element ref="x_srs:scheduledChannelID"/>
        <xs:element ref="x_srs:desiredMatchingID"/>
        <xs:element ref="x_srs:desiredQualityMode"/>
        <xs:element minOccurs="0" ref="x_srs:genreID"/>
        <xs:element ref="x_srs:conflictID"/>
        <xs:element ref="x_srs:mediaRemainAlertID"/>
        <xs:element ref="x_srs:reservationCreatorID"/>
        <xs:element ref="x_srs:titleProtectFlag"/>
        <xs:element ref="x_srs:titleNewFlag"/>
        <xs:element ref="x_srs:recordingFlag"/>
        <xs:element ref="x_srs:recordDestinationID"/>
        <xs:element ref="x_srs:recordSize"/>
        <xs:element ref="x_srs:lastPlaybackTime"/>
        <xs:element ref="x_srs:portableRecordFile"/>
      </xs:sequence>
      <xs:attribute name="id" use="required" type="xs:integer"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="title" type="xs:string"/>
  <xs:element name="scheduledStartDateTime" type="xs:string" />
  <xs:element name="scheduledDuration" type="xs:integer"/>
  <xs:element name="scheduledConditionID" type="xs:integer"/>
  <xs:element name="scheduledChannelID">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:string">
          <xs:attribute name="broadcastingType" use="required" type="xs:integer"/>
          <xs:attribute name="channelType" use="required" type="xs:integer"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>
  <xs:element name="desiredMatchingID">
    <xs:complexType mixed="true">
      <xs:attribute name="type" use="required" type="xs:string"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="desiredQualityMode" type="xs:string" />
  <xs:element name="genreID">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:integer">
          <xs:attribute name="type" use="required" type="xs:integer"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>
  <xs:element name="conflictID" type="xs:integer"/>
  <xs:element name="mediaRemainAlertID" type="xs:integer"/>
  <xs:element name="reservationCreatorID" type="xs:integer"/>
  <xs:element name="titleProtectFlag" type="xs:integer"/>
  <xs:element name="titleNewFlag" type="xs:integer"/>
  <xs:element name="recordingFlag" type="xs:integer" />
  <xs:element name="recordDestinationID" type="xs:string" />
  <xs:element name="recordSize" type="xs:integer"/>
  <xs:element name="lastPlaybackTime">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:string">
          <xs:attribute name="resumePoint" use="required" type="xs:integer"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>
  <xs:element name="portableRecordFile">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:integer">
          <xs:attribute name="target" use="required" type="xs:string"/>
          <xs:attribute name="transferPath" use="required" type="xs:string"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>
</xs:schema>

そして作成したXMLスキーマから、JDK付属のxjcコマンドで対応するJavaクラスを生成します。

xjc XSRS.xsd -d (出力先ディレクトリ)

サンプル

上記で作成したクラスを使って、nasneから録画タイトルの一覧を取得してみます。

import java.io.StringReader;

import javax.xml.bind.JAXB;

import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.transport.http.HTTPConstants;
import org.xsrs.schemas.metadata_1_0.x_srs.Xsrs;

import _2.x_scheduledrecording.service.schemas_xsrs_org.X_ScheduledRecordingServiceStub;
import _2.x_scheduledrecording.service.schemas_xsrs_org.X_ScheduledRecordingServiceStub.X_GetTitleList;
import _2.x_scheduledrecording.service.schemas_xsrs_org.X_ScheduledRecordingServiceStub.X_GetTitleListResponse;

public class SampleClient {

    private static final String END_POINT = "http://192.168.0.2:64230/XSRS";

    public static void main(String[] args) throws Exception {
        // スタブを生成
        X_ScheduledRecordingServiceStub stub = new X_ScheduledRecordingServiceStub(END_POINT);
        ServiceClient client = stub._getServiceClient();
        // チャンク転送を無効に設定
        client.getOptions().setProperty(HTTPConstants.CHUNKED, false);
        // モジュールを登録
        client.engageModule("header-remover");
        // リクエスト作成
        X_GetTitleList req = new X_GetTitleList();
        req.setStartingIndex(0);
        req.setRequestedCount(0);
        // レスポンス取得
        X_GetTitleListResponse res = stub.x_GetTitleList(req);
        // JAXBでオブジェクトにマッピング
        Xsrs data = JAXB.unmarshal(new StringReader(res.getResult()), Xsrs.class);
        // 全件のタイトルを出力する
        data.getItem().stream().forEach(item -> System.out.println(item.getTitle()));
    }

}

実行するとコンソールに一覧が出力されるはずです。
Windows TV ゴシックなどARIB外字を含むフォントがおすすめです。

参考

雨雲レーダーを監視してメール通知する

某雨雲レーダーを監視し、指定範囲内のメッシュが一定の割合を超えるとGmailからメールを送信します。
PIL以外は全て標準ライブラリを使っているので、AndroidのQPythonでも動きました。
事前にcurlなどでリフレッシュトークンを取得しておいてください。

ソース

# -*- coding: utf-8 -*-

import base64, io, json, logging, time, urllib, urllib2
from datetime import datetime, timedelta
from PIL import Image
from email.mime.text import MIMEText
from email.header import Header

# レーダー設定
AMESH_BASE_URL = "http://tokyo-ame.jwa.or.jp/mesh/100/{0}.gif"
CROP_BOX = (100, 100, 100, 100) # 選択範囲
NOTIFY_PERCENTAGE = 10 # 10%以上で通知
NOTIFY_INTERVAL = 30 # 30分間隔で確認

# Gmail認証
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
REFRESH_TOKEN = "YOUR_REFRESH_TOKEN"

# 送信メール設定
MAIL_TO = "foo@bar.ne.jp" # 宛先
MAIL_SUBJECT = u"雨雲レーダー" # 件名
MAIL_BODY = u"雨雲が接近しています" # 本文

last_notified = datetime(1, 1, 1)
interval_time = timedelta(minutes = NOTIFY_INTERVAL)

def main():
    global NOTIFY_PERCENTAGE
    global NOTIFY_INTERVAL
    global scheduler
    global last_notified
    global interval_time
    logging.info("処理を開始します")
    now = datetime.now()
    if now - last_notified > interval_time:
        logging.info("レーダー画像を取得します")
        clouds_exists = check_clouds(now)
        if clouds_exists:
            token = get_token()
            if token:
                send_mail(token, now)
        else:
            logging.info("メッシュが%d%%以下です", NOTIFY_PERCENTAGE)
    else:
        logging.info("最後の通知から%d分以内です", NOTIFY_INTERVAL)
    logging.info("処理が終了しました")

def check_clouds(now):
    try:
        timestamp = get_timestamp(now)
        # レーダー画像をダウンロード
        img_file = io.BytesIO(urllib2.urlopen(AMESH_BASE_URL.format(timestamp)).read())
        # PILで開く
        img = Image.open(img_file)
        # 選択範囲でトリミングしてRGB値を取得
        img = img.crop(CROP_BOX)
        img_rgb = img.convert("RGB").getdata()
        # 雨雲のメッシュを数える
        mesh_count = 0.0
        for rgb in img_rgb:
            if rgb != (0, 0, 1):
                mesh_count += 1
        # 割合を算出
        mesh_percentage = mesh_count / len(img_rgb) * 100
        logging.info("現在のメッシュ: %s%%", mesh_percentage)
        # 閾値を超えた場合は通知
        if mesh_percentage > NOTIFY_PERCENTAGE:
            return True
        return False

    except Exception as e:
        logging.error("画像取得でエラーが発生しました: %s", e.message)
        return False

def get_timestamp(now):
    return now.strftime("%Y%m%d%H") + str(now.minute / 5 * 5).zfill(2)

def get_token():
    global CLIENT_ID
    global CLIENT_SECRET
    global REFRESH_TOKEN
    url = "https://accounts.google.com/o/oauth2/token"
    params = {"client_id": CLIENT_ID,
              "client_secret": CLIENT_SECRET,
              "refresh_token": REFRESH_TOKEN,
              "grant_type": "refresh_token"}

    try:
        request = urllib2.Request(url, urllib.urlencode(params))
        response = urllib2.urlopen(request)
        token = json.load(response)["access_token"]
        logging.info("トークンを取得しました")
        return token

    except Exception as e:
        logging.error("トークン取得でエラーが発生しました: %s", e.message)

def send_mail(token, now):
    global MAIL_TO
    global MAIL_SUBJECT
    global MAIL_BODY
    global last_notified
    url = "https://www.googleapis.com/gmail/v1/users/me/messages/send"
    # メールを作成
    encoding = "utf-8"
    message = MIMEText(MAIL_BODY, "plain", encoding)
    message['to'] = MAIL_TO
    message['from'] = "me"
    message['subject'] = Header(MAIL_SUBJECT, encoding)
    data = {'raw': base64.urlsafe_b64encode(message.as_string())}
    # リクエスト送信
    try:
        request = urllib2.Request(url, json.dumps(data))
        request.add_header("Content-Type", "application/json")
        request.add_header("Authorization", "Bearer " + token)
        urllib2.urlopen(request)
        logging.info("メールを送信しました")
        last_notified = now

    except Exception as e:
        logging.error("メール送信でエラーが発生しました: %s", e.message)

if __name__ == "__main__":
    logging.basicConfig(level = logging.INFO, format = "%(asctime)s %(levelname)s %(message)s")
    while True:
        main()
        # 5分間隔で実行
        time.sleep(300)

Base64エンコードする際、URLセーフにするのがポイントではないでしょうか。
しかしglobalが多すぎる、これはひどい

DXライブラリをPowerShellから呼び出してみる

.NET Framework上で動作するスクリプト言語PowerShellでは、指定した.NETアセンブリを読み込んで使用できます。
今回はC#用として配布されているDXライブラリを使い、公式のサンプルを移植してみました。2.0以上で動きます。

コード

スクリプトと同一ディレクトリ内に必要なDLLを配置してください。
名前空間を省略するテクニックには目からウロコでした。これがないと毎回完全修飾名を書くことになるのでキツいです…

参考

Fiddlerでクロスドメイン制約を回避してlocalhostと通信する

ちょっと一般公開されているWebサイトから、クロスドメイン制約を回避してlocalhostと通信したくなりました。
そんな時にWindowsデバッグプロキシであるFiddlerが便利なんですよ。 FiddlerScriptに以下を追記して必要なヘッダーを追加します。
なんとこれ、今では黒歴史感がハンパないJScript.NETらしいです。

static function OnBeforeResponse(oSession: Session) {
    /* 省略 */
    oSession.oResponse["Access-Control-Allow-Origin"] = "*";
    oSession.oResponse["Access-Control-Allow-Headers"] = "*";
    oSession.oResponse["Access-Control-Allow-Methods"] = "*";
}  

既に該当ヘッダーがある場合は上書きしてくれます。
しかし対象ページがHTTPSで配信されている場合、そこからXMLHttpRequestでHTTPリクエストを送信することはできません。
localhost(Sinatra + WEBrick)はHTTPで配信しているので、これでは弾かれてしまいます。
そこで、今度はlocalhost宛のHTTPSリクエストをHTTPにリダイレクトさせましょう。

static function OnBeforeRequest(oSession: Session) {
    /* 省略 */
    if (oSession.HTTPMethodIs("CONNECT"))
    {
        oSession.oFlags["X-ReplyWithTunnel"] = "Fake for HTTPS Tunnel";
        return;
    }
    if (oSession.isHTTPS && oSession.HostnameIs("localhost"))
    {
        oSession.fullUrl = oSession.fullUrl.Replace("https://", "http://");
    }
}

同じように既存メソッドに追記してやります。
CONNECTメソッドに関しては、このように記述することでAutoResponder相当の動作をしてくれるそうです。

参考