View
2.517
Download
0
Category
Tags:
Preview:
DESCRIPTION
Using Groovy to write plugins for Atlassian produces such as Jira and Confluence
Citation preview
1
Groovy Plugins
Why you should be developing
Atlassian plugins using Groovy
Dr Paul King, Director, ASERT
2
What is Groovy?
3
“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
What is Groovy?
4
Now free
What is Groovy?
5
What alternative JVM language are you using or intending to use
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
http://www.jroller.com/scolebourne/entry/devoxx_2008_whiteboard_votes
Source: http://www.grailspodcast.com/
Reason: Language Features
• Closures
• Runtime metaprogramming
• Compile-time metaprogramming
• Grape modules
• Builders
• DSL friendly
• Productivity
• Clarity
• Maintainability
• Quality
• Fun
• Shareability
6
Reason: Testing
• Support for Testing DSLs and
BDD style tests
• Built-in assert, power asserts
• Built-in testing
• Built-in mocks
• Metaprogramming eases testing
pain points
7
• Productivity
• Clarity
• Maintainability
• Quality
• Fun
• Shareability
Myth: Dynamic typing == No IDE support
• Completion through inference
• Code analysis
• Seamless debugging
• Seamless refactoring
• DSL completion
8
Myth: Scripting == Non-professional
• Analysis tools
• Coverage tools
• Testing support
9
Java
10
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);}
}return result;
}public static void main(String[] args) {
List names = new ArrayList();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);
}}
}
names = ["Ted", "Fred", "Jed", "Ned"]println namesshortNames = names.findAll{ it.size() <= 3 }println shortNames.size()shortNames.each{ println it }
Groovy
Java
11
Groovyimport 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) {
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();}
}}
def p = new XmlParser()def records = p.parse("records.xml")records.car.each {
println "year = ${it.@year}"}
Java
12
Groovy
@Immutable class Punter {String first, last
}
public final class Punter {private final String first;private final String last;
public String getFirst() {return first;
}
public String getLast() {return last;
}
@Overridepublic int hashCode() {
final int prime = 31;int result = 1;result = prime * result + ((first == null)
? 0 : first.hashCode());result = prime * result + ((last == null)
? 0 : last.hashCode());return result;
}
public Punter(String first, String last) {this.first = first;this.last = last;
}// ...
// ...@Overridepublic boolean equals(Object obj) {
if (this == obj)return true;
if (obj == null)return false;
if (getClass() != obj.getClass())return false;
Punter other = (Punter) obj;if (first == null) {
if (other.first != null)return false;
} else if (!first.equals(other.first))return false;
if (last == null) {if (other.last != null)
return false;} else if (!last.equals(other.last))
return false;return true;
}
@Overridepublic String toString() {
return "Punter(first:" + first+ ", last:" + last + ")";
}
}
Java
13
Groovy
@InheritConstructors
class CustomException
extends RuntimeException { }
public class CustomException extends RuntimeException {
public CustomException() {
super();
}
public CustomException(String message) {
super(message);
}
public CustomException(String message, Throwable cause) {
super(message, cause);
}
public CustomException(Throwable cause) {
super(cause);
}
}
14
@Grab('org.gcontracts:gcontracts:1.1.1')import org.gcontracts.annotations.*
@Invariant({ first != null && last != null })class Person {
String first, last
@Requires({ delimiter in ['.', ',', ' '] })@Ensures({ result == first+delimiter+last })String getName(String delimiter) {
first + delimiter + last}
}
new Person(first: 'John', last: 'Smith').getName('.')
Groovy
@Grab('org.codehaus.gpars:gpars:0.10')import groovyx.gpars.agent.Agent
withPool(5) {def nums = 1..100000println nums.parallel.
map{ it ** 2 }.filter{ it % 7 == it % 5 }.filter{ it % 3 == 0 }.reduce{ a, b -> a + b }
}
@Grab('com.google.collections:google-collections:1.0')import com.google.common.collect.HashBiMap
HashBiMap fruit =[grape:'purple', lemon:'yellow', lime:'green']
assert fruit.lemon == 'yellow'assert fruit.inverse().yellow == 'lemon'
Groovy and Gpars both OSGi compliant
Groovy 1.8+
Plugin Tutorial: World of WarCraft...
• http://confluence.atlassian.com/display/CONFDEV/
WoW+Macro+explanation
15
• Normal instructions for gmaven:
http://gmaven.codehaus.org/
16
...Plugin Tutorial: World of WarCraft...
...
<plugin><groupId>org.codehaus.gmaven</groupId><artifactId>gmaven-plugin</artifactId><version>1.2</version><configuration>...</configuration><executions>...</executions><dependencies>...</dependencies>
</plugin>...
17
...Plugin Tutorial: World of WarCraft...package com.atlassian.confluence.plugins.wowplugin;
import java.io.Serializable;import java.util.Arrays;import java.util.List;
/*** Simple data holder for basic toon information*/public final class Toon implements Comparable, Serializable{
private static final String[] CLASSES = {"Warrior","Paladin","Hunter","Rogue","Priest","Death Knight","Shaman","Mage","Warlock","Unknown", // There is no class with ID 10. Weird."Druid"
};
private final String name;private final String spec;private final int gearScore;private final List recommendedRaids;private final String className;
public Toon(String name, int classId, String spec, int gearScore, String... recommendedRaids){
this.className = toClassName(classId - 1);this.name = name;this.spec = spec;this.gearScore = gearScore;this.recommendedRaids = Arrays.asList(recommendedRaids);
}...
...public String getName() {
return name;}
public String getSpec() {return spec;
}
public int getGearScore() {return gearScore;
}
public List getRecommendedRaids() {return recommendedRaids;
}
public String getClassName() {return className;
}
public int compareTo(Object o){
Toon otherToon = (Toon) o;
if (otherToon.gearScore - gearScore != 0)return otherToon.gearScore - gearScore;
return name.compareTo(otherToon.name);}
private String toClassName(int classIndex){
if (classIndex < 0 || classIndex >= CLASSES.length)return "Unknown: " + classIndex + 1;
elsereturn CLASSES[classIndex];
}}
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"]
String nameint classIdString specint gearScoredef recommendedRaids
String getClassName() {classId in 0..<CLASSES.length ? CLASSES[classId - 1] : "Unknown: " + classId
}}
83 -> 17
19
...Plugin Tutorial: World of WarCraft...package com.atlassian.confluence.plugins.wowplugin;
import com.atlassian.cache.Cache;
import com.atlassian.cache.CacheManager;
import com.atlassian.confluence.util.http.HttpResponse;
import com.atlassian.confluence.util.http.HttpRetrievalService;
import com.atlassian.renderer.RenderContext;
import com.atlassian.renderer.v2.RenderMode;
import com.atlassian.renderer.v2.SubRenderer;
import com.atlassian.renderer.v2.macro.BaseMacro;
import com.atlassian.renderer.v2.macro.MacroException;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* 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 cached for $DEFAULT_CACHE_LIFETIME to reduce
* load on the server.
* <p/>
* Usage: {guild-gear|realm=Nagrand|guild=A New Beginning|zone=us}
* <p/>
* Problems:
* <p/>
* * wow-heroes reports your main spec, but whatever gear you logged out in. So if you logged out in off-spec gear
* your number will be wrong
* * gear score != ability. l2play nub.
*/
public class GuildGearMacro extends BaseMacro {
private HttpRetrievalService httpRetrievalService;
private SubRenderer subRenderer;
private CacheManager cacheManager;
private static final String[] RAIDS = {
"Heroics",
"Naxxramas 10", // and OS10
"Naxxramas 25", // and OS25/EoE10
"Ulduar 10", // and EoE25
"Onyxia 10",
"Ulduar 25", // and ToTCr10
"Onyxia 25",
"Trial of the Crusader 25",
"Icecrown Citadel 10"
};
private static final String[] SHORT_RAIDS = {
"H",
"Naxx10/OS10",
"Naxx25/OS25/EoE10",
"Uld10/EoE25",
"Ony10",
"Uld25/TotCr10",
"Ony25",
"TotCr25",
"IC"
};
...
...
public boolean isInline() { return false; }
public boolean hasBody() { return false; }
public RenderMode getBodyRenderMode() {
return RenderMode.NO_RENDER;
}
public String execute(Map map, String s, RenderContext renderContext) throws MacroException {
String guildName = (String) map.get("guild");
String realmName = (String) map.get("realm");
String zone = (String) map.get("zone");
if (zone == null) zone = "us";
StringBuilder out = new StringBuilder("||Name||Class||Gear Score");
for (int i = 0; i < SHORT_RAIDS.length; i++) {
out.append("||").append(SHORT_RAIDS[i].replace('/', '\n'));
}
out.append("||\n");
List<Toon> toons = retrieveToons(guildName, realmName, zone);
for (Toon toon : toons) {
out.append("| ");
try {
String url = String.format("http://xml.wow-heroes.com/index.php?zone=%s&server=%s&name=%s",
URLEncoder.encode(zone, "UTF-8"),
URLEncoder.encode(realmName, "UTF-8"),
URLEncoder.encode(toon.getName(), "UTF-8"));
out.append("["); out.append(toon.getName());
out.append("|"); out.append(url); out.append("]");
}
catch (UnsupportedEncodingException e) {
out.append(toon.getName());
}
out.append(" | ");
out.append(toon.getClassName());
out.append(" (");
out.append(toon.getSpec());
out.append(")");
out.append("|");
out.append(toon.getGearScore());
boolean found = false;
for (String raid : RAIDS) {
if (toon.getRecommendedRaids().contains(raid)) {
out.append("|(!)");
found = true;
} else {
out.append("|").append(found ? "(x)" : "(/)");
}
}
out.append("|\n");
}
return subRenderer.render(out.toString(), renderContext);
}
private List<Toon> retrieveToons(String guildName, String realmName, String zone)
throws MacroException {
String url = null;
...
...
try {
url = String.format("http://xml.wow-heroes.com/xml-guild.php?z=%s&r=%s&g=%s",
URLEncoder.encode(zone, "UTF-8"),
URLEncoder.encode(realmName, "UTF-8"),
URLEncoder.encode(guildName, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new MacroException(e.getMessage(), e);
}
Cache cache = cacheManager.getCache(this.getClass().getName() + ".toons");
if (cache.get(url) != null)
return (List<Toon>) cache.get(url);
try {
List<Toon> toons = retrieveAndParseFromWowArmory(url);
cache.put(url, toons);
return toons;
}
catch (IOException e) {
throw new MacroException("Unable to retrieve information for guild: " + guildName + ", " + e.toString());
}
catch (DocumentException e) {
throw new MacroException("Unable to parse information for guild: " + guildName + ", " + e.toString());
}
}
private List<Toon> retrieveAndParseFromWowArmory(String url) throws IOException, DocumentException {
List<Toon> toons = new ArrayList<Toon>();
HttpResponse response = httpRetrievalService.get(url);
InputStream responseStream = response.getResponse();
try {
SAXReader reader = new SAXReader();
Document doc = reader.read(responseStream);
List toonsXml = doc.selectNodes("//character");
for (Object o : toonsXml) {
Element element = (Element) o;
toons.add(new Toon(element.attributeValue("name"), Integer.parseInt(element.attributeValue("classId")),
element.attributeValue("specName"),
Integer.parseInt(element.attributeValue("score")), element.attributeValue("suggest").split(";")));
}
Collections.sort(toons);
}
finally {
responseStream.close();
}
return toons;
}
public void setHttpRetrievalService(HttpRetrievalService httpRetrievalService) {
this.httpRetrievalService = httpRetrievalService;
}
public void setSubRenderer(SubRenderer subRenderer) {
this.subRenderer = subRenderer;
}
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
}
20
...Plugin Tutorial: World of WarCraft...package com.atlassian.confluence.plugins.gwowplugin
import com.atlassian.cache.CacheManagerimport com.atlassian.confluence.util.http.HttpRetrievalServiceimport com.atlassian.renderer.RenderContextimport com.atlassian.renderer.v2.RenderModeimport com.atlassian.renderer.v2.SubRendererimport com.atlassian.renderer.v2.macro.BaseMacroimport com.atlassian.renderer.v2.macro.MacroException
/*** 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* cached for $DEFAULT_CACHE_LIFETIME to reduce load on the server.* <p/>* Usage: {guild-gear:realm=Nagrand|guild=A New Beginning|zone=us}*/class GuildGearMacro extends BaseMacro {HttpRetrievalService httpRetrievalServiceSubRenderer subRendererCacheManager cacheManager
private static final String[] RAIDS = ["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 = ["H", "Naxx10/OS10", "Naxx25/OS25/EoE10", "Uld10/EoE25", "Ony10","Uld25/TotCr10", "Ony25", "TotCr25", "IC"]
boolean isInline() { false }boolean hasBody() { false }RenderMode getBodyRenderMode() { RenderMode.NO_RENDER }
String execute(Map map, String s, RenderContext renderContext) throws MacroException {def zone = map.zone ?: "us"def out = new StringBuilder("||Name||Class||Gear Score")SHORT_RAIDS.each { out.append("||").append(it.replace('/', '\n')) }out.append("||\n")
def toons = retrieveToons(map.guild, map.realm, zone)...
...toons.each { toon ->
def url = "http://xml.wow-heroes.com/index.php?zone=${enc zone}&server=${enc map.realm}&name=${enc toon.name}"out.append("| [${toon.name}|${url}] | $toon.className ($toon.spec)| $toon.gearScore")boolean found = falseRAIDS.each { raid ->
if (raid in toon.recommendedRaids) {out.append("|(!)")found = true
} else {out.append("|").append(found ? "(x)" : "(/)")
}}out.append("|\n")
}subRenderer.render(out.toString(), renderContext)
}
private retrieveToons(String guildName, String realmName, String zone) throws MacroException {def url = "http://xml.wow-heroes.com/xml-guild.php?z=${enc zone}&r=${enc realmName}&g=${enc guildName}"def cache = cacheManager.getCache(this.class.name + ".toons")if (!cache.get(url)) cache.put(url, retrieveAndParseFromWowArmory(url))return cache.get(url)
}
private retrieveAndParseFromWowArmory(String url) {def toonshttpRetrievalService.get(url).response.withReader { reader ->
toons = new XmlSlurper().parse(reader).guild.character.collect {new Toon(
name: it.@name,classId: it.@classId.toInteger(),spec: it.@specName,gearScore: it.@score.toInteger(),recommendedRaids: it.@suggest.toString().split(";"))
}}toons.sort{ a, b -> a.gearScore == b.gearScore ? a.name <=> b.name : a.gearScore <=> b.gearScore }
}
def enc(s) { URLEncoder.encode(s, 'UTF-8') }}
200 -> 90
21
...Plugin Tutorial: World of WarCraft...{groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us}
22
...Plugin Tutorial: World of WarCraft...> atlas-mvn clover2:setup test clover2:aggregate clover2:clover
•
• Testing with Spock • Or Cucumber, EasyB, JBehave,
Robot Framework, JUnit, TestNg23
...Plugin Tutorial: World of WarCraft
package com.atlassian.confluence.plugins.gwowplugin
class ToonSpec extends spock.lang.Specification {def "successful name of Toon given classId"() {
given:def t = new Toon(classId: thisClassId)
expect:t.className == name
where:name | thisClassId"Hunter" | 3"Rogue" | 4"Priest" | 5
}}
narrative 'segment flown', {as_a 'frequent flyer'i_want 'to accrue rewards points for every segment I fly'so_that 'I can receive free flights for my dedication to the airline'
}
scenario 'segment flown', {given 'a frequent flyer with a rewards balance of 1500 points'when 'that flyer completes a segment worth 500 points'then 'that flyer has a new rewards balance of 2000 points'
}
scenario 'segment flown', {given 'a frequent flyer with a rewards balance of 1500 points', {
flyer = new FrequentFlyer(1500)}when 'that flyer completes a segment worth 500 points', {
flyer.fly(new Segment(500))}then 'that flyer has a new rewards balance of 2000 points', {
flyer.pointsBalance.shouldBe 2000}
}
24
Scripting on the fly...
Consider also non-coding alternatives to these plugins, e.g.:
http://wiki.angry.com.au/display/WOW/Wow-Heros+User+Macro
Supports Groovy and other languages in:
Conditions, Post-Functions, Validators and Services
25
...Scripting on the fly...
26
...Scripting on the fly
27
Recommended