Groovy a RSS čítačka na 50 riadkov

Urobme si v Groovy RSS čítačku a ukážme si, ako možno veľmi rýchlo naplácať aplikáciu v duchu rapid prototyping, teda rýchlej výroby predbežných verzií aplikácií.

Budeme potrebovať:

  • načítavať XML (RSS) z webu
  • previesť XML objekty na civilizované doménové objekty (napr. RssItem)
  • zobraziť tieto doménové objekty v zozname JList

Načítanie XML/RSS z webu

XmlSlurper pomôže veľmi jednoducho:

def xmlSlurper = new XmlSlurper()
def rss = xmlSlurper.parse(url)

V objekte rss sa objaví naparsované XML: je to objekt NodeChildren, kde môžeme následne radostne používať bodkovú notáciu a tým behať po hierarchii. Zoznam všetkých <item>-ov získame:

def items = rss.channel.item

Ak ich chceme preiterovať, môžme:

rss.channel.item.each {
    println it
}

Previesť XML objekty na doménové objekty RssItem)

Objekt rss.channel.item je objekt typu NodeChildren. Ten by bol skvelý, akurát, že sa nedá použiť v ovládacom prvku JList: tento komponent totiž potrebuje buď pole alebo zoznam objektov a NodeChildren zoznamom nie je.

Ale to nie je problém: vytvorme si normálnu doménovú triedu pre položku RSS kanála:

class RssItem {
    def title
    def description
    def pubDate

    String toString() {
        return title
    }
}

Prevod medzi XML objektom a regulárnym zoznamom bude jednoduchý: collect()-neme (namapujeme) každý prvok a prevedieme ho na objekt RssItem:

def List<RssItem> parse(String url) {
    def xmlSlurper = new XmlSlurper()
    def rss = xmlSlurper.parse(url)

    rss.channel.item.collect {
        new RssItem(title: it.title, 
                    description: it.description, 
                    pubDate: it.pubDate) 
    }
}

Zobraziť tieto doménové objekty v zozname JList

Na zobrazenie použijeme klasiku: jedno textové políčko JTextArea a jeden zoznam JList. Aby vyzeralo okno pekne, použijeme BorderLayout, ktorý rozdelí okno na osem sektorov. Zoznam bude v hornom (severnom) sektore, čo znamená, že sa bude naťahovať do šírky, ale nie do výšky. Textové políčko bude zase v strednom sektore, čiže sa bude naťahovať do všetkých smerov.

Oba komponenty musíme hodiť do JScrollPane, inak sa v nich nebude dať scrollovať: posúvadlá sa totiž bez neho nezobrazia.

Technický detail: aj keď by bolo logické vytvárať okno zhora nadol, musíme najprv vytvoriť textové políčko a až potom zoznam: v metóde obsluhy zmeny výberu položky sa potrebujeme odkázať na premennú textového políčka.

Všetko to postavíme cez skvelú pomocnú triedu SwingBuilder, ktorá má kopu syntaktického cukru na budovanie UI. Napr. vnáraním kučeravoozátvorkovaných blokov definujeme hierarchiu komponentov, a v rámci neho využívame viacero konštruktoroidných metód: napr:

frame(show: true, size: [200, 200], pack: true)

…vráti nový objekt JFrame, kde sa nastaví postupne setShow(true), setSize(new Dimension(200, 200)) a zavolá pack().

Ak si potrebujeme odložiť vytvorený komponent pre ďalšie použitie, môžeme si ho poznačiť do premennej:

textArea = textArea(
    rows: 10,
    lineWrap: true,
    wrapStyleWord: true,
    text: "???",
)

Všetky parametre možno nájsť v dokumentácii: nájdite si vhodný widget a prezrite zoznam atribútov. (Popravde, dokumentácia je niekedy zastaralá, tak je to často pokus-omyl, resp. chýbajú v nej veľmi užitočné atribúty: napr. items pri zozname).

Ukážme si celý kód:

SwingBuilder.edtBuilder {
  frame(show: true, size: [200, 200], pack: true) {
    borderLayout()

    scrollPane(constraints: BorderLayout.CENTER) {
        textArea = textArea(
            rows: 10,
            lineWrap: true,
            wrapStyleWord: true,
            text: "???",
        )
    }

    scrollPane(constraints: BorderLayout.NORTH) {

        rssItemList = list(
            items: parse(rss),
            selectionMode: ListSelectionModel.SINGLE_SELECTION,
            valueChanged: {
                textArea.text = rssItemList.selectedValue.description
            },
        )
    }
  }
}

Všimnime si vlastnosti textového políčka:

  • rows udáva počet riadkov (analógia JTextArea#setRows())
  • lineWrap zapne zalamovanie (analógia JTextArea#setLineWrap())
  • wrapStyleWord zapne zalamovanie (analógia JTextArea#setWrapStyleWord())
  • text nastaví defaultný text (analógia JTextArea#setText())

A vlastnosti zoznamu:

  • items je “tajná” vlastnosť: umožňuje zobrať zoznam/pole objektov, ktoré zobrazí (vo vnútri vytvorí model, ktorý naplní dátami). To je presne dôvod, prečo sme hore prevádzali NodeChildren na zoznam.
  • selectionMode je klasický indikátor, ktorý zapne či vypne multivýber položiek. U nás multivýber nedáva zmysel, preto ho vypneme. (analógia JList#setSelectionMode())

Obsluha kliknutia do zoznamu

SwingBuilder podporuje v mnohých komponentoch skrátenie obsluhy: namiesto ukecaných listenerov stačí prihodiť obslužnú closure a je to. Vybudovaný zoznam má na toto vlastnosť valueChanged:

valueChanged: {
    textArea.text = rssItemList.selectedValue.description
},

Všimnime si, ako pristupujeme k premenným textArea aj rssItemList, ktoré vypadli z konštruktoroidných metód.

Zároveň využívame vlastnosti Swingu: rssItemList je JList, ktorý má metódu getSelectedValue() (tu zápis skrátime), čo je objekt z modelu, typu RssItem, na ktorom zavoláme getDescription() skráteným zápisom.

Sumarizácia

Hľa, celý kód:

import groovy.swing.SwingBuilder
import javax.swing.*
import javax.swing.event.*
import java.awt.BorderLayout

def rss = "http://rss.sme.sk/rss/rss.asp?id=frontpage"

class RssItem {
    def title
    def description
    def pubDate

    String toString() {
        return title
    }
}

def List<RssItem> parse(String url) {
    def xmlSlurper = new XmlSlurper()
    def rss = xmlSlurper.parse(url)

    rss.channel.item.collect {
        new RssItem(title: it.title, 
                    description: it.description, 
                    pubDate: it.pubDate) 
    }
}

SwingBuilder.edtBuilder {
  frame(show: true, size: [200, 200], pack: true) {
    borderLayout()

    scrollPane(constraints: BorderLayout.CENTER) {
        textArea = textArea(
            rows: 10,
            lineWrap: true,
            wrapStyleWord: true,
            text: "???",
        )
    }

    scrollPane(constraints: BorderLayout.NORTH) {

        list = list(
            items: parse(rss),
            selectionMode: ListSelectionModel.SINGLE_SELECTION,
            valueChanged: {
                textArea.text = list.selectedValue.description
            },
        )
    }
  }
}

Zákerný bug

Ak púšťate zdroják z Groovy konzoly, pozor na zákerný bug (GROOVY-4596). Okno JFrame, ktoré zavriete, neukončí aplikáciu. Tá bude bežať ďalej, akurát nebude možné s ňou interagovať.

Spustite si aplikáciu 40krát a máte po RAMke.

V tejto aplikácii ste povinní nastaviť:

frame(defaultCloseOperation: JFrame.EXIT_ON_CLOSE, ...)

Ibaže ak to urobíte a aplikáciu zavriete, umrie celá konzola: vrátane vašich neuložených súborov.

Pozor na to! (Obídenie je neštastné: spúšťať veci z príkazového riadku alebo z Groovy IDE).

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *