Groovyでテーブルのデータを取得したい(2)

GroovyでOracleデータベースに接続して、データを取得するサンプルです。

import java.text.*
def sql = groovy.sql.Sql.newInstance(
'jdbc:oracle:thin:@localhost:1521:ORCL','username','password','oracle.jdbc.driver.OracleDriver')

def username = "create_user"
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
sql.eachRow("SELECT table_name FROM user_tables WHERE table_name LIKE 'BIZ_AB_%'"){ rs->
    def rows = sql.rows("SELECT * FROM " + rs.table_name +" WHERE create_user_cd ='" + username + "'")
    if( rows.size() > 0 ){
        def f = new File(rs.table_name+".csv").newWriter("Shift_JIS")
        boolean isWroteHeader = false
        for(r in rows){
            r["CREATE_DATE"] = sdf.format(r["CREATE_DATE"].dateValue())
            r["RECORD_DATE"] = sdf.format(r["RECORD_DATE"].dateValue())
            if(!isWroteHeader){
                f.writeLine( r*.key.join(",") )
                isWroteHeader = true
            }
            f.writeLine( r*.value.join(",") )
        }
        f.close()
    }
}


前回のVBAのようにExcelに書き出す方式ではなく、まずはCSVファイルに出力する方式にしました。
簡単に書ける上、コードが短いのですっきりします。
OracleのTimeStamp型が変換しないとオブジェクトの文字列になってしまうため、変換しています。


調べてみると、DataSet型というものがあって、

def rows = sql.rows("SELECT * FROM " + rs.table_name +" WHERE create_user_cd ='" + username + "'")

は、

//def mySet = sql.dataSet("SELECT * FROM " + rs.table_name).findAll{ it.create_user_cd == ${username} } ←間違い(^^;
def mySet = sql.dataSet(rs.table_name).findAll{ it.create_user_cd == ${username} }

といったような書き方ができるらしいのですが、以下のエラーが表示され、どうしてもうまくいきません。。。


Caught: groovy.lang.GroovyRuntimeException: Could not find the ClassNode for MetaClass:
org.codehaus.groovy.runtime.metaclass.ClosureMetaClass@1142196[class DbTools$_test2_closure4]


検索してみると、http://jira.codehaus.org/browse/GROOVY-2450 がヒットするのですが、このチケットはクローズされています。
他のGroovy関連のページでは、DataSetのfindAllは沢山見かけるのですが、皆さんは正常に実行できているのでしょうか。。。
最初は、Groovyのバージョンがいけないのだと思い、最新安定バージョンの1.7.10を実行してNG。
1.5.4/1,5.8もNG。さらに1.8.0-RC4まで試して全部NG。


簡単なSELECT操作とINSERT操作であれば、DataSetを使用したら簡単そうなので、ぜひ使用したいところなんですが。。。
何か情報が分かりましたらUPしたいと思います。
今度は、EXCELへ出力するように改造すべく、Scriptomにチャレンジする予定です。

Groovyでテーブルのデータを取得したい(1)

以前の日記で、Groovyがどうのこうの言っていた気がするのですが、最近忙しくて日記の更新もままならない状態です。
私の普段の作業で定型化できるような作業のうち、そのほとんどがExcelVBAのお世話になっているのですが、
どうせなら気になっているGroovyでどこまでできるのか試してみたくなりました。勉強にもなるし。
手始めにテーブルのデータを取得するようなVBAをGroovyで書いたらどんな風になるのか試してみることに。


今日は、変換元になるVBAを記載しておきます。

