承認これくしょん

my black histories

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相当の動作をしてくれるそうです。

参考

JJUG CCC 2015 Springに参加してまいりました

Javaを始めたばかりの修行の身ではありますが、この私も先日4/11(土)の JJUG CCC 2015 Spring に参加してまいりました。
たくさんの学びと刺激をいただきました。スピーカーの方々、幹事の皆様本当にありがとうございました!
以下レポっす。チラシの裏すんません。

受付を済ませてふと隣を見ると、3DプリンタDukeくんが今まさに生まれようとしているところでした。
Dukeくんかわいい!一番好きなマスコットキャラクターです!
周りにはナウでヤングな方々から渋いオジサマまで、年齢層が幅広い。20年の歴史を感じます。

CD-2 はまる!JPA(初学者向けライト版)

普段は素のJDBCのコードしか触れないので、イマドキの方法を知りたいと思い参加しました。
最初のエンティティの状態遷移図のおかげで、はまりポイントの原因がイメージしやすかったです。
「削除だけできません問題」を見たとき、私もmerge()すればできるんじゃ…って思ってました。
ダメなんですね。自分で考えたことと解説がリンクしてスッと頭に入ってきました。
NetBeansがオススメとのこと。

G-3 新人エンジニア奮闘記「Javaって何?からwebサービスを公開するまで」

歳は違いますが昨年入社で2年目という、私と似たようなスタートを切った方の発表でした。
とても堂々とされているように見えて、社内での練習時に比べて緊張されていたとは思えませんでした。
Seasar2をお使いなんですね。1年目でしかも一人でサービス立ち上げとは本当にすごいと思います。
周りの先輩方に相談できる環境はちょっとうらやましいです。
質問タイムでは私の隣に座っていた方が2年目、次に挙手された方も2年目とおっしゃっていて驚きました。
自分もがんばろう!という元気をいただいたセッションでした。

G-6 Java開発の強力な相棒として今すぐ使えるGroovy

Groovy JDKのおかげで、JavaのオブジェクトをそのままLL風に操作できるのがいい感じ。
ClosureはRubyのblock, Proc, lambdaに相当するんでしょうか。パッと見こちらの方が使いやすそう。
標準でPower Assertもついてくるし、至れり尽くせりですね。
ResultSet#nextとか毎回書くのつらいなーと思ってたので、Groovy SQLは特に魅力的でした。

G-7 JavaFXグラフィックスとアニメーション入門 デスクトップにアナログ時計を出してみよう

先日ちょっとだけJavaFX触ったのですが、シーングラフについてよく分かっていませんでした。
Sceneの下にはParentが必要で、これが各種Paneだったりするんですね。AWTやSwing触った人なら当たり前の知識なのでしょう。
標準のAPIで強力なアニメーションやグラフィックスを扱えるので、結構インタラクティブなコンテンツに向いてそう。
そしてやっぱりNetBeansでした。結構評判いいのでは…?

感想

一応私も業界の片隅にいるのですが、普段技術的な話を聞ける機会がないので非常に新鮮な経験でした。
いずれは話を聞くだけでなく、実際に使ってみたと言えるようになりたいです。
安易に環境を理由にしてはいけませんが、もっと様々なことに取り組めたら仕事も楽しくなりそうですね。

Oracleで特定のテーブル・ビューに依存するビューの一覧を取得する

ビューを参照するビュー、つまり多重ビューがあるとテーブルのスキーマ変更も一苦労です。そもそもアンチパターンでは…?
とはいえ、既に作られてしまったなら立ち向かうしかない。とにかく依存関係を明らかにしなければ。
そこでテーブル・ビュー名を指定すると、それに依存するビューの一覧を出力するスクリプトを作りました。
Oracle専用です。動作確認には11g XEを使いました。

ソース (viewFinder.js)

var Properties = java.util.Properties;
var OracleDriver = Packages.oracle.jdbc.OracleDriver;
var PreparedStatement = java.sql.PreparedStatement;

var queries = {
  dependencies: "SELECT NAME FROM USER_DEPENDENCIES WHERE TYPE = 'VIEW' AND REFERENCED_NAME = ? ORDER BY NAME",
  views: 'SELECT VIEW_NAME, TEXT FROM USER_VIEWS WHERE VIEW_NAME IN '
};
var viewNames = [];

// JDBC接続設定
var url = 'jdbc:oracle:thin:@localhost:1521:xe';  // 接続文字列
var user = 'scott'; // ユーザーID
var password = 'tiger'; // パスワード

if (arguments[0]) {
  var arg = arguments[0];
  var prop = new Properties();
  prop.setProperty('user', user);
  prop.setProperty('password', password);

  try {
    var conn = new OracleDriver().connect(url, prop);
    print(arg);
    printTree(arg, '');

    if (viewNames.length > 0) {
      queries.views += '(' + repeat('?', viewNames.length, ',') + ') ORDER BY VIEW_NAME';
      query(queries.views, viewNames, function(rs) {
        var viewName = rs.getString('VIEW_NAME');
        var text = rs.getString('TEXT');
        print('\r\n' + viewName + '\r\n' + repeat('-', viewName.length) + '\r\n' + text);
      });
    }

  } finally {
    if (conn) {
      try {
        conn.close();
      } catch(e) {}
    }
  }
}

function printTree(name, prefix) {
  var items = query(queries.dependencies, [name], function(rs) {
    return rs.getString('NAME');
  });

  items.forEach(function(item) {
    if (viewNames.indexOf(item) == -1) {
      viewNames.push(item);
    }
  });

  if (items.length > 0) {
    for (var i = 0, length = items.length; i < length; i++) {
      var item = items[i];
      if (i == length - 1) {
        print(prefix + '└─' + item);
        printTree(item, prefix + '    ');
      } else {
        print(prefix + '├─' + item);
        printTree(item, prefix + '│   ');
      }
    }
  }
}

function query(query, params, callback) {
  try {
    var ps = conn.prepareStatement(query);
    if (params) {
      for (var i = 0, length = params.length; i < length; i++) {
        var param = params[i];
        ps.setObject(i + 1, param);
      }
    }
    var arr = [];
    var rs = ps.executeQuery();
    while (rs.next()) {
      arr.push(callback(rs));
    }
    return arr;
  } finally {
    if (ps) {
      try {
        ps.close();
      } catch(e) {}
    }
  }
}

function repeat(str, times, separator) {
  var newStr = '';
  for (var i = 0; i < times; i++) {
    if (separator && i > 0) newStr += separator;
    newStr += str;
  }
  return newStr;
}

実行方法

まずはojdbc6.jarことOracle JDBC Thin Driverを用意。
そしてJDK付属のjrunscript、JREにも入ってるjjsコマンドのいずれかで実行します。

jrunscript -cp ojdbc6.jar viewFinder.js (テーブル・ビュー名)
jjs -cp ojdbc6.jar viewFinder.js -- (テーブル・ビュー名)

出力イメージ

treeコマンド風にビューの依存関係を、その下に各ビューのソースを出力します。

EMP
└─EMP1
    ├─EMP2
    │   └─EMP4
    └─EMP3

EMP1
----
SELECT "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" FROM EMP

EMP2
----
SELECT "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" FROM EMP1

EMP3
----
SELECT EMPNO FROM EMP1

EMP4
----
SELECT JOB FROM EMP2

参考