Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share

Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

  • 1,544 views
Uploaded on

Building Atlassian Plugins with Groovy ...

Building Atlassian Plugins with Groovy

Paul King, ASERT

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
1,544
On Slideshare
960
From Embeds
584
Number of Embeds
10

Actions

Shares
Downloads
16
Comments
0
Likes
1

Embeds 584

http://craigsmith.id.au 411
http://www.atlassian.com 146
http://magnolia-staging.private.atlassian.com 13
https://summit.atlassian.com 4
https://www.atlassian.com 3
https://wacdev.internal.atlassian.com 2
http://www.slideshare.net 2
http://localhost 1
http://willyp.local:8080 1
https://craigsmith.id.au 1

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. 1 1
  • 2. Groovy Plugins Why you should be developing Atlassian plugins using Groovy Dr Paul King, Director, ASERT 2 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 3
  • 4. What is Groovy? Now free 4 4
  • 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 5
  • 6. Reason: Language Features • Closures • Productivity • Runtime metaprogramming • Clarity • Compile-time metaprogramming • Maintainability • Grape modules • Quality • Builders • Fun • DSL friendly 6 6
  • 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 7
  • 8. Myth: Dynamic typing == No IDE support • Completion through inference • Code analysis • Seamless debugging • Seamless refactoring • DSL completion 8 8
  • 9. Myth: Scripting == Non-professional • Analysis tools • Coverage tools • Testing support 9 9
  • 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)  {                                result.add(s); names  =  ["Ted",  "Fred",  "Jed",  "Ned"]                        } println  names                }                return  result; shortNames  =  names.findAll{  it.size()  <=  3  }        }        public  static  void  main(String[]  args)  { println  shortNames.size()                List  names  =  new  ArrayList();                names.add("Ted");  names.add("Fred"); shortNames.each{  println  it  }                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 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)  {                DocumentBuilderFactory  builderFactory  =  DocumentBuilderFactory.newInstance(); records.car.each  {                try  {                        DocumentBuilder  builder  =  builderFactory.newDocumentBuilder();        println  "year  =  ${it.@year}"                        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 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 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 13
  • 14. Groovy @Grab('com.google.collections:google-­‐collections:1.0') import  com.google.common.collect.HashBiMap @Grab('org.gcontracts:gcontracts:1.0.2') 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      @Requires({  delimiter  in  ['.',  ',',  '  ']  }) @Grab('org.codehaus.gpars:gpars:0.10')      @Ensures({  result  ==  first+delimiter+last  }) import  groovyx.gpars.agent.Agent      String  getName(String  delimiter)  {            first  +  delimiter  +  last withPool(5)  {      }        def  nums  =  1..100000 }        println  nums.parallel.                map{  it  **  2  }. new  Person(first:  'John',  last:  'Smith').getName('.')                filter{  it  %  7  ==  it  %  5  }.                filter{  it  %  3  ==  0  }.                reduce{  a,  b  -­‐>  a  +  b  } } Groovy and Gpars both OSGi compliant 14 14
  • 15. Plugin Tutorial: World of WarCraft... • http://confluence.atlassian.com/display/CONFDEV/ WoW+Macro+explanation 15 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 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 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 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.*; catch (IOException e) { List<Toon> toons = retrieveToons(guildName, realmName, zone); 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 out.append("|"); out.append(url); out.append("]"); logged out in off-spec gear * 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", "TotCr25", private List<Toon> retrieveToons(String guildName, String realmName, String zone) public void setCacheManager(CacheManager cacheManager) { "IC" throws MacroException { this.cacheManager = cacheManager; }; String url = null; } ... ... } 19 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 20
  • 21. ...Plugin Tutorial: World of WarCraft... {groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us} 21 21
  • 22. ...Plugin Tutorial: World of WarCraft... > atlas-mvn clover2:setup test clover2:aggregate clover2:clover 22 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          }                "Hunter"      |    3          when  'that  flyer  completes  a  segment  worth  500  points',  {                "Rogue"        |    4                  flyer.fly(new  Segment(500))          }                "Priest"      |    5          then  'that  flyer  has  a  new  rewards  balance  of  2000  points',  {                  flyer.pointsBalance.shouldBe  2000        }          } }•  } • Testing with Spock • Or Cucumber, EasyB, JBehave, 23 23
  • 24. Scripting on the fly... 24 24
  • 25. ...Scripting on the fly... 25 25
  • 26. ...Scripting on the fly 26 26
  • 27. 27 27