Public Sub getTableData()

    Dim objDB As New ADODB.Connection
    Dim objRS As ADODB.Recordset
    Dim strSQL As String
    
    Dim bizTableCollection As Collection
    Dim table_name As Variant
    
    objDB.Open "dsn=myDSN; uid=userid; pwd=password"
    'カウント数を取りたいのでクライアントサイドカーソールに変更
    objDB.CursorLocation = adUseClient
    Set objRS = objDB.Execute("SELECT table_name FROM user_tables WHERE table_name LIKE 'BIZ_AB_%';")
    
    Set bizTableCollection = New Collection
    Do Until objRS.EOF
        bizTableCollection.Add (objRS(0))
        objRS.MoveNext
    Loop

    For Each table_name In bizTableCollection
        
        sSql = "SELECT * FROM " & table_name & " WHERE CREATE_USER_CD = '" & ActiveWorkbook.Worksheets("Main").Cells(7, 2) & "'"
        'SQLの実行
        Set objRS = objDB.Execute(sSql)
        
        If objRS.RecordCount > 0 Then
           'ワークシートの準備
            make_worksheet (table_name)
            rownum = 2

           '列名を出力する
            For colnum = 1 To objRS.Fields.Count
                ActiveWorkbook.Worksheets(table_name).Cells(1, colnum) = objRS(colnum).name
            Next
            'データを出力する
            Do Until objRS.EOF
                For colnum = 1 To objRS.Fields.Count
                    ActiveWorkbook.Worksheets(table_name).Cells(rownum, colnum) = objRS(colnum).Value
                Next
                objRS.MoveNext
                rownum = rownum + 1
            Loop
        End If
    Next

    If Err.Number <> 0 Then
        MsgBox Err.Description
    End If

    'ADOをクローズします
    objDB.Close

    'オブジェクトの破棄
    Set objDB = Nothing

    MsgBox "終了しました。"

End Sub

Private Sub make_worksheet(name As String)

    '探してあれば削除
    For Each sh In ActiveWorkbook.Worksheets
        If sh.name = name Then
            Application.DisplayAlerts = False
            sh.Delete
            Application.DisplayAlerts = True
        End If
    Next
    '新しいワークシートの作成
    ActiveWorkbook.Worksheets("Template").Visible = xlSheetVisible
    ActiveWorkbook.Worksheets("Template").Select
    ActiveWorkbook.Worksheets("Template").Copy After:=Sheets(Sheets.Count)
    Set NewSheet = ActiveWorkbook.Worksheets("Template (2)")
    NewSheet.Select
    NewSheet.name = name
    
    ActiveWorkbook.Worksheets("Template").Visible = xlSheetHidden

End Sub


ADODBを使用していますが、いつも忘れる参照設定も載せておきます。



これからGroovyで実装してみるのですが、ExcelVBAがかなり良くできているので
今回のケースだと優位点がでないかなと思います。

メールでJenkinsにコマンドを送りたい(5) 〜Groovy対応〜

Groovy対応って言ってますが、たいした物ではありません。
いやいや、威張る所ではない(笑)のですが、基本方針は以下の通り。

  • CLIコマンドとして「groovy」を見つけたら、メール本文をファイルに保存
  • ファイルの保存先は、ビルドごとのフォルダ(つまり重複しない)
  • CLIクラスに渡す時に、「groovy」の引数として先ほど保存したファイルのフルパスを渡す


実装ですが、全体としては前回載せているので、メールの件名判定ループ部分だけ。

    public String receive(AbstractBuild build, BuildListener listener) {
        //〜〜〜省略〜〜〜
            for (Message message : messages) {
                String findStr = message.getSubject().split(" ")[0];
                if (commands.indexOf(findStr) != -1) {
                    subject = message.getSubject();
                    InternetAddress[] addresses = (InternetAddress[]) message.getFrom();
                    saveFile(build, "tmp.address", addresses[0].getAddress());
                    if ("groovy".indexOf(findStr) != -1)
                        saveFile(build, "groovy.script", (String) message.getContent());
                    message.setFlag(Flag.DELETED, true);
                    break;
                }
            }
        //〜〜〜省略〜〜〜
    }

上のsaveFileは以下のような簡単なファイル保存のロジックです。

    private void saveFile(AbstractBuild build, String filename, String contents) {
        try {
            FileOutputStream fos = new FileOutputStream(new File(build.getRootDir(), filename));
            OutputStreamWriter osw = new OutputStreamWriter(fos);
            BufferedWriter bw = new BufferedWriter(osw);
            bw.write(contents);
            bw.close();
            osw.close();
            fos.close();
        } catch (IOException ioe) {
            System.err.println(ioe.getMessage());
        } finally {
        }
    }

