読者です 読者をやめる 読者になる 読者になる

ScalaでRSS読み取り(続き)

プログラミング

前回の続き
lilyext.hatenablog.com

このサイトを参考にRSSの仕様をおおまかに把握した。
amarron.hatenablog.com

で、雑に実装したクラス群が以下の通り

import scala.xml._
import java.net.URL

class RSS(val url:String,val sitealias:String,val title:String ,val link:String,val description:String,val articles:Seq[Article]){
  override def toString:String=sitealias
}
class Article(val title:String,val link:String,val description:String,val pubdate:String,val categories:Seq[String]=Seq.empty){
  override def toString:String=title
}
trait RSSParser{
  def apply(xml:NodeSeq,url:String,sitealias:String):RSS
}
object RSS1 extends RSSParser{
  def apply(xml:NodeSeq,url:String,sitealias:String):RSS={
    val title= (xml \ "channel" \ "title").text
    val link = (xml \ "channel" \ "link").text
    val description = (xml \ "channel" \ "description").text
    val articles = (xml \ "item").map(item=>new Article((item \ "title").text,
                                                        (item \ "link").text,
                                                        (item \ "description").text,
                                                        (item \ "dc:date").text))
    new RSS(url,sitealias,title,link,description,articles)
  }
}
object RSS2 extends RSSParser{
  
  def apply(xml:NodeSeq,url:String,sitealias:String):RSS={
    
    val title = (xml \ "title").text
    val link = (xml \ "link").text
    val description = (xml \ "description").text
    val articles=(xml \ "item").map(item=>new Article((item \ "title").text,
                                                    (item \ "link").text,
                                                    (item \ "description").text,
                                                    (item \ "pubDate").text,
                                                    ((item \ "category").map(_.text))))
                                               
    new RSS(url,sitealias,title,link,description,articles)
  }
}
object Atom extends RSSParser{
  
  def apply(xml:NodeSeq,url:String,sitealias:String):RSS={
    val title= (xml \ "title").text
    val link=(xml \ "link").text
    val description=(xml \ "subtitle").text
    val articles=(xml \ "entry").map(item=>{
      val entrytitle= (item \ "title").text
      val entrylink=(item \ "link" ).filter(link=>{val t=(link \ "@type").text;t=="text/html" || t==""}).map(link=>(link \ "@href").text).head
      val entryupdated=(item \ "updated").text
      val entrysummary=(item \ "summary").text
      new Article(entrytitle,entrylink,entrysummary,entryupdated)
      
    })
    new RSS(url,sitealias,title,link,description,articles)
  }
  
}


object RSS {
  def apply(url:String,sitealias:String):RSS={
    val xml=XML.load(new URL(url))
    if (xml.head.namespace=="http://www.w3.org/2005/Atom"){
      Atom(xml,url,sitealias)
    }else if (xml.head.label=="rss" && (xml \ "@version").text=="2.0"){
      RSS2(xml \ "channel",url,sitealias)
    }else if(xml.head.namespace=="http://purl.org/rss/1.0/"){
      RSS1(xml,url,sitealias)
    }else{
      RSS1(xml,url,sitealias)
    }
  }
}

RSSのフォーマットにはおおよそRSS1.0,RSS2.0,ATOMの3種類あるのでまず分類した後にパースする必要がある。
まぁ本格的なものを作るつもりも(技術も)ないのでATOM、RSS1.0はXML名前空間、RSS2.0はルート要素のバージョン属性で分類することにした。
名前空間はNodeSeqの\演算子で取得しようと悪戦苦闘したが結局xml.head.namespaceであっさり取得できることがわかった。
あとはそれぞれの規則に(雑に)従いパースした。

GUI面については以下のように実装した。

class RSSFrame(var rssarticles:List[RSS]) extends MainFrame {
  title="RSS"
  val tabPane:TabbedPane= new TabbedPane{
    preferredSize=new Dimension(500,500)
    listenTo(mouse.clicks)
    reactions+={
      case e :MouseClicked=>{
        val event=e.asInstanceOf[MouseClicked]
        if((event.peer.getButton)==MouseEvent.BUTTON3){
          val popupMenu = new PopupMenu{
            contents+=new MenuItem("削除"){
            listenTo(mouse.clicks)
            reactions+={
              case MousePressed (_,_,m,_,e)=>{
                val i:Int=tabPane.selection.index
                  if (i>=0){
                  tabPane.pages.remove(i)
                  }
               }
            }
           }
          }
          popupMenu.show(this,event.point.x,event.point.y)
        }
      }
    }
  }
  contents=tabPane
  menuBar = new MenuBar{
    contents+=new Menu("File"){
      contents+=new MenuItem(Action("add tab"){
        new Frame{
          title="Add new tab"
          contents=new ScrollPane( new BoxPanel(Orientation.Vertical){
            contents+=new ListView(rssarticles){
              listenTo(mouse.clicks)
              reactions+={
                case e : MouseClicked=>{
                  val event=e.asInstanceOf[MouseClicked]
                  if(event.clicks==2){
                    val index=peer.locationToIndex(event.point)
                    tabPane.pages+=new TabbedPane.Page(
                        rssarticles(index).sitealias,
                        new ScrollPane(new ListView(rssarticles(index).articles){
                          listenTo(mouse.clicks)
                          reactions+={
                            case e :MouseClicked=>{
                              val event=e.asInstanceOf[MouseClicked]
                              if (event.clicks==2){
                                val targeturi=rssarticles(index).articles(peer.locationToIndex(event.point)).link
                                val desktop=Desktop.getDesktop()
                                desktop.browse(new URI(targeturi))
                              }
                            }
                          }
                    }))
                  }
                }
              }
              
            }
            
          })
          visible=true
        }
        val title="テスト"
      })
      contents+=new MenuItem(Action("add feed source"){
       Dialog.showInput(message="URL",initial="http://hoge.hoge") match{
         case Some (x)=>{
           if (isExist(x)){
             Dialog.showInput(message="Name",initial="name") match{
               case Some(y)=>{
                 rssarticles=rssarticles:+RSS(x,y)
                 val file=new File("./rsstestdoc")
                 val fw=new FileWriter(file,true)
                 fw.write(y+","+x)
               }
               case None =>
             }
           }else{
             Dialog.showMessage(message="Not found")
           }
         }
         case None=>
       }
       def isExist(url:String):Boolean={
         try{
           XML.load(new URL(url))
         }catch{
           case _=>return false
         }
         return true
       }
      })
      contents+=new MenuItem(Action("Quit"){
        sys.exit(0)
      })
    }
  }

サイトごとにタブを作りそれぞれに記事のリストを入れる感じに実装。記事のタイトルをダブルクリックするとブラウザが起動する。

  • ダブルクリックの検知
  • 右クリックの検知

このあたりの方法がひどいことになっている。もっとシンプルな方法がないものか……
あとRSSを購読するサイトを登録するためにDialog.showInputを使ったのだがコピーアンドペーストが出来ないのでめんどくさい。コピーアンドペーストが出来るDialogって自分で実装しないといけないんだよな……(してない)

出来上がったやつのスクショが以下の通り
f:id:lilyext:20170305134914p:plain
Gigazinehttp://gigazine.net/とgizmodohttp://www.gizmodo.jp/RSSを表示している。