0
1
Groovy Plugins
Why you should be developing
Atlassian plugins using Groovy

Dr Paul King, Director, ASERT




            ...
What is Groovy?
 “Groovy is like a super version of Java. It
  can leverage Java's enterprise capabilities
  but also has ...
What is Groovy?




 Now free




                  4
What is Groovy?
                                         What alternative JVM language are you using or intending to use

...
Reason: Language Features
• Closures
                                 • Productivity
• Runtime metaprogramming
           ...
Reason: Testing
• Support for Testing DSLs and     • Productivity
  BDD style tests
                                   • C...
Myth: Dynamic typing == No IDE support
• Completion through inference
• Code analysis
• Seamless debugging
• Seamless refa...
Myth: Scripting == Non-professional
• Analysis tools
• Coverage tools
• Testing support




                              ...
Java                                                            Groovy
import java.util.List;
import java.util.ArrayList;
...
Java                                                                                    Groovy
import   org.w3c.dom.Docume...
Java                                                                                          Groovy
public final class Pu...
Java                                                            Groovy
public class CustomException extends RuntimeExcepti...
Groovy
@Grab('com.google.collections:google-collections:1.0')
import com.google.common.collect.HashBiMap                  ...
Plugin Tutorial: World of WarCraft...
• http://confluence.atlassian.com/display/CONFDEV/
  WoW+Macro+explanation




     ...
...Plugin Tutorial: World of WarCraft...
            • Normal instructions for gmaven:
              http://gmaven.codehau...
...Plugin Tutorial: World of WarCraft...
package com.atlassian.confluence.plugins.wowplugin;                              ...
...Plugin Tutorial: World of WarCraft...
package com.atlassian.confluence.plugins.gwowplugin

class Toon implements Serial...
...Plugin Tutorial: World of WarCraft...
package com.atlassian.confluence.plugins.wowplugin;                    ...       ...
...Plugin Tutorial: World of WarCraft...
package com.atlassian.confluence.plugins.gwowplugin                              ...
...Plugin Tutorial: World of WarCraft...
  {groovy-wow-item:1624}   {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Synd...
...Plugin Tutorial: World of WarCraft...
> atlas-mvn clover2:setup test clover2:aggregate clover2:clover




             ...
...Plugin Tutorial: World of WarCraft
                                                      narrative 'segment flown', {
p...
Scripting on the fly...




 Consider also non-coding alternatives to these plugins, e.g.:   Supports Groovy and other lan...
...Scripting on the fly...




                             25
...Scripting on the fly




                          26
27
Upcoming SlideShare
Loading in...5
×

Atlassian Groovy Plugins

2,708

Published on

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

Published in: Technology
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
2,708
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
29
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

Transcript of "Atlassian Groovy Plugins"

  1. 1. 1
  2. 2. Groovy Plugins Why you should be developing Atlassian plugins using Groovy Dr Paul King, Director, ASERT 2
  3. 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
  4. 4. What is Groovy? Now free 4
  5. 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
  6. 6. Reason: Language Features • Closures • Productivity • Runtime metaprogramming • Clarity • Compile-time metaprogramming • Maintainability • Grape modules • Quality • Builders • Fun • DSL friendly • Shareability 6
  7. 7. 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
  8. 8. Myth: Dynamic typing == No IDE support • Completion through inference • Code analysis • Seamless debugging • Seamless refactoring • DSL completion 8
  9. 9. Myth: Scripting == Non-professional • Analysis tools • Coverage tools • Testing support 9
  10. 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. 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. 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. 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. 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. 15. Plugin Tutorial: World of WarCraft... • http://confluence.atlassian.com/display/CONFDEV/ WoW+Macro+explanation 15
  16. 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. 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. 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. 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. 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. 21. ...Plugin Tutorial: World of WarCraft... {groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us} 21
  22. 22. ...Plugin Tutorial: World of WarCraft... > atlas-mvn clover2:setup test clover2:aggregate clover2:clover 22
  23. 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. 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
  25. 25. ...Scripting on the fly... 25
  26. 26. ...Scripting on the fly 26
  27. 27. 27
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×