<recieveメソッドについて>
以下の部分で、メール本文を保存しています。

if ("groovy".indexOf(findStr) != -1)
    saveFile(build, "groovy.script", (String) message.getContent());

ただし、現在は文字コードなどを考慮せずに保存しているので、ダブルバイトコードが存在すると問題が発生する恐れがあります。


さて実験の時間です。下のような状況の(若干寂しい感じ)のJenkinsが動作している環境があったとします。


件名に「groovy」本文に好きなgroovyスクリプト(こちらも寂しいが。)を書いた以下のようなメールを送信すると、、、


以下のように、groovyスクリプトの実行結果がメールで送られてきます。


もちろん、携帯でも!


最初のぼんやりとした発想時点では、手元に携帯くらいしかないときにメールか何かでコマンド発行できたら
便利かなというのがあったので、携帯はありだと思っていました。


なんとか目標のところまでたどり着くことができました。
このプラグインは、jenkins-ci.orgにホスティングしましたので、インストールして試してみるも、
ソースをいじって自分なりに楽しんでみるのもご自由にどうぞ。

メールでJenkinsにコマンドを送りたい(4) 〜Trigger編〜

コマンドメールの送信、および結果のメール受信までたどり着きました。
実際に使用してみると、これはこれで問題なく動作するのですが、ずっと動かしているとビルド履歴が大変なことに。。。


SCMのポーリング機能を思い出しました。
SCMのポーリング機能は、SCMに変更があるかどうかをまず確認し、変更がなければビルドを実行しない仕様になっています。
つまり、余計なビルド履歴が残りません。この仕様(実装も)を流用できないかと考えました。
基本仕様はSCMポーリングそのままに、CLIメールの到着だけ確認して到着してなければMailCommanderを実行しないようにします。
また、ポーリングのログも見たいと思いましたので、Actionも拡張することにします。
参考にするソースやリソースは、hudson.triggers.SCMTriggerです。



以下がconfig.jellyとなります。SCMTriggerで使用しているconfigと同じにしました。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="Schedule" field="spec"  help="/descriptor/hudson.triggers.TimerTrigger/help/spec">
    <f:textarea />
  </f:entry>
</j:jelly>

以下がhelp_ja.htmlです。

<div>
  CLIコマンドの書かれた電子メールの到着を監視します。
</div>

上の2つを用意すると以下のようなイメージとなります。


以下がindex.jellyとなります。プロジェクトのトップページを表示したときに、左側のメニューに表示させるための設定となります。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <l:layout>
    <st:include it="${it.owner}" page="sidepanel.jelly" />
    <l:main-panel>
      <h1>${%title(it.displayName)}</h1>
      <j:set var="log" value="${it.log}" />
      <j:choose>
        <j:when test="${empty(log)}">
          ${%Polling has not run yet.}
        </j:when>
        <j:otherwise>
          <pre>
            <st:getOutput var="output" />
            <j:whitespace>${it.writeLogTo(output)}</j:whitespace>
          </pre>
        </j:otherwise>
      </j:choose>
    </l:main-panel>
  </l:layout>
</j:jelly>

上のリソースが加わって、以下の図のように、左側のメニューの設定の下に表示されます。


以下が実装になります。ちょっと長いのですが、MailCommanderTriggerのrunメソッド以外は、ほとんどSCMTriggerの内容と同じです。
MailCommandTriggerのrunメソッドでやっていることは、主に3つです。
1)MailCommandBuilderの設定画面で入力されたPOP3サーバの設定をとってきています。
  方法が分からなかったので、設定ファイルを直読みしています(←多分よくないだろうな。。。)
2)取得した設定で、メールサーバに問い合わせします。
  CLIの書かれたメールがあれば、

if (isCommandExist)
    job.scheduleBuild(0, new MailCommandTriggerCause());

 のように、ビルドを即時にスケジュールします。
3)最後に、ポーリングログを吐き出しています。

public class MailCommandTrigger extends Trigger<SCMedItem> {

