• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Atlassian Groovy Plugins
 

Atlassian Groovy Plugins

on

  • 3,073 views

Using Groovy to write plugins for Atlassian produces such as Jira and Confluence

Using Groovy to write plugins for Atlassian produces such as Jira and Confluence

Statistics

Views

Total Views
3,073
Views on SlideShare
3,042
Embed Views
31

Actions

Likes
3
Downloads
28
Comments
0

2 Embeds 31

http://www.slideshare.net 30
http://www.lmodules.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    Atlassian Groovy Plugins Atlassian Groovy Plugins Presentation Transcript

    • 1
    • Groovy Plugins Why you should be developing Atlassian plugins using Groovy Dr Paul King, Director, ASERT 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.” Groovy = Java – boiler plate code + optional dynamic typing + closures + domain specific languages + builders + meta-programming 3
    • What is Groovy? Now free 4
    • 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
    • Reason: Language Features • Closures • Productivity • Runtime metaprogramming • Clarity • Compile-time metaprogramming • Maintainability • Grape modules • Quality • Builders • Fun • DSL friendly • Shareability 6
    • Reason: Testing • Support for Testing DSLs and • Productivity BDD style tests • Clarity • Built-in assert, power asserts • Maintainability • Built-in testing • Quality • Built-in mocks • Fun • Metaprogramming eases testing pain points • Shareability 7
    • 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 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
    • 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
    • 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
    • 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
    • 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
    • Plugin Tutorial: World of WarCraft... • http://confluence.atlassian.com/display/CONFDEV/ WoW+Macro+explanation 15
    • ...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
    • ...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
    • ...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
    • ...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 ... }
    • ...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 }
    • ...Plugin Tutorial: World of WarCraft... {groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us} 21
    • ...Plugin Tutorial: World of WarCraft... > atlas-mvn clover2:setup test clover2:aggregate clover2:clover 22
    • ...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
    • 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
    • ...Scripting on the fly... 25
    • ...Scripting on the fly 26
    • 27