承認これくしょん

my black histories

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外字を含むフォントがおすすめです。

参考