    private static final Logger LOGGER = Logger.getLogger(MailCommandTrigger.class.getName());

    @DataBoundConstructor
    public MailCommandTrigger(String spec) throws ANTLRException {
        super(spec);
    }

    @Override
    public Collection<? extends Action> getProjectActions() {
        return Collections.singleton(new MailCommandAction());
    }

    public File getLogFile() {
        return new File(job.getRootDir(), "mailcommander-polling.log");
    }

    @SuppressWarnings("unchecked")
    @Override
    public void run() {

        String address = null, port = null, username = null, password = null;
        boolean isCommandExist = false;
        try {
            FileInputStream fis = new FileInputStream(new File(job.getRootDir(), "config.xml"));
            Document dom = new SAXReader().read(fis);

            Element mailcommander =
                dom.getRootElement().element("builders").element(MailCommandBuilder.class.getName());
            address = mailcommander.elementText("address");
            port = mailcommander.elementText("port");
            username = mailcommander.elementText("username");
            password = mailcommander.elementText("password");
            fis.close();
        } catch (IOException ioe) {
            ioe.printStackTrace();

        } catch (DocumentException e) {
            e.printStackTrace();
        }

        Properties props = new Properties();
        Session sess = Session.getDefaultInstance(props);
        Store store;
        try {
            store = sess.getStore("pop3");
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
            return;
        }

        StringBuffer commands = new StringBuffer();
        for (CLICommand c : CLICommand.all())
            commands.append(c.getName());

        try {
            store.connect(address, Integer.valueOf(port), username, password);

            Folder rootFolder = store.getDefaultFolder();
            Folder inbox = rootFolder.getFolder("INBOX");
            inbox.open(Folder.READ_ONLY);

            Message[] messages = null;
            if (inbox.getMessageCount() > 10)
                messages = inbox.getMessages(inbox.getMessageCount() - 10 + 1, inbox.getMessageCount());
            else
                messages = inbox.getMessages();
            FetchProfile fp = new FetchProfile();
            fp.add("Subject");
            inbox.fetch(messages, fp);
            for (Message message : messages) {
                String findStr = message.getSubject().split(" ")[0];
                if (commands.indexOf(findStr) != -1) {
                    isCommandExist = true;
                    break;
                }
            }
            inbox.close(true);
            store.close();
        } catch (MessagingException e) {
            e.printStackTrace();
            return;
        }

        if (isCommandExist)
            job.scheduleBuild(0, new MailCommandTriggerCause());

        try {
            StreamTaskListener listener = new StreamTaskListener(getLogFile());

            try {
                PrintStream logger = listener.getLogger();
                logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date()));
                if (isCommandExist)
                    logger.println("Changes found");
                else
                    logger.println("No changes");
            } catch (Error e) {
                e.printStackTrace(listener.error("Failed to record Mail Commander polling"));
                LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e);
                throw e;
            } catch (RuntimeException e) {
                e.printStackTrace(listener.error("Failed to record Mail Commander polling"));
                LOGGER.log(Level.SEVERE, "Failed to record Mail Commander polling", e);
                throw e;
            } finally {
                listener.close();
            }
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Failed to record Mail Commander polling", e);
        }

    }

    @Extension
    public static class DescriptorImpl extends TriggerDescriptor {
        public boolean isApplicable(Item item) {
            return item instanceof BuildableItem;
        }
        public String getDisplayName() {
            return Messages.MailCommandTrigger_DisplayName();
        }
        public FormValidation doCheck(@QueryParameter String value) {
            return doCheckSpec(value);
        }
        public FormValidation doCheckSpec(@QueryParameter String value) {
            try {
                String msg = CronTabList.create(fixNull(value)).checkSanity();
                if (msg != null)
                    return FormValidation.warning(msg);
                return FormValidation.ok();
            } catch (ANTLRException e) {
                return FormValidation.error(e.getMessage());
            }
        }
    }
    public static class MailCommandTriggerCause extends Cause {
        @Override
        public String getShortDescription() {
            return "Mail Commander Trigger";
        }
        @Override
        public boolean equals(Object o) {
            return o instanceof MailCommandTriggerCause;
        }
        @Override
        public int hashCode() {
            return 5;
        }
    }
    public final class MailCommandAction implements Action {
        public AbstractProject<?, ?> getOwner() {
            return job.asProject();
        }
        public String getIconFileName() {
            return "clipboard.gif";
        }
        public String getDisplayName() {
            return "Mail Command Action";
        }
        public String getUrlName() {
            return "mailcommandPollLog";
        }
        public String getLog() throws IOException {
            return Util.loadFile(getLogFile());
        }
        public void writeLogTo(XMLOutput out) throws IOException {
            new AnnotatedLargeText<MailCommandAction>(getLogFile(), Charset.defaultCharset(), true, this).writeHtmlTo(0,out.asWriter());
        }
    }
}


