2. Groovy Plugins
Why you should be developing
Atlassian plugins using Groovy
Dr Paul King, Director, ASERT
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
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
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) { names = ["Ted", "Fred", "Jed", "Ned"]
result.add(s);
} println names
}
return result;
shortNames = names.findAll{ it.size() <= 3 }
} println shortNames.size()
public static void main(String[] args) {
List names = new ArrayList(); shortNames.each{ println it }
names.add("Ted"); names.add("Fred");
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
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) { records.car.each {
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
try { println "year = ${it.@year}"
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();
}
}
}
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
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
14. Groovy
@Grab('com.google.collections:google-collections:1.0')
import com.google.common.collect.HashBiMap @Grab('org.gcontracts:gcontracts:1.1.1') 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
@Grab('org.codehaus.gpars:gpars:0.10') @Requires({ delimiter in ['.', ',', ' '] })
import groovyx.gpars.agent.Agent @Ensures({ result == first+delimiter+last })
String getName(String delimiter) {
withPool(5) { first + delimiter + last
def nums = 1..100000 }
println nums.parallel. }
map{ it ** 2 }.
filter{ it % 7 == it % 5 }. new Person(first: 'John', last: 'Smith').getName('.')
filter{ it % 3 == 0 }.
reduce{ a, b -> a + b }
} Groovy and Gpars both OSGi compliant
14
15. Plugin Tutorial: World of WarCraft...
• http://confluence.atlassian.com/display/CONFDEV/
WoW+Macro+explanation
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
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
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
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.*; List<Toon> toons = retrieveToons(guildName, realmName, zone); catch (IOException e) {
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 logged out in off-spec gear
out.append("|"); out.append(url); out.append("]");
* 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",
private List<Toon> retrieveToons(String guildName, String realmName, String zone)
};
"TotCr25",
"IC"
...
throws MacroException {
String url = null;
public void setCacheManager(CacheManager cacheManager) {
}
this.cacheManager = cacheManager; 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
}
21. ...Plugin Tutorial: World of WarCraft...
{groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us}
21
22. ...Plugin Tutorial: World of WarCraft...
> atlas-mvn clover2:setup test clover2:aggregate clover2:clover
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 when 'that flyer completes a segment worth 500 points', {
"Hunter" | 3 flyer.fly(new Segment(500))
"Rogue" | 4 }
"Priest" | 5 then 'that flyer has a new rewards balance of 2000 points', {
flyer.pointsBalance.shouldBe 2000
}
•
} }
}
• Testing with Spock • Or Cucumber, EasyB, JBehave,
Robot Framework, JUnit, TestNg
23
24. Scripting on the fly...
Consider also non-coding alternatives to these plugins, e.g.: Supports Groovy and other languages in:
http://wiki.angry.com.au/display/WOW/Wow-Heros+User+Macro Conditions, Post-Functions, Validators and Services
24