30 czerwca 2009

Z rozdziału 9. "Working with XML" z "Programming Groovy"

W rozdziale 9. "Working with XML" w "Programming Groovy: Dynamic Productivity for the Java Developer" autor przedstawia różne techniki obsługi plików XML w Groovy. Każdorazowo przy nowym temacie Venkat wprowadza nas w temat ciekawym stwierdzeniem. Jako, że ja należę do tego grona osób, które zachłysnęły się prostotą Groovy, więc stanowią one dla mnie dodatkową pożywkę za stosowaniem Groovy w miejscach, w których wielu nawet Javy miałoby obawy zastosować - chociażby obsługa XMLi. Groovy nie będzie pełnym antidotum na XMLowe bolączki, ale biorąc znaczne uproszczenia jakie wprowadza spotkanie z XMLem może być zauważalnie przyjemniejsze.

"Working with traditional Java APIs and libraries to create and parse XML documents tends to lower my spirits. And naigating the document hierarchy using the DOM API is one sure way to drive me insane."

Można się z tym zgodzić bądź nie, ale pochylić się nad tym i rozważyć rozwiązania alternatywne zdecydowanie warto. Może się w końcu okazać, że będziemy mieli więcej czasu na inne czynności niż obsługa XMLi.

Na początek omawiana jest klasa DOMCategory, która korzysta z DOM do obsługi XML. Klasa korzysta z mechanizmu kategorii w Groovy, które pozwalają definiować nowe metody w klasach bez ich modyfikacji (!) DOMCategory dodaje metody tak, że nawigacja po strukturze DOM sprowadza się do odczytu atrybutów klasy (która de facto tych atrybutów nie ma, ale właśnie z DOMCategory mamy wrażenie, jakby miała). Atrybuty dostępne są za pomocą konstrukcji @<nazwa-atrybutu>. Podobnie jak XPath służy do nawigacji po XML, tak GPath umożliwia nawigację po hierarchii POJO i POGO oraz XML. Przykład powinien rozwiać wszelkie wątpliwości.
 groovy:000> f = File.createTempFile("dane", ".xml")