と、ここまで作成してきて、ふと疑問。
今回の要件のような、BuilderやTrigger、Publisherのように単独ではあまり意味をなさないようなプラグインって
1つのクラスで実装して、TriggerやPublisherはBuilderの内部クラスにまとめてしまったほうが良いのでしょうか。


ともあれ、この実装により定期実行をずっとしていてもビルド履歴が無駄に溜まる心配がなくなりました。
最後に、Builderを少し改良してgroovyシェルを実行できるようにしたいと思います。

メールでJenkinsにコマンドを送りたい(3) 〜Publisher編〜

前日までの実装では、メールの件名(subject)に書かれた内容をCLIコマンドとして認識し、
実行するところまでできました。ですが、このままだと実行結果が分かりません。
もともと思い描いていた構想の2番目は、


2.成功・失敗を問わず、実行結果をコマンド送信元のメールアドレス宛てに送信したい

    • 標準のメール通知だと成功時にメールが飛ばない
    • Publisherを拡張したらできそう?


という感じで、成功や失敗を問わずに結果を知りたいし、コマンド送信を行ったアドレスに対して
結果が通知されてほしいのです。というわけで、Publisherを拡張して専用の通知を作成したいと思います。
今回の設定画面では、チェックだけ付けられればOKなので、以下のような(ほぼ空の)config.jellyを用意しました。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
</j:jelly>

また、リソースとしてhelp_ja.htmlファイルを置いておくと、説明として認識されます。
上で設定したチェックボックスの右側に?アイコンがついて、クリックすると説明が展開される仕組みのようです。
help_ja.htmlの内容は、以下のようなものです。

<div>
  Mail Commander実行後、コマンドの送信元にジョブの結果をメールします。
</div>

help_ja.html上は日本語圏向けなので、それ以外用としてhelp.htmlも用意しておきます。


Publisherの書き方は、サンプルに無いので分からないのですが、標準で用意されているメール通知自体が
Publisherを継承している(正確にはPublisherを継承しているNotifierをさらにMailerが継承している)ので、
hudson.tasks.Mailerを参考にして作成しました。

public class MailCommandPublisher extends Publisher {
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {

        File logfile = build.getLogFile();
        StringBuffer logbuf = new StringBuffer();
        try {
            FileReader in = new FileReader(logfile);
            BufferedReader br = new BufferedReader(in);
            String line;
            while ((line = br.readLine()) != null) {
                logbuf.append(line);
                logbuf.append("\n");
            }
            br.close();
            in.close();
        } catch (IOException e) {
            listener.getLogger().println(e);
            return false;
        }

        File addressFile = new File(build.getRootDir(), "tmp.address");
        String to_address = null;
        if (addressFile.exists()) {
            try {
                FileReader in = new FileReader(addressFile);
                BufferedReader br = new BufferedReader(in);
                String line;
                while ((line = br.readLine()) != null) {
                    to_address = line;
                }
                br.close();
                in.close();
            } catch (IOException e) {
                listener.getLogger().println(e);
                return false;
            }
            if (to_address != null) {
                try {
                    MimeMessage msg = new MimeMessage(Mailer.descriptor().createSession());
                    msg.setRecipients(RecipientType.TO, to_address);
                    msg.setFrom(new InternetAddress(Mailer.descriptor().getAdminAddress()));
                    msg.setSubject("This is a result of mail command");
                    msg.setSentDate(new Date());
                    msg.setText(logbuf.toString());
                    Transport.send(msg);
                } catch (MessagingException mex) {
                    listener.getLogger().println(mex);
                    mex.printStackTrace();
                    return false;
                }
            }
        }
        return true;
    }

    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }
    public static DescriptorImpl DESCRIPTOR;

    public static DescriptorImpl descriptor() {
        return Hudson.getInstance().getDescriptorByType(MailCommandPublisher.DescriptorImpl.class);
    }

    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
        public DescriptorImpl() {
            load();
            DESCRIPTOR = this;
        }
        public String getDisplayName() {
            return Messages.MailCommandPublisher_DisplayName();
        }
        @Override
        public Publisher newInstance(StaplerRequest req, JSONObject formData) {
            MailCommandPublisher m = new MailCommandPublisher();
            return m;
        }
        @Override
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return true;
        }
    }
}


<performメソッドについて>
やっていることは大きく2つで、1つ目は、buildのログを取得してStringBufferに溜め込むことです。
2つ目はメール送信元に対して、StringBufferの内容をメールで送信しています。
メールの送信元は、Builderでしか分からないのでBuilderでメールのFromアドレスをファイルに保存し、
そのファイルの内容をPublisherで開いて宛先にセットしています。(←この方法が良いかどうか。。。不明)
JavaMailの送信では(おそらく他の送信プログラムでも)、Fromアドレスは必須となっているため
Jenkinsのシステム設定にある管理者のメールアドレスを利用しました。⇒Mailer.descriptor().getAdminAddress();
ここに適切なアドレスが指定されていないと送信エラーとなります。上のコードではエラーチェックしてませんが。


今回はわざと間違ったコマンドを送信してみたいと思い、件名に「build -s -p aaa=bbb」として送信してみました。

すると、以下のようなメッセージが帰ってきます。



いままで作成したBuilderとPublisherを組み合わせれば、やりたいことが出来ることがわかったのですが、あくまで定期的に実行しないと駄目です。
標準の定期実行でこのジョブを実行すると、該当するメールが無かった場合もビルド履歴として残ってしまいます。
SCMのポーリングというトリガーを利用すると、SCMに変更が無い場合はビルドが実行されず、余計なビルド履歴が残りません。
次回は、Triggerを使用してこの問題をクリアしたいと思います。

メールでJenkinsにコマンドを送りたい(2) 〜Builder編〜

昨日の日記で書いたのは、


1.メールの件名(subject)にコマンドを書いて専用のアドレス宛てに送信することでJenkinsに実行させたい

    • コマンドの形式はCLIを流用すれば楽できそう
    • Builderの拡張としてプラグインを作成して、一定間隔で実行させればいいかな


だったので、サンプルプロジェクトをちょこっと修正したら実現できそうです。
実際にやった作業を箇条書きしておくことにします。

  • プラグインプロジェクトの作成(mvn -cpu hpi:create) ※参考(id:sikakura:20110314)
  • サンプルのクラス名を変更(リファクタリング)HelloWorldBuilder⇒MailCommandBuilder
  • リソースの削除(global.jelly)および内容変更(config.jelly)
  • MailCommandBuilderの実装


今回のプラグインは、MailCommandBuilderというシンプルなものにしました。
また、global.jellyは、システムの設定で入力させたいパラメータがある場合に記載するので、今回は不要と判断して削除しました。
同時にサンプルに存在していたコーディングも削除しています。config.jellyは使用します。
メールクライアントの実装としてPOP3を考え、メールサーバホスト、ポート番号、ユーザ、パスワードをジョブの設定画面で
指定してもらう想定としました。よって、以下のようなconfig.jellyを作成しました。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="POP3 mail server address" field="address">
    <f:textbox />
  </f:entry>
  <f:entry title="POP3 mail server port" field="port">
    <f:textbox default="110"/>
  </f:entry>
  <f:entry title="POP3 User Name" field="username">
    <f:textbox />
  </f:entry>
  <f:entry title="POP3 Password" field="password">
    <f:password />
  </f:entry>
</j:jelly>


こんなイメージです。



次に実装ですが、パッケージやインポート、コメントを省いて、全て載せておきます。
Builderとして必須のperformメソッドの実装と、POP3サーバへ接続してメールを取得する処理のrecieveメソッドが主な修正点となります。

public class MailCommandBuilder extends Builder {

    private final String address;
    private final String port;
    private final String username;
    private final String password;

    @DataBoundConstructor
    public MailCommandBuilder(String address, String port, String username,String password) {
        this.address = address;
        this.port = port;
        this.username = username;
        this.password = password;
    }
    public String getAddress() {return address;}
    public String getPort() {return port;}
    public String getUsername() {return username;}
    public String getPassword() {return password;}

    public String receive(BuildListener listener, String commands) {

        Properties props = new Properties();
        Session sess = Session.getDefaultInstance(props);
        String subject = null;

        Store store;
        try {
            store = sess.getStore("pop3");//imapsでもそのまま行けそうな気が
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
            return null;
        }
        try {
            store.connect(address, Integer.valueOf(port), username, password);
            Folder rootFolder = store.getDefaultFolder();
            Folder inbox = rootFolder.getFolder("INBOX");
            inbox.open(Folder.READ_WRITE); //メールを消すためにREAD_WRITEが必要

            Message[] messages = null;
            if (inbox.getMessageCount() > 10)//後でパラメータ化
                messages = inbox.getMessages(inbox.getMessageCount() - 10, inbox.getMessageCount() - 1);
            else
                messages = inbox.getMessages();
            FetchProfile fp = new FetchProfile();
            fp.add("Subject");
            inbox.fetch(messages, fp);
            for (Message message : messages) {
                String findStr = message.getSubject().split(" ")[0];
                if (commands.indexOf(findStr) != -1) {
                    subject = message.getSubject();
                    message.setFlag(Flag.DELETED, true);
                    break;//1件でも処理したらループ終了
                }
            }
            inbox.close(true);
            store.close();
        } catch (MessagingException e) {
            e.printStackTrace();
            return null;
        }
        return subject;
    }

    @Override
    public boolean perform(AbstractBuild build, Launcher launcher,BuildListener listener) {

        if (Hudson.getInstance().getRootUrl() == null) { //nullのケースあり
            listener.getLogger().println("Please save a once System property!");
            return false;
        }
        StringBuffer commands = new StringBuffer();
        for (CLICommand c : CLICommand.all())
            commands.append(c.getName());

        String command = receive(listener, commands.toString());
        if (command == null) {
            listener.getLogger().println("Don't find command mail.");
            return true;
        }
        String cliCommand = "-s " + Hudson.getInstance().getRootUrl() + " " + command; //StringBuilder使うべき?
        List<String> args = Arrays.asList(cliCommand.split(" "));
        String url = System.getenv("JENKINS_URL");
        while (!args.isEmpty()) {
            String head = args.get(0);
            if (head.equals("-s") && args.size() >= 2) {
                url = args.get(1);
                args = args.subList(2, args.size());
                continue;
            }
            break;
        }
        if (url == null)
            return false;
        if (args.isEmpty())
            args = Arrays.asList("help"); // default to help

        CLI cli = null;
        int resultInt = -1;
        try {
            cli = new CLI(new URL(url));
            args = new ArrayList<String>(args);
            resultInt = cli.execute(args, System.in, listener.getLogger(), listener.getLogger());
            cli.close();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
        if (resultInt == 0)
            return true;
        else
            return false;
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }
        public String getDisplayName() {
            return "Mail Commander";
        }
    }
}

<performメソッドについて>
基本路線は手抜き再利用なので、メールの件名(subject)にCLIのコマンドを書いてもらうような想定にしています。
さすがに-s http://〜は省略したかったので、Hudson.getInstance().getRootUrl()からURLを取得することにしましたが、
システムの設定を一度も保存していないとnullが返却されるようです。
CLICommand.all()で実装されているCLIコマンドの一覧を取得し、文字列を作成してからrecieveメソッドをコールします。
recieveメソッドからnullが返却されれば、該当のメールなしと判断して終了します。
null以外は、CLIコマンドが返却されている想定ですので、「-s http://〜」を先頭に結合してCLIに渡してあげます。


