ついったかうんた その2

書き直したつもりが別エントリになってたので、そのまま別エントリとして書き直します^^;

大幅に手直ししてWeb startも用意してみました。が、groovyがリフレクションを使ってる関係で無制限アクセスが必要なようで、証明書の警告が出てしまいます…

大幅改定後のバージョンも再度晒し上げ。

UserTimeline.groovy

package twitterCounter;

import java.net.*
public class UserTimeline {
    public String userId;
    public int pages;

    def eachStatus = {append, flush={} ->
        def slurper = new XmlSlurper()
        for (int page: 1..pages) {
            def url = "http://twitter.com/statuses/user_timeline/${userId}.xml" +
                      "?count=200&page=${page}"
            def status = slurper.parse(url).status
            status.each(append)
            flush()
            if (status.size() < 200) {
                break;
            }
        }
    }
}

まずTwitterAPIを叩いてる部分をクラス化して別ファイルにしました。userIdとpagesはプロパティ化してます。appendとflushはそのままクロージャ引数にしてますが、これもプロパティ化してしまっても良かったかも…
もうちょっと練り様は有る気がします。

Summary.groovy

package twitterCounter;

import org.apache.commons.lang.time.DateUtils

public class Summary {
    def map = new TreeMap()
    public void add(Date date) {
        def day = DateUtils.truncate(date, Calendar.DATE)
        if (!(map.containsKey(day))) {
            map[day] = new ArrayList<Date>()
        }
        map[day].add(date)
    }
    
    public List<Date> getDays() {
        (map.keySet().min())..(map.keySet().max())
    }
    
    public getDailySummary() {
        def result =[:]
        days.each{day->
            result[day] = map.containsKey(day) ? map[day].size() : 0
        }
        return result
    }
    
    public clear() {
        map.clear()
    }
}

集計用クラスもファイルを分けました。これはほぼそのままです。

DailyChart.groovy

package twitterCounter;

import java.text.*
import java.net.*

public class DailyChart {
    public Map<Date, ? extends Number> map = [:]
    
    Collection<Date> getKeys() {
        return map.keySet() as List
    }

    Collection<? extends Number> getValues() {
        return map.values() as List
    }
    
    def isWeekFirst = {date ->
        date.getAt(Calendar.DAY_OF_WEEK) == Calendar.MONDAY
    }
    
    public String getSimpleEncode(
        max = values.collect({it == null || Double.isNaN(it) || it < 0 ? null : it}).max()
    ) {
        def format = ('A'..'Z') + ('a'..'z') + (0..9)
        values.collect(["chxr=1,0,${max}&chd=s:"]) {
            (it == null || Double.isNaN(it) || it < 0
                ? "_" : format[(int)(it * (format.size()-1) / max)])
        }.join()
    }
    
    public String getDateLabel() {
        "|${thinOutKeys.collect{it instanceof Date?it.format('M/d'):it}.join('|')}|"
    }
    
    public getThinOutKeys() {
        def result = []
        keys.each{it->
            if (isWeekFirst(it))result += it
            else result += ""
        }
        return result
    }
    
    public getColorBar() {
        def dateWidth = (int)(10000.0 / keys.size() + 0.5)
        def count = 0;
        def result = []
        (keys[0]..(keys.size < 7 ? keys[-1] : keys[6])).each {day->
            def color = Calendar.with{
                ([SATURDAY, SUNDAY].contains(day.getAt(DAY_OF_WEEK))
                        ? "cccccc" : "ffffff")}
            if (result.isEmpty()) {
                result += color;
            }
            if (result[-1] == color) {
                count++
            } else {
                result += "${dateWidth * count / 10000.0}"
                result += color;
                count = 1
            }
        }
        result += "${dateWidth * count / 10000.0}"
        return result.join(",")
    }
    
    public int getGraphWidth() {
        30 + (5 + 1) * keys.size()
    }
    
    public URL getUrl() {
        new URL("http://chart.apis.google.com/chart?" + 
               ["cht=bvs",
                "chco=0099ff",
                "chf=c,ls,0,${colorBar}", 
                "chbh=5,1,1",
                "chs=${graphWidth}x200",
                "chxt=x,y",
                "chxl=0:$dateLabel",
                simpleEncode
               ].join("&")
        )
    }
}

GoogleChart周りはまとめて手直しし、URLを組み立てるクラスに仕立てました。イベントハンドラ内に有ったURLの組上げをこっちに持って来たので、イベントハンドラもすっきりしています。
DateをキーにしてNumberを持つmapプロパティにグラフのデータを渡してやると、urlプロパティでグラフURLが取得できるようになります…が、LinkedHashMapかSortedMapを使わないとグラフが順不同になってしまうので要注意です。

Control.groovy

package twitterCounter;

import java.text.*
import java.awt.EventQueue
import javax.swing.*
import javax.swing.text.*

class Control {
    static final String MESSAGE = """
タイムラインが取得できませんでした。IDを確認してみてください。

API制限、あるいはtwitterがダウン中の可能性もあります。
(ログイン無しのAPI使用は、同一IPについて1時間で100回までに制限されているようです。)

"""
    def summary = new Summary()
    def twitterFormat = new SimpleDateFormat(
            "EEE MMM dd HH:mm:ss Z yyyy", Locale.US)
    def chart = new DailyChart();
    
    JComponent button
    JComponent graph
    JTextComponent out
    
    public void update(String userId, int pages) {
        button.enabled = false
        new Thread({
            try {
                summary.clear()
                new UserTimeline(userId:userId, pages:pages).eachStatus(
                    {summary.add(twitterFormat.parse(it.created_at.text()))},
                    {
                        chart.map = summary.dailySummary
                        def url = chart.url
                        EventQueue.invokeLater({
                            graph.icon = new ImageIcon(url)
                            out.text = url.toString()
                        })
                    }
                )
            } catch (IOException e) {
                EventQueue.invokeAndWait({JOptionPane.showMessageDialog(button,
                        MESSAGE + "詳細:" + e,
                        "タイムラインを取得できませんでした。",
                        JOptionPane.ERROR_MESSAGE)})
            } finally {
                EventQueue.invokeLater({button.enabled = true})
            }
        }).start()
    }
}

そのコントローラです。ファイル分けるために1クラスにしたんですけど、viewと同じファイルにupdateだけ書いとけば良かったかもですねー。アプレット化しようとしたのも有ってファイルを分けたんですけど、結局セキュリティ関係でgroovyはアプレット化出来ないことがわかっただけでした^^;
(ほんとは出来るんですけど、各クライアントマシンにセキュリティ設定が必要になるんで動かせないも同然…)

やってる事は前と一緒です。URLの組み立てが無くなったくらいで…

TwitterCounter.groovy

package twitterCounter;

import java.text.*
import java.awt.BorderLayout
import java.awt.EventQueue
import javax.swing.*
import groovy.swing.SwingBuilder

def control = new Control();
def frame = new SwingBuilder().frame(
        title: "ついったかうんた",
        size: [500, 300],
        defaultCloseOperation: JFrame.DISPOSE_ON_CLOSE) {
    
    box(constraints: BorderLayout.NORTH) {
        label("ユーザID:")
        def userId = textField(columns:10)
        widget(Box.createHorizontalStrut(20))
        label("取得ページ数:")
        def pages = widget(new JFormattedTextField(new DecimalFormat("##0")))
        pages.columns = 2
        pages.value = 16
        
        widget(Box.createHorizontalStrut(20))
        control.button = button("取得",
                actionPerformed:{control.update(userId.text, pages.value)})
    }
    
    scrollPane(constraints: BorderLayout.CENTER) {
        control.graph = label();
    }
    box (constraints: BorderLayout.SOUTH) {
        label("グラフ画像URL:")
        control.out = textField(editable:false)
    }
}
frame.rootPane.defaultButton = control.button

frame.visible = true

で、最後に残ったのはSwingBuilderを使ってGUIを組み立てて表示する部分です。ここもほとんど弄ってません。
あと、ほぼ同じ形のJAppletを組み立てるTwitterCounterApplet.groovyも作ったんですけど、こいつはセキュリティ設定をしないと実行できないので、ほぼ放棄状態です。mainメソッドでrootPaneをJFrameに移し替えて動かす事で、ひとまず動く事だけは確認できましたが…