More Related Content
Similar to XML and Web Services with Groovy (20)
XML and Web Services with Groovy
- 1. XML and Web
Services with Groovy
Dr Paul King, ASERT: @paulk_asert, paulk@asert.com.au
SpringOne2GX - 1
- 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-2009
Groovy = Java – boiler plate code
+ optional dynamic typing
+ closures
+ domain specific languages
+ builders
+ metaprogramming
SpringOne2GX - 2
- 3. Growing Acceptance …
A slow and steady start but now gaining in
momentum, maturity and mindshare
Now free
- 5. … Growing Acceptance …
© ASERT 2006-2009
Groovy and Grails downloads:
70-90K per month and growing SpringOne2gx_Oct2009 - 5
- 6. … Growing Acceptance …
© ASERT 2006-2009
Source: http://www.micropoll.com/akira/mpresult/501697-116746
Source: http://www.grailspodcast.com/
SpringOne2gx_Oct2009 - 6
- 7. … Growing Acceptance …
© ASERT 2006-2009
http://www.jroller.com/scolebourne/entry/devoxx_2008_whiteboard_votes
http://www.java.net
SpringOne2gx_Oct2009 - 7
- 8. … Growing Acceptance …
What alternative JVM language are you using or intending to use
© ASERT 2006-2009
http://www.leonardoborges.com/writings
SpringOne2gx_Oct2009 - 8
- 9. … Growing Acceptance …
© ASERT 2006-2009
http://it-republik.de/jaxenter/quickvote/results/1/poll/44 (translated using http://babelfish.yahoo.com)
SpringOne2gx_Oct2009 - 9
- 11. Better XML Manipulation...
<records> records.xml
<car name='HSV Maloo' make='Holden' year='2006'>
<country>Australia</country>
<record type='speed'>Production Pickup Truck with speed of 271kph</record>
</car>
© ASERT 2006-2009
<car name='P50' make='Peel' year='1962'>
<country>Isle of Man</country>
<record type='size'>Smallest Street-Legal Car at 99cm wide and 59 kg weight</record>
</car>
<car name='Royale' make='Bugatti' year='1931'>
<country>France</country>
<record type='price'>Most Valuable Car at $15 million</record>
</car>
</records>
AUG 2009 - 11
- 12. ...Better XML Manipulation...
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
public class FindYearsJava {
public static void main(String[] args) {
© ASERT 2006-2009
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document document = builder.parse(new File("records.xml"));
NodeList list = document.getElementsByTagName("car");
for (int i = 0; i < list.getLength(); i++) {
Node n = list.item(i);
Node year = n.getAttributes().getNamedItem("year");
System.out.println("year = " + year.getTextContent());
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
AUG 2009 - 12
- 13. ...Better XML Manipulation...
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node; boilerplate
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
public class FindYearsJava {
public static void main(String[] args) {
© ASERT 2006-2009
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document document = builder.parse(new File("records.xml"));
NodeList list = document.getElementsByTagName("car");
for (int i = 0; i < list.getLength(); i++) {
Node n = list.item(i);
Node year = n.getAttributes().getNamedItem("year");
System.out.println("year = " + year.getTextContent());
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
AUG 2009 - 13
- 14. ...Better XML Manipulation
def records = new XmlParser().parse("records.xml")
records.car.each {
© ASERT 2006-2009
println "year = ${it.@year}"
}
year = 2006
year = 1962
year = 1931
AUG 2009 - 14
- 15. XML
SpringOne 2GX 2009. All rights reserved. Do not distribute without permission.
- 16. 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-2009
• Creating XML
– Special Groovy support:
MarkupBuilder and StreamingMarkupBuilder
– Or again, enhanced syntax for your current
favorites
SpringOne2GX - 16
- 17. ... 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-2009
• Verifying XML
– Also DTD, W3C XML Schema, Relax NG in a
similar fashion to Java mechanisms for these
features
SpringOne2GX - 17
- 18. ... 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-2009
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
SpringOne2GX - 18
- 19. An Xml Example ...
import groovy.xml.dom.DOMCategory
class Flights {
static final String XML = '''
<trip>
<flight hours="13">
<from>Brisbane</from>
© ASERT 2006-2009
<to>Los Angeles</to>
</flight>
<flight hours="4">
<from>Los Angeles</from>
<to>New Orleans</to>
</flight>
</trip>
'''
...
SpringOne2GX - 19
- 20. ... An Xml Example
...
static final Reader getReader() {
new StringReader(XML)
}
static final Set getCities(flights) {
Set cities = []
© ASERT 2006-2009
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.
SpringOne2GX - 20
- 21. 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-2009
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
SpringOne2GX - 21
- 22. 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-2009
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
SpringOne2GX - 22
- 23. 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-2009
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 SpringOne2GX - 23
- 24. ...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-2009
def longFlights = trip.flight.findAll(moreThanFiveHours)
def longLaxFlights = longFlights.findAll(arrivingLax)
def longOzLaxFlights = longLaxFlights.findAll(departingOz)
assert longOzLaxFlights.@hours == '13'
SpringOne2GX - 24
- 25. ...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-2009
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 haveSpringOne2GX - 25
to care
- 26. A Namespace Example
class Books {
static final String XML = '''
<rdf:rdf xmlns:bibterm="http://www.book-stuff.com/terms/"
xmlns:dc="http://purl.org/dc/elements/1.0/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:description rdf:about="http://www.book-stuff.com/bib">
<bibterm:book rdf:parseType="Resource">
<bibterm:year>2007</bibterm:year>
<dc:title>Groovy in Action</dc:title>
© ASERT 2006-2009
<bibterm:author rdf:parseType="Resource">
<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>
''' ...
SpringOne2GX - 26
- 27. 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-2009
• 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'
SpringOne2GX - 27
- 28. 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-2009
assert b.'dc:title' == 'Groovy in Action'
assert b.'bibterm:year' == '2007'
SpringOne2GX - 28
- 29. 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-2009
['trip', 'flight', 'flight', 'from', 'to', 'from', 'to']
assert trip.'**'.from*.text() == ['Brisbane', 'Los Angeles']
SpringOne2GX - 29
- 30. What about non-XML?
@Grab('nekohtml:nekohtml:1.9.6.2')
import org.cyberneko.html.parsers.SAXParser
def neko = new 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')
}
© ASERT 2006-2009
data.each { println it }
def neko = new SAXParser() // alternate style
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.javanicus.com/search.html
http://www.dcs.napier.ac.uk/~cs05/groovy/groovy.html
http://www.ej-technologies.com/products/jprofiler/overview.html
SpringOne2GX - 30
- 31. 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-2009
assert dest.firstChild.nodeValue == 'Los Angeles'
assert flights.item(1).getAttribute('hours') == '4'
assert Flights.getCities(flights) ==
['Brisbane', 'Los Angeles', 'New Orleans'] as Set
SpringOne2GX - 31
- 32. 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-2009
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'
SpringOne2GX - 32
- 33. DOMCategory
import groovy.xml.DOMBuilder
import groovy.xml.dom.DOMCategory
def doc = DOMBuilder.parse(Flights.reader)
def trip = doc.documentElement
© ASERT 2006-2009
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
}
SpringOne2GX - 33
- 34. DOM4J
@Grab('dom4j:dom4j:1.6.1')
import org.dom4j.io.SAXReader
def trip = new SAXReader().read(Flights.reader).rootElement
© ASERT 2006-2009
assert trip.elements()[0].elementText('to') == 'Los Angeles'
assert trip.elements()[1].attributeValue('hours') == '4'
SpringOne2GX - 34
- 35. JDOM
@Grab('org.jdom:jdom:1.1')
import org.jdom.input.SAXBuilder
def b = new SAXBuilder()
def trip = b.build(Flights.reader).rootElement
© ASERT 2006-2009
assert trip.children[0].getChildText('to') == 'Los Angeles'
assert trip.children[1].getAttribute('hours').value == '4'
SpringOne2GX - 35
- 36. XOM
@Grab('xom:xom:1.1')
import nu.xom.Builder
def doc = new Builder().build(Flights.reader)
def flights = doc.rootElement.childElements
def to = flights.get(0).getFirstChildElement('to')
© ASERT 2006-2009
assert to.value == 'Los Angeles'
def hours = flights.get(1).getAttribute('hours')
assert hours.value == '4'
SpringOne2GX - 36
- 37. 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-2009
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'
SpringOne2GX - 37
- 38. 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-2009
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'
SpringOne2GX - 38
- 39. 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-2009
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
SpringOne2GX - 39
- 40. 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-2009
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
}
SpringOne2GX - 40
- 41. Xalan XPath
@Grab('xalan:xalan:2.7.1')
import static org.apache.xpath.XPathAPI.*
import groovy.xml.DOMBuilder
def trip = DOMBuilder.parse(Flights.reader).documentElement
assert eval(trip, 'flight/to/text()').str() ==
© ASERT 2006-2009
'Los Angeles'
assert eval(trip, 'flight[2]/@hours').str() == '4'
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
SpringOne2GX - 41
- 42. Jaxen XPath
@Grab('jaxen#jaxen;1.1.1')
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'
© ASERT 2006-2009
assert new DOMXPath('flight[2]/@hours').
stringValueOf(trip) == '4'
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
SpringOne2GX - 42
- 43. 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-2009
def hourAttr = { it.item.node.getAttribute('hours') as int }
def flights = "document { ${Flights.XML} }"
def exp =
new SaxonXQDataSource().connection.createExpression()
def seq = exp.executeQuery("$flights/trip/flight/to/text()")
assert seq.collect(asString) == ['Los Angeles',
'New Orleans']
seq = exp.executeQuery("$flights/trip/flight")
assert seq.collect(hourAttr).sum() == 17
SpringOne2GX - 43
- 44. XSLT...
import static javax.xml.transform.TransformerFactory.newInstance as xsltFactory
import javax.xml.transform.stream.*
def xslt = '''
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/trip">
<html>
<body>
<h1>Flights</h1>
<ul>
<xsl:apply-templates select="flight"/>
</ul>
© ASERT 2006-2009
</body>
</html>
</xsl:template>
<xsl:template match="flight">
<li>
<xsl:value-of select="from"/> => <xsl:value-of select="to"/>
</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))
SpringOne2GX - 44
- 45. ...XSLT
import static javax.xml.transform.TransformerFactory.newInstance as xsltFactory
import javax.xml.transform.stream.*
def xslt = '''
<html>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/trip"> <body>
<html> <h1>Flights</h1>
<body> <ul>
<h1>Flights</h1> <li>Brisbane => Los Angeles</li>
<ul> <li>Los Angeles => New Orleans</li>
</ul>
<xsl:apply-templates select="flight"/>
</ul> </body>
© ASERT 2006-2009
</body> </html>
</html>
</xsl:template>
<xsl:template match="flight">
<li>
<xsl:value-of select="from"/> => <xsl:value-of select="to"/>
</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))
SpringOne2GX - 45
- 46. MarkupBuilder
import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.flights {
flight(hours:13) {
© ASERT 2006-2009
from('Brisbane')
to('Los Angeles')
}
flight(hours:4) {
from('Los Angeles')
to('New Orleans')
}
}
println writer
SpringOne2GX - 46
- 47. StreamingMarkupBuilder
import groovy.xml.StreamingMarkupBuilder
def writer = new StreamingMarkupBuilder().bind {
flights {
flight(hours: 13) {
from('Brisbane')
© ASERT 2006-2009
to('Los Angeles')
}
flight(hours: 4) {
from('Los Angeles')
to('New Orleans')
}
}
}
println writer
SpringOne2GX - 47
- 48. DOMBuilder
import groovy.xml.*
def builder = DOMBuilder.newInstance()
def root = builder.flights {
flight(hours: 13) {
from('Brisbane')
© ASERT 2006-2009
to('Los Angeles')
}
flight(hours: 4) {
from('Los Angeles')
to('New Orleans')
}
}
new XmlNodePrinter().print(root)
SpringOne2GX - 48
- 49. MarkupBuilder with Namespaces
import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.'rdf:description'(
'xmlns:bibterm': "http://www.book-stuff.com/terms/",
'xmlns:dc': "http://purl.org/dc/elements/1.0/",
'xmlns:rdf': "http://www.w3.org/1999/02/22-rdf-syntax-
© ASERT 2006-2009
ns#") {
'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>
SpringOne2GX - 49
- 50. StreamingMarkupBuilder with Namespaces
import groovy.xml.*
XmlUtil.serialize(new StreamingMarkupBuilder().bind {
mkp.declareNamespace(
bibterm: "http://www.book-stuff.com/terms/",
dc: "http://purl.org/dc/elements/1.0/",
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
'rdf:description' {
© ASERT 2006-2009
'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>
SpringOne2GX - 50
- 51. DOMBuilder with Namespaces
import groovy.xml.DOMBuilder
def b = DOMBuilder.newInstance()
def root = b.'rdf:description'(
'xmlns:bibterm': "http://www.book-stuff.com/terms/",
'xmlns:dc': "http://purl.org/dc/elements/1.0/",
'xmlns:rdf': "http://www.w3.org/1999/02/22-rdf-syntax-ns#") {
'bibterm:book' {
'dc:title'('Groovy in Action')
© ASERT 2006-2009
'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>
SpringOne2GX - 51
- 52. 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-2009
'bibterm:year'('2007')
}
}
new XmlNodePrinter().print(root)
<?xml version="1.0" encoding="UTF-8"?>
<rdf:description xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<bibterm:book xmlns:bibterm="http://www.book-stuff.com/terms/">
<dc:title xmlns:dc="http://purl.org/dc/elements/1.0/">Groovy in Action</dc:title>
<bibterm:year>2007</bibterm:year>
</bibterm:book>
</rdf:description>
SpringOne2GX - 52
- 53. 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-2009
flight(hours: 13) {
from('Brisbane')
to('Los Angeles')
}
flight(hours: 4) {
from('Los Angeles')
to('New Orleans')
}
}
println writer
SpringOne2GX - 53
- 54. StaxBuilder for non-XML (JSON)
@Grab('org.codehaus.jettison#jettison;1.1')
import org.codehaus.jettison.mapped.*
import groovy.xml.StaxBuilder
def writer = new StringWriter()
def con = new MappedNamespaceConvention()
def mappedWriter = new MappedXMLStreamWriter(con,
writer)
def builder = new StaxBuilder(mappedWriter)
© ASERT 2006-2009
builder.flights {
flight(hours: 13) {
from('Brisbane') {"flights":{"flight":[
to('Los Angeles') { "@hours":"13",
} "from":"Brisbane",
flight(hours: 4) { "to":"Los Angeles" },
from('Los Angeles') { "@hours":"4",
to('New Orleans') "from":"Los Angeles",
} "to":"New Orleans" }
} ]}}
println writer
SpringOne2GX - 54
- 55. Updating XML
<shopping>
<shopping> <category type="groceries">
<category type="groceries"> <item>Luxury Chocolate</item>
<item>Chocolate</item> <item>Luxury Coffee</item>
<item>Coffee</item> </category>
</category> <category type="supplies">
<category type="supplies"> <item>Paper</item>
<item>Paper</item> <item quantity="6"
© ASERT 2006-2009
<item quantity="4">Pens</item> when="Urgent">Pens</item>
</category> </category>
<category type="present"> <category type="present">
<item when="Aug 10"> <item>Mum's Birthday</item>
Kathryn's Birthday <item when="Oct 15">
</item> Monica's Birthday
</category> </item>
</shopping> </category>
</shopping>
SpringOne2GX - 55
- 56. Updating with XmlParser
...
def root = new XmlParser().parseText(Shopping.XML)
// modify groceries: luxury items please
root.category.find{ it.@type == 'groceries' }.item.each{ g ->
g.value = 'Luxury ' + g.text()
}
// modify supplies: we need extra pens now
root.category.find{
© ASERT 2006-2009
it.@type == 'supplies'
}.item.findAll{ it.text() == 'Pens' }.each{ p ->
p.@quantity = p.@quantity.toInteger() + 2
p.@when = 'Urgent'
}
// modify presents: August has come and gone
def presents = root.category.find{ it.@type == 'present' }
presents.children().clear()
presents.appendNode('item', "Mum's Birthday")
presents.appendNode('item', [when:'Oct 15'], "Monica's Birthday")
...
SpringOne2GX - 56
- 57. Updating with XmlSlurper
...
// modify groceries: luxury 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 now
root.category.find{
it.@type == 'supplies'
© ASERT 2006-2009
}.item.findAll{ it.text() == 'Pens' }.each { p ->
p.@quantity = (p.@quantity.toInteger() + 2).toString()
p.@when = 'Urgent'
}
// modify presents: August has come and gone
root.category.find{ it.@type == 'present' }.replaceNode{ node ->
category(type:'present'){
item("Mum's Birthday")
item("Monica's Birthday", when:'Oct 15')
}
}
...
SpringOne2GX - 57
- 58. Updating with DOMCategory
use(DOMCategory) {
// modify groceries: luxury items please
def groceries = root.category.find{ it.'@type' == 'groceries' }.item
groceries.each { g ->
g.value = 'Luxury ' + g.text()
}
// modify supplies: we need extra pens now
def supplies = root.category.find{ it.'@type' == 'supplies' }.item
supplies.findAll{ it.text() == 'Pens' }.each { p ->
© ASERT 2006-2009
p['@quantity'] = p.'@quantity'.toInteger() + 2
p['@when'] = 'Urgent'
}
// modify presents: August has come and gone
def presents = root.category.find{ it.'@type' == 'present' }
presents.item.each { presents.removeChild(it) }
presents.appendNode('item', "Mum's Birthday")
presents.appendNode('item', [when:'Oct 15'], "Monica's Birthday")
...
}
SpringOne2GX - 58
- 59. Validating against a DTD...
def xml = '''
<!DOCTYPE records [
<!ELEMENT car (country,record)>
<!ATTLIST car
make NMTOKEN #REQUIRED
name CDATA #REQUIRED
year NMTOKEN #REQUIRED
>
<!ELEMENT country (#PCDATA)>
<!ELEMENT record (#PCDATA)>
© ASERT 2006-2009
<!ATTLIST record type NMTOKEN #REQUIRED>
<!ELEMENT records (car+)>
]>
<records>
<car name="HSV Maloo" make="Holden" year="2006">
<country>Australia</country>
<record
type="speed">Production Pickup Truck with speed of 271kph</record>
</car>
...
</records>
'''.trim()
SpringOne2GX - 59
- 60. ...Validating against a DTD
def validating = true
def namespaceAware = false
new XmlParser(validating, namespaceAware).parseText(xml)
© ASERT 2006-2009
new XmlSlurper(validating, namespaceAware).parseText(xml)
import groovy.xml.DOMBuilder
DOMBuilder.parse(new StringReader(xml), validating, namespaceAware)
SpringOne2GX - 60
- 61. Validating against a W3C Schema...
def xsd = '''
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="records">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" ref="car"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="car">
<xs:complexType>
<xs:sequence>
© ASERT 2006-2009
<xs:element ref="country"/>
<xs:element ref="record"/>
</xs:sequence>
<xs:attribute name="make" use="required" type="xs:NCName"/>
<xs:attribute name="name" use="required"/>
<xs:attribute name="year" use="required" type="xs:integer"/>
</xs:complexType>
</xs:element>
<xs:element name="country" type="xs:string"/>
<xs:element name="record">
<xs:complexType mixed="true">
<xs:attribute name="type" use="required" type="xs:NCName"/>
</xs:complexType>
</xs:element>
</xs:schema>
'''.trim()
SpringOne2GX - 61
- 62. ...Validating against a W3C Schema
import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.SchemaFactory
def factory = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
// println factory.isSchemaLanguageSupported(W3C_XML_SCHEMA_NS_URI)
© ASERT 2006-2009
def schema =
factory.newSchema(new StreamSource(new StringReader(xsd)))
def validator = schema.newValidator()
validator.validate(
new StreamSource(new StringReader(XmlExamples.CAR_RECORDS)))
SpringOne2GX - 62
- 63. Validating against a RelaxNG Schema...
def rng = '''
<grammar xmlns="http://relaxng.org/ns/structure/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<start>
<ref name="records"/>
</start>
<define name="car">
<element name="car">
<attribute name="make">
<data type="token"/> ...
</attribute> <define name="record">
<attribute name="name">
© ASERT 2006-2009
<element name="record">
<text/> <attribute name="type">
</attribute> <data type="token"/>
<attribute name="year"> </attribute>
<data type="integer"/> <text/>
</attribute> </element>
<ref name="country"/> </define>
<ref name="record"/> <define name="records">
</element> <element name="records">
</define> <oneOrMore>
<define name="country"> <ref name="car"/>
<element name="country"> </oneOrMore>
<text/> </element>
</element> </define>
</define> </grammar>
... '''.trim()
SpringOne2GX - 63
- 64. ...Validating against a RelaxNG Schema
// require isorelax.jar, isorelax-jaxp-bridge.jar
// require one of Jing, MSV, ...
import static javax.xml.XMLConstants.RELAXNG_NS_URI
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.SchemaFactory
def factory = SchemaFactory.newInstance(RELAXNG_NS_URI)
© ASERT 2006-2009
def schema =
factory.newSchema(new StreamSource(new StringReader(rng)))
def validator = schema.newValidator()
validator.validate(new StreamSource(new StringReader(CAR_RECORDS)))
SpringOne2GX - 64
- 65. WEB SERVICES
SpringOne 2GX 2009. All rights reserved. Do not distribute without permission.
- 66. 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-2009
• RESTful Options
– Restlet.org, RESTlet DSL, roll your own
– JAX-RS: Jersey, CXF, JBoss RESTeasy
• Frameworks layered upon SOA
– Synapse, Tuscany, ServiceMix
SpringOne2GX - 66
- 67. 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-2009
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
SpringOne2GX - 67
- 68. 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-2009
def server = new WSServer()
server.setNode MathService.name,
"http://localhost:6980/MathService"
import groovyx.net.ws.WSClient
def url = "http://localhost:6980/MathService?wsdl"
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
SpringOne2GX - 68
- 69. JAXB Server
import javax.xml.ws.Endpoint
import javax.jws.WebService
import javax.jws.soap.SOAPBinding
import javax.jws.WebMethod
@WebService(name="Echo", serviceName="EchoService",
targetNamespace="http://jaxws.asert.com")
@SOAPBinding(style=SOAPBinding.Style.RPC)
© ASERT 2006-2009
class EchoImpl {
@WebMethod(operationName = "echo")
String echo(String message) {
println "Received: $message"
"nYou said: " + message
}
}
Endpoint.publish("http://localhost:8080/Echo", new EchoImpl())
println 'EchoService published and running ...'
SpringOne2GX - 69
- 70. JAXB Client
• JAXB Client
import javax.xml.namespace.QName
import com.asert.jaxws.EchoService
def url = new URL("http://localhost:8080/Echo?wsdl")
def qname = new QName("http://jaxws.asert.com", "EchoService")
def echoServer = new EchoService(url, qname).echoPort
© ASERT 2006-2009
println echoServer.echo("Today is ${new Date()}")
• Build instructions
wsimport -d ../build -p com.asert.jaxws http://localhost:8080/Echo?wsdl
SpringOne2GX - 70
- 71. 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-2009
some special things you can do with Groovy
SpringOne2GX - 71
- 72. 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-2009
– http://www.developer.com/services/article.ph
p/10928_3570031_2
SpringOne2GX - 72
- 73. 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-2009
http://wikis.sun.com/display/Jersey/Main
– restlet.org
http://www.restlet.org
SpringOne2GX - 73
- 74. 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-2009
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("Hello, world!", MediaType.TEXT_PLAIN)
}
// ...
SpringOne2GX - 74
- 75. GroovyRestlet DSL
builder.component {
current.servers.add(protocol.HTTP, 8182)
application(uri: "") {
router {
def guard = guard(uri: "/docs", scheme: challengeScheme.HTTP_BASIC,
realm: "Restlet Tutorials")
guard.secrets.put("scott", "tiger".toCharArray())
guard.next = directory(root: "", autoAttach: false)
restlet(uri: "/users/{user}", handle: {req, resp ->
resp.setEntity("Account of user "${req.attributes.get('user')}"",
mediaType.TEXT_PLAIN)
© ASERT 2006-2009
})
restlet(uri: "/users/{user}/orders", handle: {req, resp ->
resp.setEntity("Orders or user "${req.attributes.get('user')}"",
mediaType.TEXT_PLAIN)
})
restlet(uri: "/users/{user}/orders/{order}", handle: {req, resp ->
def attrs = req.attributes
def message =
"Order "${attrs.get('order')}" for User "${attrs.get('user')}""
resp.setEntity(message, mediaType.TEXT_PLAIN)
})
}
}
}.start() Source: http://docs.codehaus.org/display/GROOVY/GroovyRestlet
SpringOne2GX - 75
- 76. 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 ("/helloworld")
class HelloWorldResource {
@GET @Produces("text/plain")
© ASERT 2006-2009
String getPlainMessage() {
"Hello World"
}
}
def baseUri = "http://localhost:9998/"
def initParams = ["com.sun.jersey.config.property.packages": "com.asert"]
println """
Starting grizzly with Jersey...
App WADL available at ${baseUri}application.wadl
App available at ${baseUri}helloworld
"""
create(baseUri, initParams)
SpringOne2GX - 76
- 77. ...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-2009
@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())
}
}
SpringOne2GX - 77
- 78. ...Jersey
@Path("/flight/xml/{flightnum}")
class FlightInfoXml {
@GET @Produces("text/xml")
String getXmlMessage(@PathParam('flightnum') String num) {
def trip = new XmlParser().parse(Flights.reader)
PrettyXml.print(trip.flight[num as int])
}
}
@Path("/flight/json/{flightnum}")
© ASERT 2006-2009
class FlightInfoJson {
@GET @Produces("application/json")
FlightInfoBean getJsonMessage(@PathParam('flightnum') String num) {
FlightInfoBean.populate(num)
}
}
@Path("/flight/atom/{flightnum}")
class FlightInfoAtom {
@GET @Produces("application/atom")
FlightInfoBean getAtomMessage(@PathParam('flightnum') String num) {
FlightInfoBean.populate(num)
}
}
SpringOne2GX - 78
- 79. Jersey output
> curl http://localhost:9998/helloworld
Hello World
> curl http://localhost:9998/flight/xml/1
<flight hours="4">
<from>
Los Angeles
</from>
<to>
New Orleans
© ASERT 2006-2009
</to>
</flight>
> curl http://localhost:9998/flight/json/1
{"@hours":"4","from":"Los Angeles","to":"New Orleans"}
> curl http://localhost:9998/flight/atom/1
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<flightInfoBean hours="4">
<from>Los Angeles</from>
<to>New Orleans</to>
</flightInfoBean>
SpringOne2GX - 79
- 80. 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-2009
trunk/java/src/site/resources/presentations/
makingsoagroovyfremantle.pdf
SpringOne2GX - 80
- 81. 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-2009
– You can use legacy ScriptComponent and
GroovyComponent
– You can write Groovy JBI components
SpringOne2GX - 81
- 82. 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-2009
– You can define your services using Groovy
either using Java mechanisms
or Scripting integration
SpringOne2GX - 82
- 83. ...Tuscany...
<composite ...>
<component name="CalculatorServiceComponent" .../>
<component name="AddServiceComponent" .../>
<component name="SubtractServiceComponent">
<tuscany:implementation.java
class="calculator.SubtractServiceImpl"/>
</component>
<component name="MultiplyServiceComponent">
© ASERT 2006-2009
<tuscany:implementation.script language="groovy">
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="DivideServiceComponent">
<tuscany:implementation.script
script="calculator/DivideServiceImpl.groovy"/>
</component>
</composite>
SpringOne2GX - 83
- 84. ...Tuscany
<composite ...>
With Groovy scripts either
<component name="CalculatorServiceComponent" .../>
<component name="AddServiceComponent" .../> files – no
embedded or in
<component name="SubtractServiceComponent">
compilation necessary.
<tuscany:implementation.java
class="calculator.SubtractServiceImpl"/>
</component>
<component name="MultiplyServiceComponent">
© ASERT 2006-2009
<tuscany:implementation.script language="groovy">
def multiply(n1, n2) {
n1 * n2
}
</tuscany:implementation.script>
</component>
<component name="DivideServiceComponent">
<tuscany:implementation.script
script="calculator/DivideServiceImpl.groovy"/>
</component>
</composite>
SpringOne2GX - 84
- 85. 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-2009
• 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 ...
SpringOne2GX - 85