0
XML and Web Services
© ASERT 2006-2008




                        with Groovy
                           Dr Paul King
   ...
What is Groovy?
                    • “Groovy is like a super version
                      of Java. It can leverage Java'...
Growing Acceptance
                    • A slow and steady start but now gaining
                      in momentum, maturi...
Why Groovy?
                    •   Minimal learning curve
                    •   Compiles to bytecode
                  ...
Fun Groovy trends
© ASERT 2006-2008




                                        codecamp 2008 - 5
Groovy and XML ...
                    • Reading XML
                      – Special Groovy support: XmlParser,
          ...
... Groovy and XML ...
                    • Updating XML
                      – Using above: read followed by create
   ...
... Groovy and XML
                    • So many technologies – how to choose?
                      – Normally just use X...
An Xml Example ...
                    import groovy.xml.dom.DOMCategory

                    class Flights {
            ...
... An Xml Example
                    ...
                          static final Reader getReader() {
                   ...
XmlParser
                    def trip = new XmlParser().parseText(Flights.XML)
                    assert trip.flight[0]....
XmlParser – Under the covers
                    • For a JavaBean, this Groovy expression:
                       trip.fli...
XmlSlurper...
                    • The same again using XmlSlurper
                      – Mostly identical syntax and ca...
...XmlSlurper...
                    • What does Lazy mean?

                     def   trip = new XmlSlurper().parseText(...
...XmlSlurper
                    Light-weight scanning here

                     def   trip = new XmlSlurper().parseText...
A Namespace Example
                    class Books {
                      static final String XML = '''
                ...
XmlParser and Namespaces
                    • Recommended syntax:
                       import groovy.xml.*
            ...
XmlSlurper and Namespaces

                    def book = new XmlSlurper().parseText(Books.XML)
                    book.d...
Other GPath Features
                    • Similar syntax for XmlSlurper and DOMCategory

                      def trip =...
What about non-XML?
                    def neko = new org.cyberneko.html.parsers.SAXParser()
                    neko.set...
Raw DOM

                    import groovy.xml.DOMBuilder

                    def trip = DOMBuilder.parse(Flights.reader)...
DOM plus metaprogramming

                    import groovy.xml.DOMBuilder
                    import org.w3c.dom.Element
...
DOMCategory
                    import groovy.xml.DOMBuilder
                    import groovy.xml.dom.DOMCategory

      ...
DOM4J


                    import org.dom4j.io.SAXReader

                    def trip = new SAXReader().read(Flights.rea...
JDOM


                    import org.jdom.input.SAXBuilder

                    def b = new SAXBuilder()
                ...
XOM


                    import nu.xom.Builder

                    def doc = new Builder().build(Flights.reader)
       ...
StAX
                    import static javax.xml.stream.XMLInputFactory.newInstance as staxFactory
                    imp...
SAX
                    import javax.xml.parsers.SAXParserFactory
                    import org.xml.sax.*
               ...
XPath
                    import javax.xml.xpath.*
                    import groovy.xml.DOMBuilder

                    d...
XPath with DOMCategory


                    import groovy.xml.DOMBuilder
                    import groovy.xml.dom.DOMCat...
Xalan XPath
                    import static org.apache.xpath.XPathAPI.*
                    import groovy.xml.DOMBuilder...
Jaxen XPath
                    import org.jaxen.dom.DOMXPath
                    import groovy.xml.DOMBuilder

          ...
JSR 225 - XQJ
                    import net.sf.saxon.xqj.SaxonXQDataSource
                    import javax.xml.xquery.XQ...
XSLT...
                    import static javax.xml.transform.TransformerFactory.newInstance as xsltFactory
              ...
...XSLT
                    import static javax.xml.transform.TransformerFactory.newInstance as xsltFactory
              ...
MarkupBuilder

                    import groovy.xml.MarkupBuilder

                    def writer = new StringWriter()
  ...
StreamingMarkupBuilder

                    import groovy.xml.StreamingMarkupBuilder

                    def writer = new...
DOMBuilder

                    import groovy.xml.*

                    def builder = DOMBuilder.newInstance()
          ...
MarkupBuilder with Namespaces
                    import groovy.xml.MarkupBuilder

                    def writer = new St...
StreamingMarkupBuilder with Namespaces
                    import groovy.xml.*

                    XmlUtil.serialize(new ...
DOMBuilder with Namespaces
                    import groovy.xml.DOMBuilder

                    def b = DOMBuilder.newIns...
DOMBuilder with NamespaceBuilder
                    import groovy.xml.*
                    def b = NamespaceBuilder.newI...
StaxBuilder
                    import javax.xml.stream.XMLOutputFactory
                    import groovy.xml.StaxBuilder...
StaxBuilder for non-XML
                    import groovy.xml.StaxBuilder
                    import org.codehaus.jettison...
Updating XML

                                                       <shopping>
                    <shopping>            ...
Updating with XmlParser
                    def root = new XmlParser().parseText(input)

                    // modify gro...
Updating with XmlSlurper
                    def root = new XmlSlurper().parseText(input)

                    // modify g...
Updating with DOMCategory
                    use(DOMCategory) {
                      def categories = DOMBuilder.parse(i...
Groovy and Web Services
                    • SOAP Web Services
                      – GroovySOAP using XFire for Java 1....
GroovySOAP
                    class MathService {                  • XFire based
                       double add(double...
GroovyWS
                    class MathService {                         • CXF based
                        double add(do...
JAXB Server
                    import   javax.xml.ws.Endpoint
                    import   javax.jws.WebService
         ...
JAXB Client
                    • JAXB Client
                    import javax.xml.namespace.QName
                    imp...
Raw CXF
                    • Apache CXF helps you build and develop
                      services. You can use frontend ...
Axis2
                    • Apache Axis is a comprehensive
                      implementation of SOAP
                  ...
RESTful options
                    • Several Options
                      – Already supported in discussed
             ...
restlet.org
  import org.restlet.*
  import org.restlet.data.*
  class MailboxResource extends Restlet {
      void handle...
GroovyRestlet DSL
                    builder.component {
                      current.servers.add(protocol.HTTP, 8182)
 ...
Jersey...
                    package com.asert

                    import   javax.ws.rs.GET
                    import  ...
...Jersey...
                    import javax.ws.rs.PathParam
                    import javax.xml.bind.annotation.*

    ...
...Jersey
                    @Path(quot;/flight/xml/{flightnum}quot;)
                    class FlightInfoXml {
         ...
Jersey output
                    > curl http://localhost:9998/helloworld
                    Hello World

               ...
Synapse
                    • Apache Synapse is a simple, lightweight and
                      high performance Enterpris...
ServiceMix
                    • ServiceMix is an open source Enterprise Service
                      Bus (ESB) combining...
Tuscany...
                    • Tuscany embodies Service Component
                      Architecture (SCA) which defines...
...Tuscany...
                    <composite ...>
                        <component name=quot;CalculatorServiceComponentq...
...Tuscany
                    <composite ...>
                                                     With Groovy scripts ei...
More Information: on the web
                    • Web sites
                      –   http://groovy.codehaus.org
        ...
More Information: Groovy in Action
© ASERT 2006-2008




                                                    codecamp 2008...
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
XML and Web Services with Groovy
Upcoming SlideShare
Loading in...5
×

XML and Web Services with Groovy

3,973

Published on

Dr Paul King's presentation slides on \'XML and Web Services with Groovy\'

Published in: Technology, Education
0 Comments
12 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
3,973
On Slideshare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
289
Comments
0
Likes
12
Embeds 0
No embeds

No notes for slide

Transcript of "XML and Web Services with Groovy"

  1. 1. XML and Web Services © ASERT 2006-2008 with Groovy Dr Paul King paulk@asert.com.au ASERT, Australia codecamp 2008 - 1
  2. 2. What is Groovy? • “Groovy is like a super version of Java. It can leverage Java's enterprise capabilities but also has cool productivity features like closures, DSL support, builders and dynamic typing.” © ASERT 2006-2008 Groovy = Java – boiler plate code + optional dynamic typing + closures + domain specific languages + builders + metaprogramming codecamp 2008 - 2
  3. 3. Growing Acceptance • A slow and steady start but now gaining in momentum, maturity and mindshare © ASERT 2006-2008 codecamp 2008 - 3
  4. 4. Why Groovy? • Minimal learning curve • Compiles to bytecode • Java object model & integration • Annotations • Optional static typing © ASERT 2006-2008 • Both run-time and compile-time metaprogramming codecamp 2008 - 4
  5. 5. Fun Groovy trends © ASERT 2006-2008 codecamp 2008 - 5
  6. 6. Groovy and XML ... • Reading XML – Special Groovy support: XmlParser, XmlSlurper, DOMCategory – Or Groovy sugar for your current favorites: DOM, SAX, StAX, DOM4J, JDom, XOM, XPath, XSLT, XQuery, etc. © ASERT 2006-2008 • Creating XML – Special Groovy support: MarkupBuilder and StreamingMarkupBuilder – Or again, enhanced syntax for your current favorites codecamp 2008 - 6
  7. 7. ... Groovy and XML ... • Updating XML – Using above: read followed by create – Can be done with XmlParser, XmlSlurper, DOMCategory – Or with your Java favorites © ASERT 2006-2008 • Verifying XML – Also DTD, W3C XML Schema, Relax NG in a similar fashion to Java mechanisms for these features codecamp 2008 - 7
  8. 8. ... Groovy and XML • So many technologies – how to choose? – Normally just use XmlSlurper and StreamingMarkupBuilder – Or if you want a DOM, use XmlParser and MarkupBuilder or DOMBuilder – Or if you must have a W3C DOM, use © ASERT 2006-2008 DOMCategory – Or if you expect to have a large amount of legacy or Java parsing code, you can stick with your favorite Java XML API/stack codecamp 2008 - 8
  9. 9. An Xml Example ... import groovy.xml.dom.DOMCategory class Flights { static final String XML = ''' <trip> <flight hours=quot;13quot;> <from>Brisbane</from> © ASERT 2006-2008 <to>Los Angeles</to> </flight> <flight hours=quot;4quot;> <from>Los Angeles</from> <to>New Orleans</to> </flight> </trip> ''' ... codecamp 2008 - 9
  10. 10. ... An Xml Example ... static final Reader getReader() { new StringReader(XML) } static final Set getCities(flights) { Set cities = [] © ASERT 2006-2008 use(DOMCategory) { flights.each { f -> cities += f.to[0].text() cities += f.from[0].text() } } You can mostly ignore cities the details here for now. } We’ll cover DOMCategory } in more detail shortly. codecamp 2008 - 10
  11. 11. XmlParser def trip = new XmlParser().parseText(Flights.XML) assert trip.flight[0].to.text() == 'Los Angeles' assert trip.flight[1].@hours == '4' Set cities = trip.flight.from*.text() + trip.flight.to*.text() © ASERT 2006-2008 assert cities == ['Brisbane', 'Los Angeles', 'New Orleans'] as Set assert trip.flight.@hours == ['13', '4'] assert trip.flight.@hours*.toInteger().sum() == 17 • Builds an in-memory DOM tree codecamp 2008 - 11
  12. 12. XmlParser – Under the covers • For a JavaBean, this Groovy expression: trip.flight[0].to[0].text() • Is “roughly” converted to: trip.getflight().get(0).getTo().get(0).text() © ASERT 2006-2008 where getFlight() and getTo() return a List • But for XML Parser, it overrides this mechanism and “roughly” converts to: trip.getByName('flight').get(0). getByName('to').get(0).text() where getByName is a Groovy method similar to getElementsByTagName in org.w3c.dom.Element codecamp 2008 - 12
  13. 13. XmlSlurper... • The same again using XmlSlurper – Mostly identical syntax and capabilities def trip = new XmlSlurper().parseText(Flights.XML) assert trip.flight[0].to.text() == 'Los Angeles' assert trip.flight[1].@hours == '4' © ASERT 2006-2008 Set cities = trip.flight.from*.text() + trip.flight.to*.text() assert cities == ['Brisbane', 'Los Angeles', 'New Orleans'] as Set assert trip.flight.@hours.list() == ['13', '4'] assert trip.flight.@hours*.toInteger().sum() == 17 – But features lazy evaluation of expressions – Consider this for streaming scenarios codecamp 2008 - 13
  14. 14. ...XmlSlurper... • What does Lazy mean? def trip = new XmlSlurper().parseText(Flights.XML) def moreThanFiveHours = { f -> f.@hours.toInteger() > 5 } def arrivingLax = { f -> f.to == 'Los Angeles' } def departingOz = { f -> f.from == 'Brisbane' } © ASERT 2006-2008 def longFlights = trip.flight.findAll(moreThanFiveHours) def longLaxFlights = longFlights.findAll(arrivingLax) def longOzLaxFlights = longLaxFlights.findAll(departingOz) assert longOzLaxFlights.@hours == '13' codecamp 2008 - 14
  15. 15. ...XmlSlurper Light-weight scanning here def trip = new XmlSlurper().parseText(Flights.XML) def moreThanFiveHours = { f -> f.@hours.toInteger() > 5 } def arrivingLax = { f -> f.to == 'Los Angeles' } def departingOz = { f -> f.from == 'Brisbane' } © ASERT 2006-2008 def longFlights = trip.flight.findAll(moreThanFiveHours) def longLaxFlights = longFlights.findAll(arrivingLax) def longOzLaxFlights = longLaxFlights.findAll(departingOz) assert longOzLaxFlights.@hours == '13' Lazy expression storage Usage triggers evaluation but deferred evaluation • This may look puzzling at first – But the good news is you don’t normally havecodecamp 2008 - 15 to care
  16. 16. A Namespace Example class Books { static final String XML = ''' <rdf:rdf xmlns:bibterm=quot;http://www.book-stuff.com/terms/quot; xmlns:dc=quot;http://purl.org/dc/elements/1.0/quot; xmlns:rdf=quot;http://www.w3.org/1999/02/22-rdf-syntax-ns#quot;> <rdf:description rdf:about=quot;http://www.book-stuff.com/bibquot;> <bibterm:book rdf:parseType=quot;Resourcequot;> <bibterm:year>2007</bibterm:year> <dc:title>Groovy in Action</dc:title> © ASERT 2006-2008 <bibterm:author rdf:parseType=quot;Resourcequot;> <bibterm:last>König</bibterm:last> <bibterm:first>Dierk</bibterm:first> </bibterm:author> <rdf:comment> Coauthors: Andrew Glover, Paul King, Guillaume Laforge and Jon Skeet </rdf:comment> </bibterm:book> </rdf:description> </rdf:rdf> ''' ... codecamp 2008 - 16
  17. 17. XmlParser and Namespaces • Recommended syntax: import groovy.xml.* def book = new XmlParser().parseText(Books.XML) def rdf = new Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') def dc = new Namespace('http://purl.org/dc/elements/1.0/') def bibterm = new Namespace('http://www.book-stuff.com/terms/') def b = book[rdf.description][bibterm.book] assert b[dc.title].text() == 'Groovy in Action' assert b[bibterm.year].text() == '2007' © ASERT 2006-2008 • Options // use string style matching (exact match on prefix or wildcard or URI) assert b.'bibterm:year'.text() == '2007' assert b.'*:year'.text() == '2007' assert b.'http://www.book-stuff.com/terms/:year'.text() == '2007' // Namespace is a QName factory but you can use QName directly def bibtermYear = new QName('http://www.book-stuff.com/terms/', 'year') assert b[bibtermYear].text() == '2007' // use QName with wildcards def anyYear = new QName('*', 'year') assert b[anyYear].text() == '2007' codecamp 2008 - 17
  18. 18. XmlSlurper and Namespaces def book = new XmlSlurper().parseText(Books.XML) book.declareNamespace( rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', dc: 'http://purl.org/dc/elements/1.0/', bibterm: 'http://www.book-stuff.com/terms/') def b = book.'rdf:description'.'bibterm:book' © ASERT 2006-2008 assert b.'dc:title' == 'Groovy in Action' assert b.'bibterm:year' == '2007' codecamp 2008 - 18
  19. 19. Other GPath Features • Similar syntax for XmlSlurper and DOMCategory def trip = new XmlParser().parseText(Flights.XML) assert trip.'**'*.name() == ['trip', 'flight', 'from', 'to', 'flight', 'from', 'to'] assert trip.depthFirst()*.name() == ['trip', 'flight', 'from', 'to', 'flight', 'from', 'to'] assert trip.breadthFirst()*.name() == © ASERT 2006-2008 ['trip', 'flight', 'flight', 'from', 'to', 'from', 'to'] assert trip.'**'.from*.text() == ['Brisbane', 'Los Angeles'] codecamp 2008 - 19
  20. 20. What about non-XML? def neko = new org.cyberneko.html.parsers.SAXParser() neko.setFeature('http://xml.org/sax/features/namespaces', false) def page = new XmlParser(neko).parse('http://groovy.codehaus.org/') def data = page.depthFirst().A.'@href'.grep{ it != null && it.endsWith('.html') } data.each { println it } © ASERT 2006-2008 def neko = new org.cyberneko.html.parsers.SAXParser() def page = new XmlSlurper(neko).parse('http://groovy.codehaus.org/') def data = page.depthFirst().grep{ it.name() == 'A' && it.@href.toString().endsWith('.html') }.'@href' data.each { println it } http://groovy.codehaus.org/apidocs/index.html /faq.html /groovy-jdk.html ... codecamp 2008 - 20
  21. 21. Raw DOM import groovy.xml.DOMBuilder def trip = DOMBuilder.parse(Flights.reader).documentElement def flights = trip.getElementsByTagName('flight') def dest = flights.item(0).getElementsByTagName('to').item(0) © ASERT 2006-2008 assert dest.firstChild.nodeValue == 'Los Angeles' assert flights.item(1).getAttribute('hours') == '4' assert Flights.getCities(flights) == ['Brisbane', 'Los Angeles', 'New Orleans'] as Set codecamp 2008 - 21
  22. 22. DOM plus metaprogramming import groovy.xml.DOMBuilder import org.w3c.dom.Element def trip = DOMBuilder.parse(Flights.reader).documentElement Element.metaClass.element = { t, i -> © ASERT 2006-2008 delegate.getElementsByTagName(t).item(i) } Element.metaClass.text = {-> delegate.firstChild.nodeValue } assert trip.element('flight', 0).element('to', 0).text() == 'Los Angeles' assert trip.element('flight', 1).getAttribute('hours') == '4' codecamp 2008 - 22
  23. 23. DOMCategory import groovy.xml.DOMBuilder import groovy.xml.dom.DOMCategory def doc = DOMBuilder.parse(Flights.reader) def trip = doc.documentElement © ASERT 2006-2008 use(DOMCategory) { assert trip.flight[0].to[0].text() == 'Los Angeles' assert trip.flight[1].'@hours' == '4' assert Flights.getCities(trip.flight) == ['Brisbane', 'Los Angeles', 'New Orleans'] as Set } codecamp 2008 - 23
  24. 24. DOM4J import org.dom4j.io.SAXReader def trip = new SAXReader().read(Flights.reader).rootElement assert trip.elements()[0].elementText('to') == 'Los Angeles' © ASERT 2006-2008 assert trip.elements()[1].attributeValue('hours') == '4' codecamp 2008 - 24
  25. 25. JDOM import org.jdom.input.SAXBuilder def b = new SAXBuilder() def trip = b.build(Flights.reader).rootElement © ASERT 2006-2008 assert trip.children[0].getChildText('to') == 'Los Angeles' assert trip.children[1].getAttribute('hours').value == '4' codecamp 2008 - 25
  26. 26. XOM import nu.xom.Builder def doc = new Builder().build(Flights.reader) def flights = doc.rootElement.childElements © ASERT 2006-2008 assert flights.get(0).getFirstChildElement('to').value == 'Los Angeles' assert flights.get(1).getAttribute('hours').value == '4' codecamp 2008 - 26
  27. 27. StAX import static javax.xml.stream.XMLInputFactory.newInstance as staxFactory import javax.xml.stream.XMLStreamReader as StaxReader def flights = [] def flight def seenTag StaxReader.metaClass.attr = { s -> delegate.getAttributeValue(null, s) } def reader = staxFactory().createXMLStreamReader(Flights.reader) while (reader.hasNext()) { def name = reader.localName © ASERT 2006-2008 if (reader.startElement) { if (name == 'flight') flight = [hours:reader.attr('hours')] else if (name in ['from', 'to']) seenTag = name } else if (reader.characters) { if (seenTag) flight[seenTag] = reader.text } else if (reader.endElement) { if (name == 'flight') flights += flight seenTag = null } reader.next() } assert flights[0].to == 'Los Angeles' assert flights[1].hours == '4' codecamp 2008 - 27
  28. 28. SAX import javax.xml.parsers.SAXParserFactory import org.xml.sax.* import org.xml.sax.helpers.DefaultHandler class TripHandler extends DefaultHandler { def flights = [] private flight, seenTag void startElement(String ns, String localName, String qName, Attributes atts) { if (qName == 'flight') flight = [hours:atts.getValue('hours')] else if (qName in ['from', 'to']) seenTag = qName } © ASERT 2006-2008 public void endElement(String uri, String localName, String qName) { if (qName == 'flight') flights += flight seenTag = null } public void characters(char[] ch, int start, int length) { if (seenTag) flight[seenTag] = new String(ch, start, length) } } def handler = new TripHandler() def reader = SAXParserFactory.newInstance().newSAXParser().xMLReader reader.setContentHandler(handler) reader.parse(new InputSource(Flights.reader)) assert handler.flights[0].to == 'Los Angeles' assert handler.flights[1].hours == '4' codecamp 2008 - 28
  29. 29. XPath import javax.xml.xpath.* import groovy.xml.DOMBuilder def xpath = XPathFactory.newInstance().newXPath() def trip = DOMBuilder.parse(Flights.reader).documentElement assert xpath.evaluate('flight/to/text()', trip) == 'Los Angeles' assert xpath.evaluate('flight[2]/@hours', trip) == '4' © ASERT 2006-2008 def flights = xpath.evaluate( 'flight', trip, XPathConstants.NODESET ) def hoursAsInt = { n -> xpath.evaluate('@hours', n).toInteger() } assert flights.collect(hoursAsInt).sum() == 17 assert Flights.getCities(flights) == ['Brisbane', 'Los Angeles', 'New Orleans'] as Set codecamp 2008 - 29
  30. 30. XPath with DOMCategory import groovy.xml.DOMBuilder import groovy.xml.dom.DOMCategory import static javax.xml.xpath.XPathConstants.* def trip = DOMBuilder.parse(Flight.reader).documentElement © ASERT 2006-2008 use (DOMCategory) { assert trip.xpath('flight/to/text()') == 'Los Angeles' assert trip.xpath('flight[2]/@hours', NUMBER) == 4 flights = trip.xpath('flight', NODESET) def hoursAsNum = { n -> n.xpath('@hours', NUMBER) } assert flights.collect(hoursAsNum).sum() == 17 } codecamp 2008 - 30
  31. 31. Xalan XPath import static org.apache.xpath.XPathAPI.* import groovy.xml.DOMBuilder def trip = DOMBuilder.parse(Flights.reader).documentElement assert eval(trip, 'flight/to/text()').str() == 'Los Angeles' assert eval(trip, 'flight[2]/@hours').str() == '4' © ASERT 2006-2008 def flights = selectNodeList(trip, '//flight') def hoursAsInt = { n -> eval(n, '@hours').str().toInteger() } assert flights.collect(hoursAsInt).sum() == 17 assert Flights.getCities(flights) == ['Brisbane', 'Los Angeles', 'New Orleans'] as Set codecamp 2008 - 31
  32. 32. Jaxen XPath import org.jaxen.dom.DOMXPath import groovy.xml.DOMBuilder def trip = DOMBuilder.parse(Flights.reader).documentElement assert new DOMXPath('flight/to/text()'). stringValueOf(trip) == 'Los Angeles' assert new DOMXPath('flight[2]/@hours'). stringValueOf(trip) == '4' © ASERT 2006-2008 def flights = new DOMXPath('flight').selectNodes(trip) def hoursAsInt = { n -> new DOMXPath('@hours').numberValueOf(n) } assert flights.collect(hoursAsInt).sum() == 17 assert Flights.getCities(flights) == ['Brisbane', 'Los Angeles', 'New Orleans'] as Set codecamp 2008 - 32
  33. 33. JSR 225 - XQJ import net.sf.saxon.xqj.SaxonXQDataSource import javax.xml.xquery.XQSequence XQSequence.metaClass.collect = { Closure c -> def items = [] while (delegate.next()) items += c(delegate) items } def asString = { seq -> seq.getItemAsString(null) } © ASERT 2006-2008 def hourAttr = { it.item.node.getAttribute('hours') as int } def flights = quot;document { ${Flights.XML} }quot; def exp = new SaxonXQDataSource().connection.createExpression() def seq = exp.executeQuery(quot;$flights/trip/flight/to/text()quot;) assert seq.collect(asString) == ['Los Angeles', 'New Orleans'] seq = exp.executeQuery(quot;$flights/trip/flightquot;) assert seq.collect(hourAttr).sum() == 17 codecamp 2008 - 33
  34. 34. XSLT... import static javax.xml.transform.TransformerFactory.newInstance as xsltFactory import javax.xml.transform.stream.* def xslt = ''' <xsl:stylesheet xmlns:xsl=quot;http://www.w3.org/1999/XSL/Transformquot; version=quot;1.0quot;> <xsl:template match=quot;/tripquot;> <html> <body> <h1>Flights</h1> <ul> <xsl:apply-templates select=quot;flightquot;/> </ul> © ASERT 2006-2008 </body> </html> </xsl:template> <xsl:template match=quot;flightquot;> <li> <xsl:value-of select=quot;fromquot;/> => <xsl:value-of select=quot;toquot;/> </li> </xsl:template> </xsl:stylesheet> '''.trim() def transformer = xsltFactory().newTransformer( new StreamSource(new StringReader(xslt))) transformer.transform(new StreamSource(Flights.reader), new StreamResult(System.out)) codecamp 2008 - 34
  35. 35. ...XSLT import static javax.xml.transform.TransformerFactory.newInstance as xsltFactory import javax.xml.transform.stream.* def xslt = ''' <html> <xsl:stylesheet xmlns:xsl=quot;http://www.w3.org/1999/XSL/Transformquot; version=quot;1.0quot;> <xsl:template match=quot;/tripquot;> <body> <html> <h1>Flights</h1> <body> <ul> <h1>Flights</h1> <li>Brisbane =&gt; Los Angeles</li> <ul> <li>Los Angeles =&gt; New Orleans</li> </ul> <xsl:apply-templates select=quot;flightquot;/> </ul> </body> © ASERT 2006-2008 </body> </html> </html> </xsl:template> <xsl:template match=quot;flightquot;> <li> <xsl:value-of select=quot;fromquot;/> => <xsl:value-of select=quot;toquot;/> </li> </xsl:template> </xsl:stylesheet> '''.trim() def transformer = xsltFactory().newTransformer( new StreamSource(new StringReader(xslt))) transformer.transform(new StreamSource(Flights.reader), new StreamResult(System.out)) codecamp 2008 - 35
  36. 36. MarkupBuilder import groovy.xml.MarkupBuilder def writer = new StringWriter() def xml = new MarkupBuilder(writer) xml.flights { flight(hours:13) { © ASERT 2006-2008 from('Brisbane') to('Los Angeles') } flight(hours:4) { from('Los Angeles') to('New Orleans') } } println writer codecamp 2008 - 36
  37. 37. StreamingMarkupBuilder import groovy.xml.StreamingMarkupBuilder def writer = new StreamingMarkupBuilder().bind { flights { flight(hours: 13) { from('Brisbane') © ASERT 2006-2008 to('Los Angeles') } flight(hours: 4) { from('Los Angeles') to('New Orleans') } } } println writer codecamp 2008 - 37
  38. 38. DOMBuilder import groovy.xml.* def builder = DOMBuilder.newInstance() def root = builder.flights { flight(hours: 13) { from('Brisbane') © ASERT 2006-2008 to('Los Angeles') } flight(hours: 4) { from('Los Angeles') to('New Orleans') } } new XmlNodePrinter().print(root) codecamp 2008 - 38
  39. 39. MarkupBuilder with Namespaces import groovy.xml.MarkupBuilder def writer = new StringWriter() def xml = new MarkupBuilder(writer) xml.'rdf:description'( 'xmlns:bibterm': quot;http://www.book-stuff.com/terms/quot;, 'xmlns:dc': quot;http://purl.org/dc/elements/1.0/quot;, 'xmlns:rdf': quot;http://www.w3.org/1999/02/22-rdf-syntax- © ASERT 2006-2008 ns#quot;) { 'bibterm:book' { 'dc:title'('Groovy in Action') 'bibterm:year'('2007') } <rdf:description xmlns:dc='http://purl.org/dc/elements/1.0/' } xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' println writer xmlns:bibterm='http://www.book-stuff.com/terms/'> <bibterm:book> <dc:title>Groovy in Action</dc:title> <bibterm:year>2007</bibterm:year> </bibterm:book> </rdf:description> codecamp 2008 - 39
  40. 40. StreamingMarkupBuilder with Namespaces import groovy.xml.* XmlUtil.serialize(new StreamingMarkupBuilder().bind { mkp.declareNamespace( bibterm: quot;http://www.book-stuff.com/terms/quot;, dc: quot;http://purl.org/dc/elements/1.0/quot;, rdf: quot;http://www.w3.org/1999/02/22-rdf-syntax-ns#quot;) 'rdf:description' { © ASERT 2006-2008 'bibterm:book' { 'dc:title'('Groovy in Action') 'bibterm:year'('2007') } } <rdf:description xmlns:dc='http://purl.org/dc/elements/1.0/' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' }, System.out) xmlns:bibterm='http://www.book-stuff.com/terms/'> <bibterm:book> <dc:title>Groovy in Action</dc:title> <bibterm:year>2007</bibterm:year> </bibterm:book> </rdf:description> codecamp 2008 - 40
  41. 41. DOMBuilder with Namespaces import groovy.xml.DOMBuilder def b = DOMBuilder.newInstance() def root = b.'rdf:description'( 'xmlns:bibterm': quot;http://www.book-stuff.com/terms/quot;, 'xmlns:dc': quot;http://purl.org/dc/elements/1.0/quot;, 'xmlns:rdf': quot;http://www.w3.org/1999/02/22-rdf-syntax-ns#quot;) { 'bibterm:book' { 'dc:title'('Groovy in Action') © ASERT 2006-2008 'bibterm:year'('2007') } } new XmlNodePrinter().print(root) <rdf:description xmlns:dc='http://purl.org/dc/elements/1.0/' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:bibterm='http://www.book-stuff.com/terms/'> <bibterm:book> <dc:title>Groovy in Action</dc:title> <bibterm:year>2007</bibterm:year> </bibterm:book> </rdf:description> codecamp 2008 - 41
  42. 42. DOMBuilder with NamespaceBuilder import groovy.xml.* def b = NamespaceBuilder.newInstance(DOMBuilder.newInstance()) b.namespace('http://www.book-stuff.com/terms/', 'bibterm') b.namespace('http://purl.org/dc/elements/1.0/', 'dc') b.namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdf') def root = b.'rdf:description' { 'bibterm:book' { 'dc:title'('Groovy in Action') © ASERT 2006-2008 'bibterm:year'('2007') } } new XmlNodePrinter().print(root) <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?> <rdf:description xmlns:rdf=quot;http://www.w3.org/1999/02/22-rdf-syntax-ns#quot;> <bibterm:book xmlns:bibterm=quot;http://www.book-stuff.com/terms/quot;> <dc:title xmlns:dc=quot;http://purl.org/dc/elements/1.0/quot;>Groovy in Action</dc:title> <bibterm:year>2007</bibterm:year> </bibterm:book> </rdf:description> codecamp 2008 - 42
  43. 43. StaxBuilder import javax.xml.stream.XMLOutputFactory import groovy.xml.StaxBuilder def factory = XMLOutputFactory.newInstance() def writer = new StringWriter() def xmlwriter = factory.createXMLStreamWriter(writer) def builder = new StaxBuilder(xmlwriter) builder.flights { © ASERT 2006-2008 flight(hours: 13) { from('Brisbane') to('Los Angeles') } flight(hours: 4) { from('Los Angeles') to('New Orleans') } } println writer codecamp 2008 - 43
  44. 44. StaxBuilder for non-XML import groovy.xml.StaxBuilder import org.codehaus.jettison.mapped.* @Grab(group='org.codehaus.jettison', module='jettison', version='1.0.1') def writer = new StringWriter() def con = new MappedNamespaceConvention() def mappedWriter = new MappedXMLStreamWriter(con, writer) def builder = new StaxBuilder(mappedWriter) © ASERT 2006-2008 builder.flights { flight(hours: 13) { from('Brisbane') {quot;flightsquot;:{quot;flightquot;:[ to('Los Angeles') { quot;@hoursquot;:quot;13quot;, } quot;fromquot;:quot;Brisbanequot;, flight(hours: 4) { quot;toquot;:quot;Los Angelesquot; }, from('Los Angeles') { quot;@hoursquot;:quot;4quot;, to('New Orleans') quot;fromquot;:quot;Los Angelesquot;, } quot;toquot;:quot;New Orleansquot; } } ]}} println writer codecamp 2008 - 44
  45. 45. Updating XML <shopping> <shopping> <category type=quot;groceriesquot;> <category type=quot;groceriesquot;> <item>Luxury Chocolate</item> <item>Chocolate</item> <item>Luxury Coffee</item> <item>Coffee</item> </category> </category> <category type=quot;suppliesquot;> <category type=quot;suppliesquot;> <item>Paper</item> <item>Paper</item> <item quantity=quot;6quot; © ASERT 2006-2008 <item quantity=quot;4quot;>Pens</item> when=quot;Urgentquot;>Pens</item> </category> </category> <category type=quot;presentquot;> <category type=quot;presentquot;> <item when=quot;Aug 10quot;> <item>Mum's Birthday</item> Kathryn's Birthday <item when=quot;Oct 15quot;> </item> Monica's Birthday </category> </item> </shopping> </category> </shopping> codecamp 2008 - 45
  46. 46. Updating with XmlParser def root = new XmlParser().parseText(input) // modify groceries: quality items please def groceries = root.category.findAll{ it.@type == 'groceries' }.item groceries.each { g -> g.value = 'Luxury ' + g.text() } // modify supplies: we need extra pens © ASERT 2006-2008 def supplies = root.category.findAll{ it.@type == 'supplies' }.item supplies.findAll{ it.text() == 'Pens' }.each { s -> s.@quantity = s.@quantity.toInteger() + 2 s.@when = 'Urgent' } // modify presents: August has come and gone def presentCategory = root.category.find{ it.@type == 'present' } presentCategory.children().clear() presentCategory.appendNode('item', quot;Mum's Birthdayquot;) presentCategory.appendNode('item', [when:'Oct 15'], quot;Monica's Birthdayquot;) codecamp 2008 - 46
  47. 47. Updating with XmlSlurper def root = new XmlSlurper().parseText(input) // modify groceries: quality items please def groceries = root.category.find{ it.@type == 'groceries' } (0..<groceries.item.size()).each { groceries.item[it] = 'Luxury ' + groceries.item[it] } // modify supplies: we need extra pens def pens = root.category.find{ it.@type == 'supplies' © ASERT 2006-2008 }.item.findAll{ it.text() == 'Pens' } pens.each { p -> p.@quantity = (p.@quantity.toInteger() + 2).toString() p.@when = 'Urgent' } // modify presents: August has come and gone def presents = root.category.find{ it.@type == 'present' } presents.replaceNode{ node -> category(type:'present'){ item(quot;Mum's Birthdayquot;) item(quot;Monica's Birthdayquot;, when:'Oct 15') } } codecamp 2008 - 47
  48. 48. Updating with DOMCategory use(DOMCategory) { def categories = DOMBuilder.parse(input).documentElement.category // modify groceries: quality items please def groceries = categories.findAll{ it.'@type' == 'groceries' }[0].item groceries.each { g -> g.value = 'Luxury ' + g.text() } // modify supplies: we need extra pens def supplies = categories.findAll{ it.'@type' == 'supplies' }[0].item © ASERT 2006-2008 supplies.findAll{ it.text() == 'Pens' }.each { s -> s['@quantity'] = s.'@quantity'.toInteger() + 2 s['@when'] = 'Urgent' } // modify presents: August has come and gone def presents = categories.find{ it.'@type' == 'present' } presents.item.each { presents.removeChild(it) } presents.appendNode('item', quot;Mum's Birthdayquot;) presents.appendNode('item', [when:'Oct 15'], quot;Monica's Birthdayquot;) } codecamp 2008 - 48
  49. 49. Groovy and Web Services • SOAP Web Services – GroovySOAP using XFire for Java 1.4 – GroovyWS using CXF for Java 1.5+ – JAXB out of the box for Java 6 – CXF, Axis2, Spring Web Services © ASERT 2006-2008 • RESTful Options – Restlet.org, RESTlet DSL, roll your own – JAX-RS: Jersey, CXF, JBoss RESTeasy • Frameworks layered upon SOA – Synapse, Tuscany, ServiceMix codecamp 2008 - 49
  50. 50. GroovySOAP class MathService { • XFire based double add(double a, double b) { a + b • Java 1.4 } double square(double c) { c * c } } import groovy.net.soap.SoapServer © ASERT 2006-2008 def server = new SoapServer('localhost', 6789) server.setNode('MathService') server.start() import groovy.net.soap.SoapClient def url = 'http://localhost:6789/MathServiceInterface?wsdl' def math = new SoapClient(url) assert math.add(1.0, 2.0) == 3.0 assert math.square(3.0) == 9.0 codecamp 2008 - 50
  51. 51. GroovyWS class MathService { • CXF based double add(double a, double b) { a + b • Java 1.5+ } double square(double c) { c * c } } import groovyx.net.ws.WSServer © ASERT 2006-2008 def server = new WSServer() server.setNode MathService.name, quot;http://localhost:6980/MathServicequot; import groovyx.net.ws.WSClient def url = quot;http://localhost:6980/MathService?wsdlquot; def proxy = new WSClient(url, this.class.classLoader) def result = proxy.add(1.0d, 2.0d) assert result == 3.0d result = proxy.square(3.0d) assert result == 9.0d codecamp 2008 - 51
  52. 52. JAXB Server import javax.xml.ws.Endpoint import javax.jws.WebService import javax.jws.soap.SOAPBinding import javax.jws.WebMethod @WebService(name=quot;Echoquot;, serviceName=quot;EchoServicequot;, targetNamespace=quot;http://jaxws.asert.comquot;) @SOAPBinding(style=SOAPBinding.Style.RPC) © ASERT 2006-2008 class EchoImpl { @WebMethod(operationName = quot;echoquot;) String echo(String message) { println quot;Received: $messagequot; quot;nYou said: quot; + message } } Endpoint.publish(quot;http://localhost:8080/Echoquot;, new EchoImpl()) println 'EchoService published and running ...' codecamp 2008 - 52
  53. 53. JAXB Client • JAXB Client import javax.xml.namespace.QName import com.asert.jaxws.EchoService def url = new URL(quot;http://localhost:8080/Echo?wsdlquot;) def qname = new QName(quot;http://jaxws.asert.comquot;, quot;EchoServicequot;) def echoServer = new EchoService(url, qname).echoPort © ASERT 2006-2008 println echoServer.echo(quot;Today is ${new Date()}quot;) • Build instructions wsimport -d ../build -p com.asert.jaxws http://localhost:8080/Echo?wsdl codecamp 2008 - 53
  54. 54. Raw CXF • Apache CXF helps you build and develop services. You can use frontend programming APIs, like JAX-WS and support is provided for SOAP, XML/HTTP, RESTful HTTP, ... over HTTP, JMS, JBI, ... – Follow instructions for Java but there is also © ASERT 2006-2008 some special things you can do with Groovy codecamp 2008 - 54
  55. 55. Axis2 • Apache Axis is a comprehensive implementation of SOAP – Follow the instructions for Java but use Groovy instead and precompile – Use GroovyShell to call script at runtime • Another article: © ASERT 2006-2008 – http://www.developer.com/services/article.ph p/10928_3570031_2 codecamp 2008 - 55
  56. 56. RESTful options • Several Options – Already supported in discussed frameworks, e.g. CXF – Groovy Restlet DSL http://docs.codehaus.org/display/GROOVY/GroovyRestlet – Jersey © ASERT 2006-2008 http://wikis.sun.com/display/Jersey/Main – restlet.org http://www.restlet.org codecamp 2008 - 56
  57. 57. restlet.org import org.restlet.* import org.restlet.data.* class MailboxResource extends Restlet { void handle(Request request, Response response) { switch (request.method) { case Method.GET: handleGet(request, response) break case Method.PUT: handlePut(request, response) © ASERT 2006-2008 break case Method.POST: handlePost(request, response) break default: // The request method is not allowed; set an error status response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED) response.setAllowedMethods([Method.GET, Method.PUT, Method.POST] as Set) } } void handleGet(request, response) { response.setEntity(quot;Hello, world!quot;, MediaType.TEXT_PLAIN) } // ... codecamp 2008 - 57
  58. 58. GroovyRestlet DSL builder.component { current.servers.add(protocol.HTTP, 8182) application(uri: quot;quot;) { router { def guard = guard(uri: quot;/docsquot;, scheme: challengeScheme.HTTP_BASIC, realm: quot;Restlet Tutorialsquot;) guard.secrets.put(quot;scottquot;, quot;tigerquot;.toCharArray()) guard.next = directory(root: quot;quot;, autoAttach: false) restlet(uri: quot;/users/{user}quot;, handle: {req, resp -> resp.setEntity(quot;Account of user quot;${req.attributes.get('user')}quot;quot;, mediaType.TEXT_PLAIN) © ASERT 2006-2008 }) restlet(uri: quot;/users/{user}/ordersquot;, handle: {req, resp -> resp.setEntity(quot;Orders or user quot;${req.attributes.get('user')}quot;quot;, mediaType.TEXT_PLAIN) }) restlet(uri: quot;/users/{user}/orders/{order}quot;, handle: {req, resp -> def attrs = req.attributes def message = quot;Order quot;${attrs.get('order')}quot; for User quot;${attrs.get('user')}quot;quot; resp.setEntity(message, mediaType.TEXT_PLAIN) }) } } }.start() Source: http://docs.codehaus.org/display/GROOVY/GroovyRestlet codecamp 2008 - 58
  59. 59. Jersey... package com.asert import javax.ws.rs.GET import javax.ws.rs.Path import javax.ws.rs.Produces import static com.sun.jersey.api.container.grizzly.GrizzlyWebContainerFactory.* @Path (quot;/helloworldquot;) class HelloWorldResource { @GET @Produces(quot;text/plainquot;) © ASERT 2006-2008 String getPlainMessage() { quot;Hello Worldquot; } } def baseUri = quot;http://localhost:9998/quot; def initParams = [quot;com.sun.jersey.config.property.packagesquot;: quot;com.asertquot;] println quot;quot;quot; Starting grizzly with Jersey... App WADL available at ${baseUri}application.wadl App available at ${baseUri}helloworld quot;quot;quot; create(baseUri, initParams) codecamp 2008 - 59
  60. 60. ...Jersey... import javax.ws.rs.PathParam import javax.xml.bind.annotation.* class PrettyXml { static print(node) { def writer = new StringWriter() new XmlNodePrinter(new PrintWriter(writer)).print(node) writer.toString() } } © ASERT 2006-2008 @XmlRootElement @XmlAccessorType (XmlAccessType.FIELD) class FlightInfoBean { @XmlAttribute String hours @XmlElement String from, to static populate(num) { def trip = new XmlParser().parse(Flights.reader) def f = trip.flight[num as int] new FlightInfoBean(hours:f.@hours, from:f.from.text(), to:f.to.text()) } } codecamp 2008 - 60
  61. 61. ...Jersey @Path(quot;/flight/xml/{flightnum}quot;) class FlightInfoXml { @GET @Produces(quot;text/xmlquot;) String getXmlMessage(@PathParam('flightnum') String num) { def trip = new XmlParser().parse(Flights.reader) PrettyXml.print(trip.flight[num as int]) } } @Path(quot;/flight/json/{flightnum}quot;) © ASERT 2006-2008 class FlightInfoJson { @GET @Produces(quot;application/jsonquot;) FlightInfoBean getJsonMessage(@PathParam('flightnum') String num) { FlightInfoBean.populate(num) } } @Path(quot;/flight/atom/{flightnum}quot;) class FlightInfoAtom { @GET @Produces(quot;application/atomquot;) FlightInfoBean getAtomMessage(@PathParam('flightnum') String num) { FlightInfoBean.populate(num) } } codecamp 2008 - 61
  62. 62. Jersey output > curl http://localhost:9998/helloworld Hello World > curl http://localhost:9998/flight/xml/1 <flight hours=quot;4quot;> <from> Los Angeles </from> <to> New Orleans © ASERT 2006-2008 </to> </flight> > curl http://localhost:9998/flight/json/1 {quot;@hoursquot;:quot;4quot;,quot;fromquot;:quot;Los Angelesquot;,quot;toquot;:quot;New Orleansquot;} > curl http://localhost:9998/flight/atom/1 <?xml version=quot;1.0quot; encoding=quot;UTF-8quot; standalone=quot;yesquot;?> <flightInfoBean hours=quot;4quot;> <from>Los Angeles</from> <to>New Orleans</to> </flightInfoBean> codecamp 2008 - 62
  63. 63. Synapse • Apache Synapse is a simple, lightweight and high performance Enterprise Service Bus (ESB) with support for XML, Web services, binary and text formats – Groovy scripting, endpoints, Synapse DSL – https://svn.apache.org/repos/asf/synapse/ © ASERT 2006-2008 trunk/java/src/site/resources/presentations/ makingsoagroovyfremantle.pdf codecamp 2008 - 63
  64. 64. ServiceMix • ServiceMix is an open source Enterprise Service Bus (ESB) combining Service Oriented Architecture (SOA), Event Driven Architecture (EDA) and Java Business Integration (JBI) functionality – You can use ServiceMix Scripting © ASERT 2006-2008 – You can use legacy ScriptComponent and GroovyComponent – You can write Groovy JBI components codecamp 2008 - 64
  65. 65. Tuscany... • Tuscany embodies Service Component Architecture (SCA) which defines a simple, service-based model for construction, assembly and deployment of a network of services (existing and new ones) that are defined in a language-neutral way.” © ASERT 2006-2008 – You can define your services using Groovy either using Java mechanisms or Scripting integration codecamp 2008 - 65
  66. 66. ...Tuscany... <composite ...> <component name=quot;CalculatorServiceComponentquot; .../> <component name=quot;AddServiceComponentquot; .../> <component name=quot;SubtractServiceComponentquot;> <tuscany:implementation.java class=quot;calculator.SubtractServiceImplquot;/> </component> <component name=quot;MultiplyServiceComponentquot;> © ASERT 2006-2008 <tuscany:implementation.script language=quot;groovyquot;> def multiply(n1, n2) { using groovyc Compile your Groovy } (with*or without annotations) then n1 n2 </tuscany:implementation.script> Need treat just like normal Java. </component> groovy jar in your runtime classpath. <component name=quot;DivideServiceComponentquot;> <tuscany:implementation.script script=quot;calculator/DivideServiceImpl.groovyquot;/> </component> </composite> codecamp 2008 - 66
  67. 67. ...Tuscany <composite ...> With Groovy scripts either <component name=quot;CalculatorServiceComponentquot; .../> <component name=quot;AddServiceComponentquot; .../> files – no embedded or in <component name=quot;SubtractServiceComponentquot;> compilation necessary. <tuscany:implementation.java class=quot;calculator.SubtractServiceImplquot;/> </component> <component name=quot;MultiplyServiceComponentquot;> © ASERT 2006-2008 <tuscany:implementation.script language=quot;groovyquot;> def multiply(n1, n2) { n1 * n2 } </tuscany:implementation.script> </component> <component name=quot;DivideServiceComponentquot;> <tuscany:implementation.script script=quot;calculator/DivideServiceImpl.groovyquot;/> </component> </composite> codecamp 2008 - 67
  68. 68. More Information: on the web • Web sites – http://groovy.codehaus.org – http://grails.codehaus.org – http://pleac.sourceforge.net/pleac_groovy (many examples) – http://www.asert.com.au/training/java/GV110.htm (workshop) • Mailing list for users – user@groovy.codehaus.org © ASERT 2006-2008 • Information portals – http://www.aboutgroovy.org – http://www.groovyblogs.org • Documentation (1000+ pages) – Getting Started Guide, User Guide, Developer Guide, Testing Guide, Cookbook Examples, Advanced Usage Guide • Books – Several to choose from ... codecamp 2008 - 68
  69. 69. More Information: Groovy in Action © ASERT 2006-2008 codecamp 2008 - 69
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×