Upload
atlassian
View
1.306
Download
4
Tags:
Embed Size (px)
DESCRIPTION
Building Atlassian Plugins with Groovy Paul King, ASERT
Citation preview
11
Groovy PluginsWhy you should be developingAtlassian plugins using Groovy
Dr Paul King, Director, ASERT
22
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
3
What is Groovy?
4
Now free
4
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/
5
Reason: Language Features• Closures
• Runtime metaprogramming
• Compile-time metaprogramming
• Grape modules
• Builders
• DSL friendly
• Productivity
• Clarity
•Maintainability
•Quality
• Fun
66
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
7
Myth: Dynamic typing == No IDE support• Completion through inference
• Code analysis
• Seamless debugging
• Seamless refactoring
• DSL completion
88
Myth: Scripting == Non-professional• Analysis tools
• Coverage tools
• Testing support
99
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
10
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}"}
11
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; }
@Override public 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; } // ...
// ... @Override public 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; }
@Override public String toString() { return "Punter(first:" + first + ", last:" + last + ")"; }
}
12
Java
13
Groovy
@InheritConstructorsclass CustomExceptionextends 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); }}
13
14
@Grab('org.gcontracts:gcontracts:1.0.2')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..100000 println 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+
14
Plugin Tutorial: World of WarCraft...• http://confluence.atlassian.com/display/CONFDEV/
WoW+Macro+explanation
1515
• 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> ...
16
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; else return CLASSES[classIndex]; }}
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"]
String name int classId String spec int gearScore def recommendedRaids
String getClassName() { classId in 0..<CLASSES.length ? CLASSES[classId -‐ 1] : "Unknown: " + classId }}
83 -> 17
18
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; }}
19
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 httpRetrievalService SubRenderer subRenderer CacheManager 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 = false RAIDS.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 toons httpRetrievalService.get(url).response.withReader { reader -‐> toons = new XmlSlurper().parse(reader).guild.character.collect { new Toon( name: it.@name, classId: [email protected](), spec: it.@specName, gearScore: [email protected](), recommendedRaids: [email protected]().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
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
•
• Testing with Spock • Or Cucumber, EasyB, JBehave,
23
...Plugin Tutorial: World of WarCraftpackage 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 } }
23
24
Scripting on the fly...
24
25
...Scripting on the fly...
25
26
...Scripting on the fly
26
2727