2. Groovy Plugins
Why you should be developing
Atlassian plugins using Groovy
Dr Paul King, Director, ASERT
2
2
3. 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.”
Groovy
=
Java
–
boiler
plate
code
+
optional
dynamic
typing
+
closures
+
domain
specific
languages
+
builders
+
meta-‐programming
3
3
5. What is Groovy?
What alternative JVM language are you using or intending to use
http://www.jroller.com/scolebourne/entry/devoxx_2008_whiteboard_votes
http://www.leonardoborges.com/writings
http://it-republik.de/jaxenter/quickvote/results/1/poll/44
(translated using http://babelfish.yahoo.com)
Source: http://www.micropoll.com/akira/mpresult/501697-116746
http://www.java.net
Source: http://www.grailspodcast.com/
5
5
10. Java Groovy
import
java.util.List;
import
java.util.ArrayList;
class
Erase
{
private
List
removeLongerThan(List
strings,
int
length)
{
List
result
=
new
ArrayList();
for
(int
i
=
0;
i
<
strings.size();
i++)
{
String
s
=
(String)
strings.get(i);
if
(s.length()
<=
length)
{
result.add(s);
names
=
["Ted",
"Fred",
"Jed",
"Ned"]
} println
names
}
return
result; shortNames
=
names.findAll{
it.size()
<=
3
}
}
public
static
void
main(String[]
args)
{ println
shortNames.size()
List
names
=
new
ArrayList();
names.add("Ted");
names.add("Fred");
shortNames.each{
println
it
}
names.add("Jed");
names.add("Ned");
System.out.println(names);
Erase
e
=
new
Erase();
List
shortNames
=
e.removeLongerThan(names,
3);
System.out.println(shortNames.size());
for
(int
i
=
0;
i
<
shortNames.size();
i++)
{
String
s
=
(String)
shortNames.get(i);
System.out.println(s);
}
}
}
10
10
11. Java Groovy
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; def
p
=
new
XmlParser()
public
class
FindYearsJava
{ def
records
=
p.parse("records.xml")
public
static
void
main(String[]
args)
{
DocumentBuilderFactory
builderFactory
=
DocumentBuilderFactory.newInstance();
records.car.each
{
try
{
DocumentBuilder
builder
=
builderFactory.newDocumentBuilder();
println
"year
=
${it.@year}"
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();
}
}
}
11
11
12. Java Groovy
public
final
class
Punter
{
//
...
private
final
String
first;
@Override
private
final
String
last;
public
boolean
equals(Object
obj)
{
if
(this
==
obj)
public
String
getFirst()
{
return
true;
return
first;
if
(obj
==
null)
}
return
false;
if
(getClass()
!=
obj.getClass()) @Immutable
class
Punter
{
public
String
getLast()
{
return
false;
return
last;
Punter
other
=
(Punter)
obj;
String
first,
last
}
if
(first
==
null)
{
if
(other.first
!=
null)
}
@Override
return
false;
public
int
hashCode()
{
}
else
if
(!first.equals(other.first))
final
int
prime
=
31;
return
false;
int
result
=
1;
if
(last
==
null)
{
result
=
prime
*
result
+
((first
==
null)
if
(other.last
!=
null)
?
0
:
first.hashCode());
return
false;
result
=
prime
*
result
+
((last
==
null)
}
else
if
(!last.equals(other.last))
?
0
:
last.hashCode());
return
false;
return
result;
return
true;
}
}
public
Punter(String
first,
String
last)
{
@Override
this.first
=
first;
public
String
toString()
{
this.last
=
last;
return
"Punter(first:"
+
first
}
+
",
last:"
+
last
+
")";
//
...
}
}
12
12
13. Java Groovy
public class CustomException extends RuntimeException {
public CustomException() {
super(); @InheritConstructors
}
class CustomException
public CustomException(String message) { extends RuntimeException { }
super(message);
}
public CustomException(String message, Throwable cause) {
super(message, cause);
}
public CustomException(Throwable cause) {
super(cause);
}
}
13
13
14. Groovy
@Grab('com.google.collections:google-‐collections:1.0')
import
com.google.common.collect.HashBiMap @Grab('org.gcontracts:gcontracts:1.0.2') Groovy 1.8+
import
org.gcontracts.annotations.*
HashBiMap
fruit
=
[grape:'purple',
lemon:'yellow',
lime:'green']
@Invariant({
first
!=
null
&&
last
!=
null
})
assert
fruit.lemon
==
'yellow' class
Person
{
assert
fruit.inverse().yellow
==
'lemon'
String
first,
last
@Requires({
delimiter
in
['.',
',',
'
']
})
@Grab('org.codehaus.gpars:gpars:0.10')
@Ensures({
result
==
first+delimiter+last
})
import
groovyx.gpars.agent.Agent
String
getName(String
delimiter)
{
first
+
delimiter
+
last
withPool(5)
{
}
def
nums
=
1..100000
}
println
nums.parallel.
map{
it
**
2
}.
new
Person(first:
'John',
last:
'Smith').getName('.')
filter{
it
%
7
==
it
%
5
}.
filter{
it
%
3
==
0
}.
reduce{
a,
b
-‐>
a
+
b
}
}
Groovy and Gpars both OSGi compliant
14
14
15. Plugin Tutorial: World of WarCraft...
• http://confluence.atlassian.com/display/CONFDEV/
WoW+Macro+explanation
15
15
16. ...Plugin Tutorial: World of WarCraft...
• Normal instructions for gmaven:
http://gmaven.codehaus.org/
...
<plugin>
<groupId>org.codehaus.gmaven</groupId>
<artifactId>gmaven-‐plugin</artifactId>
<version>1.2</version>
<configuration>...</configuration>
<executions>...</executions>
<dependencies>...</dependencies>
</plugin>
...
16
16
17. ...Plugin Tutorial: World of WarCraft...
package
com.atlassian.confluence.plugins.wowplugin; ...
public
String
getName()
{
import
java.io.Serializable;
return
name;
import
java.util.Arrays;
}
import
java.util.List;
public
String
getSpec()
{
/**
return
spec;
*
Simple
data
holder
for
basic
toon
information
}
*/
public
final
class
Toon
implements
Comparable,
Serializable
public
int
getGearScore()
{
{
return
gearScore;
private
static
final
String[]
CLASSES
=
{
}
"Warrior",
"Paladin",
public
List
getRecommendedRaids()
{
"Hunter",
return
recommendedRaids;
"Rogue",
}
"Priest",
"Death
Knight",
public
String
getClassName()
{
"Shaman",
return
className;
"Mage",
}
"Warlock",
"Unknown",
//
There
is
no
class
with
ID
10.
Weird.
public
int
compareTo(Object
o)
"Druid"
{
};
Toon
otherToon
=
(Toon)
o;
private
final
String
name;
if
(otherToon.gearScore
-‐
gearScore
!=
0)
private
final
String
spec;
return
otherToon.gearScore
-‐
gearScore;
private
final
int
gearScore;
private
final
List
recommendedRaids;
return
name.compareTo(otherToon.name);
private
final
String
className;
}
public
Toon(String
name,
int
classId,
String
spec,
int
gearScore,
String...
recommendedRaids)
private
String
toClassName(int
classIndex)
{
{
this.className
=
toClassName(classId
-‐
1);
if
(classIndex
<
0
||
classIndex
>=
CLASSES.length)
this.name
=
name;
return
"Unknown:
"
+
classIndex
+
1;
this.spec
=
spec;
else
this.gearScore
=
gearScore;
return
CLASSES[classIndex];
this.recommendedRaids
=
Arrays.asList(recommendedRaids);
}
} }
...
17
17
18. ...Plugin Tutorial: World of WarCraft...
package
com.atlassian.confluence.plugins.gwowplugin
class
Toon
implements
Serializable
{
private
static
final
String[]
CLASSES
=
[
"Warrior",
"Paladin",
"Hunter",
"Rogue",
"Priest",
"Death
Knight",
"Shaman",
"Mage",
"Warlock",
"Unknown",
"Druid"] 83 -> 17
String
name
int
classId
String
spec
int
gearScore
def
recommendedRaids
String
getClassName()
{
classId
in
0..<CLASSES.length
?
CLASSES[classId
-‐
1]
:
"Unknown:
"
+
classId
}
}
18
18
19. ...Plugin Tutorial: World of WarCraft...
package com.atlassian.confluence.plugins.wowplugin; ... ...
public boolean isInline() { return false; } try {
import com.atlassian.cache.Cache; url = String.format("http://xml.wow-heroes.com/xml-guild.php?z=%s&r=%s&g=%s",
import com.atlassian.cache.CacheManager; public boolean hasBody() { return false; } URLEncoder.encode(zone, "UTF-8"),
import com.atlassian.confluence.util.http.HttpResponse; URLEncoder.encode(realmName, "UTF-8"),
import com.atlassian.confluence.util.http.HttpRetrievalService; public RenderMode getBodyRenderMode() { URLEncoder.encode(guildName, "UTF-8"));
import com.atlassian.renderer.RenderContext; return RenderMode.NO_RENDER; } catch (UnsupportedEncodingException e) {
import com.atlassian.renderer.v2.RenderMode; } throw new MacroException(e.getMessage(), e);
import com.atlassian.renderer.v2.SubRenderer; }
import com.atlassian.renderer.v2.macro.BaseMacro; public String execute(Map map, String s, RenderContext renderContext) throws MacroException {
import com.atlassian.renderer.v2.macro.MacroException; String guildName = (String) map.get("guild"); Cache cache = cacheManager.getCache(this.getClass().getName() + ".toons");
import org.dom4j.Document; String realmName = (String) map.get("realm");
import org.dom4j.DocumentException; String zone = (String) map.get("zone"); if (cache.get(url) != null)
import org.dom4j.Element; if (zone == null) zone = "us"; return (List<Toon>) cache.get(url);
import org.dom4j.io.SAXReader;
StringBuilder out = new StringBuilder("||Name||Class||Gear Score");
try {
for (int i = 0; i < SHORT_RAIDS.length; i++) {
import java.io.IOException; List<Toon> toons = retrieveAndParseFromWowArmory(url);
out.append("||").append(SHORT_RAIDS[i].replace('/', 'n'));
import java.io.InputStream; cache.put(url, toons);
}
import java.io.UnsupportedEncodingException; return toons;
out.append("||n");
import java.net.URLEncoder; }
import java.util.*; catch (IOException e) {
List<Toon> toons = retrieveToons(guildName, realmName, zone);
throw new MacroException("Unable to retrieve information for guild: " + guildName + ", " + e.toString());
/** for (Toon toon : toons) { }
* Inserts a table of a guild's roster of 80s ranked by gear level, with recommended raid instances. The data for catch (DocumentException e) {
* the macro is grabbed from http://wow-heroes.com. Results are cached for $DEFAULT_CACHE_LIFETIME to reduce
out.append("| "); throw new MacroException("Unable to parse information for guild: " + guildName + ", " + e.toString());
* load on the server. try { }
* <p/> }
String url = String.format("http://xml.wow-heroes.com/index.php?zone=%s&server=%s&name=%s",
* Usage: {guild-gear|realm=Nagrand|guild=A New Beginning|zone=us} URLEncoder.encode(zone, "UTF-8"),
* <p/> URLEncoder.encode(realmName, "UTF-8"), private List<Toon> retrieveAndParseFromWowArmory(String url) throws IOException, DocumentException {
* Problems: URLEncoder.encode(toon.getName(), "UTF-8")); List<Toon> toons = new ArrayList<Toon>();
* <p/> out.append("["); out.append(toon.getName()); HttpResponse response = httpRetrievalService.get(url);
* * wow-heroes reports your main spec, but whatever gear you logged out in. So if you out.append("|"); out.append(url); out.append("]");
logged out in off-spec gear
* your number will be wrong } InputStream responseStream = response.getResponse();
* * gear score != ability. l2play nub. catch (UnsupportedEncodingException e) { try {
*/ out.append(toon.getName()); SAXReader reader = new SAXReader();
public class GuildGearMacro extends BaseMacro { } Document doc = reader.read(responseStream);
private HttpRetrievalService httpRetrievalService; List toonsXml = doc.selectNodes("//character");
private SubRenderer subRenderer; out.append(" | "); for (Object o : toonsXml) {
private CacheManager cacheManager; out.append(toon.getClassName()); Element element = (Element) o;
out.append(" ("); toons.add(new Toon(element.attributeValue("name"), Integer.parseInt(element.attributeValue("classId")),
private static final String[] RAIDS = { out.append(toon.getSpec()); element.attributeValue("specName"),
"Heroics", out.append(")"); Integer.parseInt(element.attributeValue("score")), element.attributeValue("suggest").split(";")));
"Naxxramas 10", // and OS10 out.append("|"); }
"Naxxramas 25", // and OS25/EoE10 out.append(toon.getGearScore());
"Ulduar 10", // and EoE25 boolean found = false; Collections.sort(toons);
"Onyxia 10", }
"Ulduar 25", // and ToTCr10 for (String raid : RAIDS) { finally {
"Onyxia 25", if (toon.getRecommendedRaids().contains(raid)) { responseStream.close();
"Trial of the Crusader 25", out.append("|(!)"); }
"Icecrown Citadel 10" found = true; return toons;
}; } else { }
out.append("|").append(found ? "(x)" : "(/)");
private static final String[] SHORT_RAIDS = { } public void setHttpRetrievalService(HttpRetrievalService httpRetrievalService) {
"H", } this.httpRetrievalService = httpRetrievalService;
"Naxx10/OS10", out.append("|n"); }
"Naxx25/OS25/EoE10", }
"Uld10/EoE25", public void setSubRenderer(SubRenderer subRenderer) {
"Ony10", return subRenderer.render(out.toString(), renderContext); this.subRenderer = subRenderer;
"Uld25/TotCr10", } }
"Ony25",
"TotCr25", private List<Toon> retrieveToons(String guildName, String realmName, String zone) public void setCacheManager(CacheManager cacheManager) {
"IC" throws MacroException { this.cacheManager = cacheManager;
}; String url = null; }
... ... }
19
19
20. ...Plugin Tutorial: World of WarCraft...
package
com.atlassian.confluence.plugins.gwowplugin ...
toons.each
{
toon
-‐>
import
com.atlassian.cache.CacheManager
def
url
=
"http://xml.wow-‐heroes.com/index.php?zone=${enc
zone}&server=${enc
map.realm}&name=${enc
toon.name}"
import
com.atlassian.confluence.util.http.HttpRetrievalService
out.append("|
[${toon.name}|${url}]
|
$toon.className
($toon.spec)|
$toon.gearScore")
import
com.atlassian.renderer.RenderContext
boolean
found
=
false
import
com.atlassian.renderer.v2.RenderMode
RAIDS.each
{
raid
-‐>
import
com.atlassian.renderer.v2.SubRenderer
if
(raid
in
toon.recommendedRaids)
{
import
com.atlassian.renderer.v2.macro.BaseMacro
out.append("|(!)")
import
com.atlassian.renderer.v2.macro.MacroException
found
=
true
}
else
{
/**
out.append("|").append(found
?
"(x)"
:
"(/)")
*
Inserts
a
table
of
a
guild's
roster
of
80s
ranked
by
gear
level,
with
recommended
raid
}
*
instances.
The
data
for
the
macro
is
grabbed
from
http://wow-‐heroes.com.
Results
are
} 200 -> 90
*
cached
for
$DEFAULT_CACHE_LIFETIME
to
reduce
load
on
the
server.
out.append("|n")
*
<p/>
}
*
Usage:
{guild-‐gear:realm=Nagrand|guild=A
New
Beginning|zone=us}
subRenderer.render(out.toString(),
renderContext)
*/
}
class
GuildGearMacro
extends
BaseMacro
{
HttpRetrievalService
httpRetrievalService
private
retrieveToons(String
guildName,
String
realmName,
String
zone)
throws
MacroException
{
SubRenderer
subRenderer
def
url
=
"http://xml.wow-‐heroes.com/xml-‐guild.php?z=${enc
zone}&r=${enc
realmName}&g=${enc
guildName}"
CacheManager
cacheManager
def
cache
=
cacheManager.getCache(this.class.name
+
".toons")
if
(!cache.get(url))
cache.put(url,
retrieveAndParseFromWowArmory(url))
private
static
final
String[]
RAIDS
=
[
return
cache.get(url)
"Heroics",
"Naxxramas
10",
"Naxxramas
25",
"Ulduar
10",
"Onyxia
10",
}
"Ulduar
25",
"Onyxia
25",
"Trial
of
the
Crusader
25",
"Icecrown
Citadel
10"]
private
static
final
String[]
SHORT_RAIDS
=
[
private
retrieveAndParseFromWowArmory(String
url)
{
"H",
"Naxx10/OS10",
"Naxx25/OS25/EoE10",
"Uld10/EoE25",
"Ony10",
def
toons
"Uld25/TotCr10",
"Ony25",
"TotCr25",
"IC"]
httpRetrievalService.get(url).response.withReader
{
reader
-‐>
toons
=
new
XmlSlurper().parse(reader).guild.character.collect
{
boolean
isInline()
{
false
}
new
Toon(
boolean
hasBody()
{
false
}
name:
it.@name,
RenderMode
getBodyRenderMode()
{
RenderMode.NO_RENDER
}
classId:
it.@classId.toInteger(),
spec:
it.@specName,
String
execute(Map
map,
String
s,
RenderContext
renderContext)
throws
MacroException
{
gearScore:
it.@score.toInteger(),
def
zone
=
map.zone
?:
"us"
recommendedRaids:
it.@suggest.toString().split(";"))
def
out
=
new
StringBuilder("||Name||Class||Gear
Score")
}
SHORT_RAIDS.each
{
out.append("||").append(it.replace('/',
'n'))
}
}
out.append("||n")
toons.sort{
a,
b
-‐>
a.gearScore
==
b.gearScore
?
a.name
<=>
b.name
:
a.gearScore
<=>
b.gearScore
}
}
def
toons
=
retrieveToons(map.guild,
map.realm,
zone)
...
def
enc(s)
{
URLEncoder.encode(s,
'UTF-‐8')
}
} 20
20
21. ...Plugin Tutorial: World of WarCraft...
{groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us}
21
21
22. ...Plugin Tutorial: World of WarCraft...
> atlas-mvn clover2:setup test clover2:aggregate clover2:clover
22
22
23. ...Plugin Tutorial: World of WarCraft
narrative
'segment
flown',
{
package
com.atlassian.confluence.plugins.gwowplugin
as_a
'frequent
flyer'
i_want
'to
accrue
rewards
points
for
every
segment
I
fly'
class
ToonSpec
extends
spock.lang.Specification
{
so_that
'I
can
receive
free
flights
for
my
dedication
to
the
airline'
def
"successful
name
of
Toon
given
classId"()
{ }
scenario
'segment
flown',
{
given:
given
'a
frequent
flyer
with
a
rewards
balance
of
1500
points'
def
t
=
new
Toon(classId:
thisClassId)
when
'that
flyer
completes
a
segment
worth
500
points'
then
'that
flyer
has
a
new
rewards
balance
of
2000
points'
expect: }
t.className
==
name
scenario
'segment
flown',
{
given
'a
frequent
flyer
with
a
rewards
balance
of
1500
points',
{
where:
flyer
=
new
FrequentFlyer(1500)
name
|
thisClassId
}
"Hunter"
|
3
when
'that
flyer
completes
a
segment
worth
500
points',
{
"Rogue"
|
4
flyer.fly(new
Segment(500))
}
"Priest"
|
5
then
'that
flyer
has
a
new
rewards
balance
of
2000
points',
{
flyer.pointsBalance.shouldBe
2000
}
}
}•
}
• Testing with Spock • Or Cucumber, EasyB, JBehave,
23
23