<recieveメソッドについて>
引数にCLIコマンドを連結した文字列が渡されてきます。
メールボックスに溜まったメール件数×CLIコマンドの数だけループをまわすのが嫌だったので、CLIコマンドを連結させた文字列
とメールの件名(subject)をindexOfにより部分一致で判定しています。

Message[] messages = null;
if (inbox.getMessageCount() > 10)//後でパラメータ化
    messages = inbox.getMessages(inbox.getMessageCount() - 10, inbox.getMessageCount() - 1);
else
    messages = inbox.getMessages();

としている部分は、メールボックスにメールがたくさん溜まっていると重い処理になってしまうのでこうしています。
"10"と固定で指定している部分はあとからパラメータ指定できるようにする予定です。
一件でも該当するメールが見つかれば終了することにしています。
見つかった場合は、削除フラグをセットし、コネクション切断時にメールボックスから削除します。見つからなければnullを返します。


さて、ここまでの実装でメールによるコマンド実行は出来ます。例えば下のようなメールを送信しておいて

JenkinsのジョブでMailCommandをビルダーに設定したジョブを実行すると

このように実行されます。ジョブの設定を定期実行にしておけば、ある程度自動化できます。


しかし、このままだとJenkinsの画面を見ないと結果が分かりません。
次のステップとして結果をメール送信するように対応したいと思います。


そういえば、ついにWindows Installer対応しましたね!
http://jenkins-ci.org/content/windows-installers-are-now-available?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+ContinuousBlog+%28Hudson+Labs%29

メールでJenkinsにコマンドを送りたい(1)

JenkinsでJavaのソース等をビルドする以外にもいろいろなことを行っていると、ふとリモートからも
Jenkinsをコントロールできたら便利かもと思うことがあります。
ブラウザで直接Jenkinsを操作できないときに、携帯メールとかから命令を実行できたら便利
なんじゃないかと思ったのが妄想の始まり。。。


メールでJenkinsにコマンドを送りたい(2) 〜Builder編〜
メールでJenkinsにコマンドを送りたい(3) 〜Publisher編〜
メールでJenkinsにコマンドを送りたい(4) 〜Trigger編〜
メールでJenkinsにコマンドを送りたい(5) 〜Groovy対応〜


JenkinsのBuilderを拡張し、定期的に設定されたメールアドレスをチェックして、
命令を含んだメールを見つけたら、取得し、命令を実行する。
この考えで手間をそんなにかけずに面白いものが作れそうだなと思いました。
念のため同じようなプラグインが既にあるんじゃないかと思い、ちょこっと調べましたが
すぐには見当たらなかったのと、もしあったとしても勉強になるからまあいいかということで作成してみることに。


考えをまとめているうちに、やりたいことが膨れてきたので、ざっくり書いてみることに。

  1. メールの件名(subject)にコマンドを書いて専用のアドレス宛てに送信することでJenkinsに実行させたい
    • コマンドの形式はCLIを流用すれば楽できそう
    • Builderの拡張としてプラグインを作成して、一定間隔で実行させればいいかな
  2. 成功・失敗を問わず、実行結果をコマンド送信元のメールアドレス宛てに送信したい
    • 標準のメール通知だと成功時にメールが飛ばない
    • Publisherを拡張したらできそう?
  3. できればCLIコマンドのうちgroovyだけ特別扱いして、実行できるようにしたい
    • CLIコマンドの中で対応から外すのは、対話型系のコマンドかな
    • CLIのgroovyに渡すシェルスクリプトは、メールの本文に書いたらどうか


とりあえずやりたいのは1.なのですが、何せメールで実行させるので結果がすぐに帰ってこないと
不便だと思いますので、2.も結局必須な気がします。
3.はメールの本文を入力スクリプトとして扱えば結構簡単に実行できるのではという思いつきです。


明日以降、実装チャレンジしてみたいと思います。