===> c:\temp\dane7020472904153694958.xml
groovy:000> tresc = """<serwery>
groovy:001> <serwer nazwa="Apache Geronimo">
groovy:002> <dostawca>Apache Software Foundation</dostawca>
groovy:003> </serwer>
groovy:004> <serwer nazwa="JBoss Application Server">
groovy:005> <dostawca>JBoss.org</dostawca>
groovy:006> </serwer>
groovy:007> </serwery>"""
===> <serwery>
<serwer nazwa="Apache Geronimo">
<dostawca>Apache Software Foundation</dostawca>
</serwer>
<serwer nazwa="JBoss Application Server">
<dostawca>JBoss.org</dostawca>
</serwer>
</serwery>
groovy:000> f << tresc
===> c:\temp\dane7020472904153694958.xml
groovy:000> f.text
===> <serwery>
<serwer nazwa="Apache Geronimo">
<dostawca>Apache Software Foundation</dostawca>
</serwer>
<serwer nazwa="JBoss Application Server">
<dostawca>JBoss.org</dostawca>
</serwer>
</serwery>
groovy:000> doc = groovy.xml.DOMBuilder.parse(new FileReader(f))
===> [#document: null]
groovy:000> rootElement = doc.documentElement
===> [serwery: null]
groovy:000> use(groovy.xml.dom.DOMCategory) {
groovy:001> println "Serwery i ich dostawcy"
groovy:002> serwery = rootElement.serwer
groovy:003> serwery.each { serwer -> println "${serwer.'@nazwa'} dostarczany jest przez ${serwer.dostawca[0].text()}" }
groovy:004> }
Serwery i ich dostawcy
Apache Geronimo dostarczany jest przez Apache Software Foundation
JBoss Application Server dostarczany jest przez JBoss.org
===> groovy.xml.dom.DOMCategory$NodesHolder@6a2f81
W ramach use korzystałem z atrybutów serwer, dostawca i @nazwa, jakby były one zdefiniowane w ramach odpowiednich typów. Wszystko za sprawą konstrukcji use (więcej o tym w kolejnym wpisie, niebawem).

Kolejnym podejściem do obsługi plików XML jest wykorzystanie groovy.util.XMLParser. Podobnie działa jak DOMCategory, aczkolwiek nie nakłada obowiązku użycia bloku use.
 groovy:000> f = File.createTempFile("dane", ".xml")
===> c:\temp\dane483287696987389412.xml
groovy:000> tresc = """<serwery>
groovy:001> <serwer nazwa="Apache Geronimo">
groovy:002> <dostawca>Apache Software Foundation</dostawca>
groovy:003> </serwer>
groovy:004> <serwer nazwa="JBoss Application Server">
groovy:005> <dostawca>JBoss.org</dostawca>
groovy:006> </serwer>
groovy:007> </serwery>"""
===> <serwery>
<serwer nazwa="Apache Geronimo">
<dostawca>Apache Software Foundation</dostawca>
</serwer>
<serwer nazwa="JBoss Application Server">
<dostawca>JBoss.org</dostawca>
</serwer>
</serwery>
groovy:000> f << tresc
===> c:\temp\dane483287696987389412.xml
groovy:000> doc = new XmlParser().parse(f)
===> serwery[attributes={}; value=[serwer[attributes={nazwa=Apache Geronimo}; value=[dostawca[attributes={};
value=[Apache Software Foundation]]]], serwer[attributes={nazwa=JBoss Application Server};
value=[dostawca[attributes={}; value=[JBoss.org]]]]]]
groovy:000> println "Serwery i ich dostawcy"
Serwery i ich dostawcy
===> null
groovy:000> doc.each {
groovy:001> println "${it.@nazwa} dostarczany jest przez ${it.dostawca[0].text()}"
groovy:002> }
Apache Geronimo dostarczany jest przez Apache Software Foundation
JBoss Application Server dostarczany jest przez JBoss.org
===> serwery[attributes={}; value=[serwer[attributes={nazwa=Apache Geronimo}; value=[dostawca[attributes={};
value=[Apache Software Foundation]]]], serwer[attributes={nazwa=JBoss Application Server};
value=[dostawca[attributes={}; value=[JBoss.org]]]]]]
Brak użycia bloku use oraz kilka atrakcji w stylu each(), collect(), czy find() dla elementów xmlowych stanowi ciekawą alternatywę dla DOMCategory. W przypadku tego rozwiązania komentarze oraz instrukcje XML nie są zachowane.

Użycie XmlParser wiąże się z dużym narzutem pamięciowym, więc kolejnym rozwiązaniem, znacznie mniej pamięciożernym, jest XmlSlurper. Użycie podobne do XmlParser z tym, że dodatkowo mamy obsługę przestrzeni nazewniczych w XML przez określenie ich przez metodę declareNamespace().
 f = File.createTempFile("dane", ".xml")
f << tresc
tresc = """<p:serwery xmlns:p="Przyklad">
<p:serwer nazwa="Apache Geronimo">
<p:dostawca>Apache Software Foundation</p:dostawca>
</p:serwer>
</p:serwery>"""
doc = new XmlSlurper().parse(f).declareNamespace(pk: 'Przyklad')
println "${doc.'pk:serwer'.@nazwa} dostarczany przez ${doc.'pk:serwer'.dostawca[0].text()}"
Tworzenie XMLi już widzieliśmy. Po prostu korzystamy z potrójnego cudzysłowu do utworzenia treści XML, w której rozwiązywane są wyrażenia Groovy.
 groovy:000> serwery = ['Apache Geronimo':'Apache Software Foundation', 'JBoss AS':'JBoss.org']
===> {Apache Geronimo=Apache Software Foundation, JBoss AS=JBoss.org}
groovy:000> tresc = ''
===>
groovy:000> serwery.each { serwer, dostawca ->
groovy:001> wycinek = """
groovy:002> <serwer nazwa="${serwer}">
groovy:003> <dostawca>${dostawca}</dostawca>
groovy:004> </serwer>
groovy:005> """
groovy:006>
groovy:006> tresc += wycinek
groovy:007> }
===> {Apache Geronimo=Apache Software Foundation, JBoss AS=JBoss.org}
groovy:000> doc = "<serwery>${tresc}</serwery>"
===> <serwery>
<serwer nazwa="Apache Geronimo">
<dostawca>Apache Software Foundation</dostawca>
</serwer>

<serwer nazwa="JBoss AS">
<dostawca>JBoss.org</dostawca>
</serwer>
</serwery>
groovy:000>
groovy:000> println doc
<serwery>
<serwer nazwa="Apache Geronimo">
<dostawca>Apache Software Foundation</dostawca>
</serwer>

<serwer nazwa="JBoss AS">
<dostawca>JBoss.org</dostawca>
</serwer>
</serwery>
===> null
Można również skorzystać z MarkupBuilder lub StreamingMarkupBuilder. Podsumowując: "Groovy can make working with XML bearable" (str. 155). Czyż nie?

2 komentarze:

  1. 1) Tak groovy jest fajny, sam go używam jak wiesz, ale nie można zapominać, że dość dużo z tego co da się zrobić w groovy, da się osiągnąć w porządnie zaprojektowanym języku statycznie typowanym. Dlatego polecam spróbować Scala albo wręcz F# / Ocaml. Bo nie wszystko to zasługa dynamizmu!

    2) Sam stosowałem groovysh, ale wypomniano mi, że jest dużo mniej czytelne niż zrzuty z GroovyConsole i się z tym aktualnie zgodzę.

    Poza tym szkoda, że nie dotrę na Javarsovia :/

    Pozdrawiam,
    Krzysztof Kowalczyk

    OdpowiedzUsuń
  2. A ja jeszcze dodam, że oprócz XMLa Groovy/Grails całkiem fajnie obsługują JSONa, zarówno przy zwracaniu danych (render kolekcja as JSON) ja i przy ich parsowaniu (JSON.parse(..) daje mapę)

    OdpowiedzUsuń