2012年8月7日火曜日

Eclipse 4 アプリケーションの作成 (1) サービスの作成

久々のメジャーバージョンアップ版 Eclipse がリリースされて2ヶ月近くたちます。 新しく採用された Eclipse 4 プラットフォームでの開発は 3.x までとはかなり異なっています。 互換レイヤーにより、これまで同様の開発も可能ですが、新しいコンセプトを試すための簡単なアプリケーションを作成したいと思います。 このサンプルアプリケーションは、指定フォルダ以下のビデオファイルの一覧を表示して、選択ビデオを再生するというものです。 このアプリケーションでは、E4の以下の特徴を試します。
  • E4アプリケーションモデル
  • サービス・プログラミング・モデル
  • アノテーションと依存性注入
  • CSSによる外観の変更

最新の Eclipse IDE とともに、http://download.eclipse.org/e4/updates/0.12-I-builds から、e4 ツールをインストールしてください。 以下のうち、少なくとも、Eclipse e4 Tools はインストールします:


プロジェクトの新規作成
E4 アプリケーションを作成する簡単な方法は E4テンプレートを使用することです。 ウィザードでEclipse 4 アプリケーションプロジェクトを選択します(下図):


プロジェクト名は、com.itrane.myvideo、作成場所はデフォルト以外を指定します(下図):


それ以外はデフォルトのままにして、ウィザードを終了します。 作成されたE4アプリケーションプロジェクトは以下のようになります:


プロジェクトにはデフォルトのアプリケーションモデル(Application.e4xmi)が含まれていますが、UIコンポーネントの実装は含まれていません。 しかし、モデルはコンポーネントの実装がなくても実行することができます。

アプリケーションの起動
この状態で、ひとまず、アプリケーションを起動してみます。 製品構成ファイル(com.itrane.myvideo.product) を右クリックして、ポップアップメニューから Run As > Eclipse Application を選択します:


基本となる e4 アプリケーションモデルは、XMI ファイルに記述され、起動時に、アプリケーションに読み込まれます。実行中にユーザーが行ったUIに対する変更(サイズ変更など)はモデルに適用され、次回起動時はこの変更モデルが使用されます。 そのため、開発時に XMIファイルに対して行った変更がUIに反映されずに戸惑うことがあります。 XMIに対して変更を行う前に、あらかじめ起動構成を変更して、変更が確実に反映されるようにしておきます。 起動構成ダイアログを開いて、"Main" タブの "Workspace Data" セクションで "Clear" をチェックしてください(下図):

Common タブで、Shared file を選択して、保存バンドルを指定して、設定を保存します(下図):

これで、保存された起動構成(com.itrane.myvideo.product.launch)を使って起動すれば、起動時に以下の確認ダイアログが表示されます:


"Yes" を押せば Application.e4xmi で行ったUIの変更が確実に反映されるようになります。

※注意:ユーザー変更をモデルに適用すべきでない場合は、起動パラメータとして"-clearPersistedState" を指定できます。

サービスの作成
サービスは機能を提供するソフトウェアコンポーネントです。 E4 がサポートするサービス・プログラミング・モデルにより、ユーザー自身のサービスを定義して、使用することができます。 指定したフォルダ以下のビデオファイル一覧、ビデオ情報を提供するサービスを作成します。

新たに次のプラグインプロジェクトを作成します。
  • com.itrane.myvideo.videoservice
    サービスおよびファクトリの定義、ドメイン定義、ドメイン実装。
  • com.itrane.myvideo.videoservice.impl
    サービスおよびファクトリの実装。

com.itrane.myvideo.videoservice プラグインに、IVideoHome(ビデオサービスインターフェース)と、IVideoHomeFactory(ビデオサービスファクトリインターフェースを作成します。
com.itrane.myvideo.videoservice.IVideoHome:
public interface IVideoHome {
    public String getRoot();
    public void changeRoot(String rootPath);
    public String getCaptureDir();
    public String getCaptureFileName(IVideo video);
    public List<IVideo> getVideoList();
    public List<String> getArtists();
    public Map<String, IVideo> getVideoMap();
}

com.itrane.myvideo.videoservice.IVideoHomeFactory:
public interface IVideoHomeFactory {
    public IVideoHome createHome(String rootPath, String[] types);
}

com.itrane.myvideo.videoservice.domain パッケージを作成して、IVideo インターフェースを作成します。
com.itrane.myvideo.videoservice.domain.IVideo:
/**
 * ビデオインターフェース。
 * ファイル名によるグループ化をサポート。
 * ファイル名の例: "c:\video\music\Britney - Toxic.MP4" 
 */
public interface IVideo {

    /**
     * ファイルを返す.
     * @return java.io.File;
     */
    public File getFile();

    /**
     * ファイルのユニーク・キーを返す。 (例:Britney - Toxic.MP4)
     * @return ファイル名
     */
    public String getKey();
    
    /**
     * ファイル名からタイトルを抽出して返す.(例: Toxic)
     * @return タイトル
     */
    public String getTitle();
    
    /**
     * フォルダのパスを返す. (例: c:\video\music)
     * @return フォルダパス
     */
    public String getParentpath();
    
    /**
     * ファイル拡張子を返す. (例: mp4)
     * @return ファイル拡張子
     */
    public String getType();
        
    /**
     * ファイル名からグループ部を抽出して返す。(例:Britney)
     * @return
     */
    public String getGroup();
}

さらに、com.itrane.myvideo.videoservice.domain.impl パッケージを作成して、IVideo を実装した Video クラスを作成します。 Video は特定の形式("Artist - Music Title.flv", "Series-Episode.mp4" など)で名づけられたビデオファイルをグループ別(アーティスト別、シリーズ別)に分類できるように、ファイル名を解析してグループ名とタイトル名を返します
com.itrane.myvideo.videoservice.domain.impl.Video:
/**
 * IVideo を実装したビデオクラス.
 * <p>
 * ビデオファイルを特定のファイル名形式からアーティスト別にグループ化する機能を提供。</p>
 */
public class Video implements IVideo {
    
    private File fFile;
    private String fKey;
    private String fName;
    private String fGroup;
    private String fTitle;
    private String fType;
    
    /**
     * コンストラクタ.
     * @param file グループ化ファイル
     * @param type 拡張子
     */
    public Video(File file, String type) {
        fType = type;  
        fFile = file;
        fKey = file.getName();

        //拡張子を取り除いて、グループとその他の部分に分離
        splitName();
        
        if (fFile.getParentFile().getName().startsWith("_")) {
            // "_" で始まるディレクトリはグループフォルダ
            fGroup = fFile.getParentFile().getName();
        }
        
        // TODO タグを含むグループ名の場合は fName からさらにタグを抽出する
        //今は fName はタイトルしか含まない。
        fTitle = fName;
    }
    
    @Override
    public File getFile() {
        return fFile;
    }

    @Override
    public String getKey() {
        return fKey;
    }

    @Override
    public String getTitle() {
        return fTitle;
    }

    @Override
    public String getParentpath() {
        return fFile.getParentFile().getAbsolutePath();
    }

    @Override
    public String getType() {
        return fType;
    }

    @Override
    public String getGroup() {
        return fGroup;
    }
        
    /*
     * グループ名とタイトル部分に分離する.
     */
    private void splitName() {
        // 拡張子を取り除く
        String filename = fFile.getName();
        filename = filename.substring(0, filename.lastIndexOf('.'));

        String[] token = filename.split("\\s*-\\s*"); // -の両側に任意個のスペースを許す
        if (token.length >= 2) {
            String result = "";
            for (int i = 1; i < token.length; i++) {
                result += token[i];
            }
            fGroup = token[0];
            fName = result;
        } else {
            // ファイル名がグループを持っていない場合はフォルダ名を代わりに設定
            fGroup = fFile.getParentFile().getName();
            fName = filename;
        }
    }
    
}

MANIFEST.MF のランタイムページで、以下のパッケージをエクスポートします。



com.itrane.myvideo.videoservice.impl プラグインに、ビデオサービス(IVideoHome) の実装クラス VideoHome を作成します。
com.itrane.myvideo.videoservice.impl.VideoHome:
/**
 * ビデオサービスの実装.
 */
public class VideoHome  implements IVideoHome {

    public static final String NOTE_DIR = "__note/";
    private String fRoot;
    private Map<String, IVideo> fVideoMap; // ビデオキーとビデオのマップ
    private List<IVideo> fVideoList; // ファイル名順にソートされたビデオリスト
    private List<String> fGroupList; // 名前順にソートされたビデオグループリスト
    private String[] fVideoTypes;
    
    /**
     * コンストラクタ.
     * ルートフォルダの設定、各種マップ等の初期化を行う。
     * @param root         ルートフォルダ
     * @param videoTypes   対象とするビデオのタイプ(例:divx, mp4 など)
     */
    public VideoHome(String root, String[] videoTypes) {
        this.fVideoTypes = videoTypes;
        this.fRoot = root;
        if (!this.fRoot.endsWith("/")) {
            this.fRoot = this.fRoot + "/";
        }
    }

    /**
     * ルートディレクトリを変更する.
     * マップをクリアして、リスナーにルート変更を通知する
     * @param root
     */
    @Override
    public void changeRoot(String root) {
        if (root!=null && !root.equals("")) {
            this.fRoot = root;
            if (!this.fRoot.endsWith("/")) {
                this.fRoot = this.fRoot + "/";
            }
            fVideoList = null;
            fGroupList = null;
        }
        // リスナー設定
    }

    /**
     * ビデオ情報の取得.
     * @return ビデオリスト:指定されたビデオタイプに合致するルート下(サブフォルダも含む)の全ビデオのリスト
     */
    @Override
    public List<IVideo> getVideoList() {

        if (fVideoList == null) {

            // ビューファイルのキーからビューファイルを取得するためのマップ
            fVideoMap = new HashMap<String, IVideo>();

            // フォルダをスキャン
            File root = new File(fRoot);
            if (!root.exists()) 
                return new ArrayList<IVideo>();
            
            scanVideoDir(root);

            // ファイ名順にソート
            fVideoList = new ArrayList<IVideo>(fVideoMap.values());
            Collections.sort(fVideoList, new Comparator<IVideo>() {

                @Override
                public int compare(IVideo video0, IVideo video1) {
                    String key0 = video0.getGroup() + video0.getTitle();
                    String key1 = video1.getGroup() + video1.getTitle();
                    return key0.compareTo(key1);
                }
            });
        }
        return fVideoList;
    }
    
    private void scanVideoDir(File dir) {
        if (!dir.isDirectory()) {
            //ディレクトリでない場合は戻る
        }
        if (dir.getName().startsWith("__")) {
            //-- で始まるディレクトリはビデオディレクトリではないのでスキップ
            return;
        }
        
        //指定ディレクトリの全ファイルをスキャン
        File[] files = dir.listFiles();
        if (files!=null) {
            for (File file: files) {
                if (file.isDirectory()) {
                    scanVideoDir(file);
                } else {
                    addVideo(file);
                }
            }
        }
    }
    
    private void addVideo(File file) {
        String type = getType(file.getName(),
                fVideoTypes);

        if (type != null) {
            IVideo video = new Video(file, type);
            fVideoMap.put(video.getKey(), video);
        }
    }
    
    private String getType(String filename, String[] types) {
        String lowerName = filename.toLowerCase();
        for (String type : types) {
            if (lowerName.endsWith(type)) {
                return type;
            }
        }
        return null;
    }
    

    /**
     * アーティストリストを取得する.
     * ビデオリスト内の各ビデオファイル名からアーティスト名を抽出してリストにする。
     * @return アーティストリスト
     */
    @Override
    public List<String> getArtists() {
        if (fGroupList == null) {
            Set<String> aset = new TreeSet<String>();
            for (IVideo v : getVideoList()) {
                aset.add(v.getGroup());
            }
            fGroupList = new ArrayList<String>(aset);
        }
        return fGroupList;
    }

    @Override
    public Map<String, IVideo> getVideoMap() {
        return fVideoMap;
    }

    /**
     * ビデオのキーからキャプチャファイルの名前を作成する.
     * @param video
     * @return キャプチャファイル名
     */
    @Override
    public String getCaptureFileName(IVideo video) {
        return video.getKey().toLowerCase()
                .replace(video.getType(), "png");
    }

    /**
     * ビデオのビデオグループとタイトルからノートファイル名を作成する
     * @param video
     * @return ノートファイル名
     */
    public String getNoteFileName(IVideo video) {
        return (video.getGroup() + "-" + video.getTitle() + ".txt")
                .toLowerCase();
    }

    @Override
    public String getRoot() {
        return fRoot;
    }

    @Override
    public String getCaptureDir() {
        return fRoot + "__img/";
    }
}
上記ビデオサービスのインスタンスを生成する、ビデオサービスファクトリ(IVideoHomeFactory) の実装クラスを作成します。
com.itrane.myvideo.videoservice.impl.VideoHomeFactory:
public class VideoHomeFactory implements IVideoHomeFactory {

    public IVideoHome createHome(String rootPath, String[] types) {
        return new VideoHome(rootPath, types);
    }
}
MANIFEST.MF のランタイムページで、以下のパッケージをエクスポートします。

作成したサービスを com.itrane.myvideo プラグインで利用できるように、ビデオサービスファクトリを OSGi サービスとして宣言します。
com.itrane.myvideo.videoservice.impl プラグインの OSGI-INF フォルダに 次の videoHomeProvider.xml を作成します:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="com.itrane.video.videoservice.impl">
   <implementation class="com.itrane.myvideo.videoservice.impl.VideoHomeFactory"/>
   <service>
      <provide interface="com.itrane.myvideo.videoservice.IVideoHomeFactory"/>
   </service>
</scr:component>

この宣言を有効にするには、com.itrane.myvideo.videoservice.impl プラグインの MANIFEST.MFに次の2行を追加することが必要です。
Service-Component: OSGI-INF/videoHomeProvider.xml
Bundle-ActivationPolicy: lazy

これで、指定フォルダ下のビデオ情報を提供するサービスが作成され、OSGiサービスとして登録されました。次回は、このサービスを利用して、ビデオ一覧を表示するビューを作成します。

0 件のコメント: