27
1 1

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

Embed Size (px)

DESCRIPTION

Building Atlassian Plugins with Groovy Paul King, ASERT

Citation preview

Page 1: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

11

Page 2: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

Groovy PluginsWhy you should be developingAtlassian plugins using Groovy

Dr Paul King, Director, ASERT

22

Page 3: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 4: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

What is Groovy?

4

Now free

4

Page 5: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 6: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

Reason: Language Features• Closures

• Runtime metaprogramming

• Compile-time metaprogramming

• Grape modules

• Builders

• DSL friendly

• Productivity

• Clarity

•Maintainability

•Quality

• Fun

66

Page 7: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 8: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

Myth: Dynamic typing == No IDE support• Completion through inference

• Code analysis

• Seamless debugging

• Seamless refactoring

• DSL completion

88

Page 9: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

Myth: Scripting == Non-professional• Analysis tools

• Coverage tools

• Testing support

99

Page 10: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 11: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 12: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 13: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 14: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 15: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

Plugin Tutorial: World of WarCraft...• http://confluence.atlassian.com/display/CONFDEV/

WoW+Macro+explanation

1515

Page 16: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

• 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

Page 17: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 18: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 19: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 20: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

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

Page 21: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

21

...Plugin Tutorial: World of WarCraft...{groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us}

21

Page 22: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

22

...Plugin Tutorial: World of WarCraft...> atlas-mvn clover2:setup test clover2:aggregate clover2:clover

22

Page 23: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

• 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

Page 24: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

24

Scripting on the fly...

24

Page 25: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

25

...Scripting on the fly...

25

Page 26: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

26

...Scripting on the fly

26

Page 27: Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks

2727