KEMBAR78
Atlassian Groovy Plugins | PDF
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

Atlassian Groovy Plugins

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