SlideShare uma empresa Scribd logo
1 de 27
Baixar para ler offline
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

Mais conteúdo relacionado

Mais procurados

Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...
Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...
Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...Guillaume Laforge
 
Oscon Java Testing on the Fast Lane
Oscon Java Testing on the Fast LaneOscon Java Testing on the Fast Lane
Oscon Java Testing on the Fast LaneAndres Almiray
 
GR8Conf 2009: Practical Groovy DSL by Guillaume Laforge
GR8Conf 2009: Practical Groovy DSL by Guillaume LaforgeGR8Conf 2009: Practical Groovy DSL by Guillaume Laforge
GR8Conf 2009: Practical Groovy DSL by Guillaume LaforgeGR8Conf
 
Groovy and Grails in Action - Devoxx 2008 - University - Guillaume Laforge
Groovy and Grails in Action - Devoxx 2008 - University - Guillaume LaforgeGroovy and Grails in Action - Devoxx 2008 - University - Guillaume Laforge
Groovy and Grails in Action - Devoxx 2008 - University - Guillaume LaforgeGuillaume Laforge
 
groovy transforms
groovy transformsgroovy transforms
groovy transformsPaul King
 
Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020
Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020
Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020Johnny Sung
 
Groovy overview, DSLs and ecosystem - Mars JUG - 2010
Groovy overview, DSLs and ecosystem - Mars JUG - 2010Groovy overview, DSLs and ecosystem - Mars JUG - 2010
Groovy overview, DSLs and ecosystem - Mars JUG - 2010Guillaume Laforge
 
Making Java Groovy (JavaOne 2013)
Making Java Groovy (JavaOne 2013)Making Java Groovy (JavaOne 2013)
Making Java Groovy (JavaOne 2013)Ken Kousen
 
2007 09 10 Fzi Training Groovy Grails V Ws
2007 09 10 Fzi Training Groovy Grails V Ws2007 09 10 Fzi Training Groovy Grails V Ws
2007 09 10 Fzi Training Groovy Grails V Wsloffenauer
 
Polyglot Programming in the JVM
Polyglot Programming in the JVMPolyglot Programming in the JVM
Polyglot Programming in the JVMAndres Almiray
 
Eclipsecon08 Introduction To Groovy
Eclipsecon08 Introduction To GroovyEclipsecon08 Introduction To Groovy
Eclipsecon08 Introduction To GroovyAndres Almiray
 
Groovy Domain Specific Languages - SpringOne2GX 2012
Groovy Domain Specific Languages - SpringOne2GX 2012Groovy Domain Specific Languages - SpringOne2GX 2012
Groovy Domain Specific Languages - SpringOne2GX 2012Guillaume Laforge
 
Beyond JVM - YOW! Sydney 2013
Beyond JVM - YOW! Sydney 2013Beyond JVM - YOW! Sydney 2013
Beyond JVM - YOW! Sydney 2013Charles Nutter
 
DSL's with Groovy
DSL's with GroovyDSL's with Groovy
DSL's with Groovypaulbowler
 
Infinum android talks_10_getting groovy on android
Infinum android talks_10_getting groovy on androidInfinum android talks_10_getting groovy on android
Infinum android talks_10_getting groovy on androidInfinum
 
API management with Taffy and API Blueprint
API management with Taffy and API BlueprintAPI management with Taffy and API Blueprint
API management with Taffy and API BlueprintKai Koenig
 

Mais procurados (20)

Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...
Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...
Groovy DSLs, from Beginner to Expert - Guillaume Laforge and Paul King - Spri...
 
Oscon Java Testing on the Fast Lane
Oscon Java Testing on the Fast LaneOscon Java Testing on the Fast Lane
Oscon Java Testing on the Fast Lane
 
GR8Conf 2009: Practical Groovy DSL by Guillaume Laforge
GR8Conf 2009: Practical Groovy DSL by Guillaume LaforgeGR8Conf 2009: Practical Groovy DSL by Guillaume Laforge
GR8Conf 2009: Practical Groovy DSL by Guillaume Laforge
 
Polyglot Grails
Polyglot GrailsPolyglot Grails
Polyglot Grails
 
Groovy and Grails in Action - Devoxx 2008 - University - Guillaume Laforge
Groovy and Grails in Action - Devoxx 2008 - University - Guillaume LaforgeGroovy and Grails in Action - Devoxx 2008 - University - Guillaume Laforge
Groovy and Grails in Action - Devoxx 2008 - University - Guillaume Laforge
 
Groovy & Grails
Groovy & GrailsGroovy & Grails
Groovy & Grails
 
groovy transforms
groovy transformsgroovy transforms
groovy transforms
 
Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020
Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020
Flutter 是什麼?用 Flutter 會省到時間嗎? @ GDG Devfest2020
 
Polyglot JVM
Polyglot JVMPolyglot JVM
Polyglot JVM
 
Groovy overview, DSLs and ecosystem - Mars JUG - 2010
Groovy overview, DSLs and ecosystem - Mars JUG - 2010Groovy overview, DSLs and ecosystem - Mars JUG - 2010
Groovy overview, DSLs and ecosystem - Mars JUG - 2010
 
Practical Groovy DSL
Practical Groovy DSLPractical Groovy DSL
Practical Groovy DSL
 
Making Java Groovy (JavaOne 2013)
Making Java Groovy (JavaOne 2013)Making Java Groovy (JavaOne 2013)
Making Java Groovy (JavaOne 2013)
 
2007 09 10 Fzi Training Groovy Grails V Ws
2007 09 10 Fzi Training Groovy Grails V Ws2007 09 10 Fzi Training Groovy Grails V Ws
2007 09 10 Fzi Training Groovy Grails V Ws
 
Polyglot Programming in the JVM
Polyglot Programming in the JVMPolyglot Programming in the JVM
Polyglot Programming in the JVM
 
Eclipsecon08 Introduction To Groovy
Eclipsecon08 Introduction To GroovyEclipsecon08 Introduction To Groovy
Eclipsecon08 Introduction To Groovy
 
Groovy Domain Specific Languages - SpringOne2GX 2012
Groovy Domain Specific Languages - SpringOne2GX 2012Groovy Domain Specific Languages - SpringOne2GX 2012
Groovy Domain Specific Languages - SpringOne2GX 2012
 
Beyond JVM - YOW! Sydney 2013
Beyond JVM - YOW! Sydney 2013Beyond JVM - YOW! Sydney 2013
Beyond JVM - YOW! Sydney 2013
 
DSL's with Groovy
DSL's with GroovyDSL's with Groovy
DSL's with Groovy
 
Infinum android talks_10_getting groovy on android
Infinum android talks_10_getting groovy on androidInfinum android talks_10_getting groovy on android
Infinum android talks_10_getting groovy on android
 
API management with Taffy and API Blueprint
API management with Taffy and API BlueprintAPI management with Taffy and API Blueprint
API management with Taffy and API Blueprint
 

Destaque

WJAX 2008 - Grails Plug-ins verwenden und selbst entwickeln
WJAX 2008 - Grails Plug-ins verwenden und selbst entwickelnWJAX 2008 - Grails Plug-ins verwenden und selbst entwickeln
WJAX 2008 - Grails Plug-ins verwenden und selbst entwickelnmguillem
 
concurrency gpars
concurrency gparsconcurrency gpars
concurrency gparsPaul King
 
Make Your Builds More Groovy
Make Your Builds More GroovyMake Your Builds More Groovy
Make Your Builds More GroovyPaul King
 
Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...
Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...
Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...Paul King
 
WebTest - Efficient Functional Web Testing with HtmlUnit and Beyond
WebTest - Efficient Functional Web Testing with HtmlUnit and BeyondWebTest - Efficient Functional Web Testing with HtmlUnit and Beyond
WebTest - Efficient Functional Web Testing with HtmlUnit and Beyondmguillem
 
Gpars concepts explained
Gpars concepts explainedGpars concepts explained
Gpars concepts explainedVaclav Pech
 
concurrency with GPars
concurrency with GParsconcurrency with GPars
concurrency with GParsPaul King
 
groovy and concurrency
groovy and concurrencygroovy and concurrency
groovy and concurrencyPaul King
 
Vert.x introduction
Vert.x introductionVert.x introduction
Vert.x introductionGR8Conf
 

Destaque (10)

GroovyDSLs
GroovyDSLsGroovyDSLs
GroovyDSLs
 
WJAX 2008 - Grails Plug-ins verwenden und selbst entwickeln
WJAX 2008 - Grails Plug-ins verwenden und selbst entwickelnWJAX 2008 - Grails Plug-ins verwenden und selbst entwickeln
WJAX 2008 - Grails Plug-ins verwenden und selbst entwickeln
 
concurrency gpars
concurrency gparsconcurrency gpars
concurrency gpars
 
Make Your Builds More Groovy
Make Your Builds More GroovyMake Your Builds More Groovy
Make Your Builds More Groovy
 
Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...
Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...
Industrial Strength Groovy - Tools for the Professional Groovy Developer: Pau...
 
WebTest - Efficient Functional Web Testing with HtmlUnit and Beyond
WebTest - Efficient Functional Web Testing with HtmlUnit and BeyondWebTest - Efficient Functional Web Testing with HtmlUnit and Beyond
WebTest - Efficient Functional Web Testing with HtmlUnit and Beyond
 
Gpars concepts explained
Gpars concepts explainedGpars concepts explained
Gpars concepts explained
 
concurrency with GPars
concurrency with GParsconcurrency with GPars
concurrency with GPars
 
groovy and concurrency
groovy and concurrencygroovy and concurrency
groovy and concurrency
 
Vert.x introduction
Vert.x introductionVert.x introduction
Vert.x introduction
 

Semelhante a Atlassian Groovy Plugins

Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks
Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning TalksBuilding Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks
Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning TalksAtlassian
 
Boosting Your Testing Productivity with Groovy
Boosting Your Testing Productivity with GroovyBoosting Your Testing Productivity with Groovy
Boosting Your Testing Productivity with GroovyJames Williams
 
Javaone2008 Bof 5101 Groovytesting
Javaone2008 Bof 5101 GroovytestingJavaone2008 Bof 5101 Groovytesting
Javaone2008 Bof 5101 GroovytestingAndres Almiray
 
DevNexus 2020: Discover Modern Java
DevNexus 2020: Discover Modern JavaDevNexus 2020: Discover Modern Java
DevNexus 2020: Discover Modern JavaHenri Tremblay
 
Scalable and Flexible Machine Learning With Scala @ LinkedIn
Scalable and Flexible Machine Learning With Scala @ LinkedInScalable and Flexible Machine Learning With Scala @ LinkedIn
Scalable and Flexible Machine Learning With Scala @ LinkedInVitaly Gordon
 
Java 7 Whats New(), Whats Next() from Oredev
Java 7 Whats New(), Whats Next() from OredevJava 7 Whats New(), Whats Next() from Oredev
Java 7 Whats New(), Whats Next() from OredevMattias Karlsson
 
55 new things in Java 7 - Devoxx France
55 new things in Java 7 - Devoxx France55 new things in Java 7 - Devoxx France
55 new things in Java 7 - Devoxx FranceDavid Delabassee
 
GTAC Boosting your Testing Productivity with Groovy
GTAC Boosting your Testing Productivity with GroovyGTAC Boosting your Testing Productivity with Groovy
GTAC Boosting your Testing Productivity with GroovyAndres Almiray
 
Apache Groovy: the language and the ecosystem
Apache Groovy: the language and the ecosystemApache Groovy: the language and the ecosystem
Apache Groovy: the language and the ecosystemKostas Saidis
 
Lambda Chops - Recipes for Simpler, More Expressive Code
Lambda Chops - Recipes for Simpler, More Expressive CodeLambda Chops - Recipes for Simpler, More Expressive Code
Lambda Chops - Recipes for Simpler, More Expressive CodeIan Robertson
 
Apache Groovy's Metaprogramming Options and You
Apache Groovy's Metaprogramming Options and YouApache Groovy's Metaprogramming Options and You
Apache Groovy's Metaprogramming Options and YouAndres Almiray
 
Back to the future with Java 7 (Geekout June/2011)
Back to the future with Java 7 (Geekout June/2011)Back to the future with Java 7 (Geekout June/2011)
Back to the future with Java 7 (Geekout June/2011)Martijn Verburg
 
Refactoring In Tdd The Missing Part
Refactoring In Tdd The Missing PartRefactoring In Tdd The Missing Part
Refactoring In Tdd The Missing PartGabriele Lana
 
Groovy Introduction - JAX Germany - 2008
Groovy Introduction - JAX Germany - 2008Groovy Introduction - JAX Germany - 2008
Groovy Introduction - JAX Germany - 2008Guillaume Laforge
 

Semelhante a Atlassian Groovy Plugins (20)

Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks
Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning TalksBuilding Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks
Building Atlassian Plugins with Groovy - Atlassian Summit 2010 - Lightning Talks
 
Boosting Your Testing Productivity with Groovy
Boosting Your Testing Productivity with GroovyBoosting Your Testing Productivity with Groovy
Boosting Your Testing Productivity with Groovy
 
Javaone2008 Bof 5101 Groovytesting
Javaone2008 Bof 5101 GroovytestingJavaone2008 Bof 5101 Groovytesting
Javaone2008 Bof 5101 Groovytesting
 
55 New Features in Java 7
55 New Features in Java 755 New Features in Java 7
55 New Features in Java 7
 
Java 7 & 8 New Features
Java 7 & 8 New FeaturesJava 7 & 8 New Features
Java 7 & 8 New Features
 
DevNexus 2020: Discover Modern Java
DevNexus 2020: Discover Modern JavaDevNexus 2020: Discover Modern Java
DevNexus 2020: Discover Modern Java
 
Scalable and Flexible Machine Learning With Scala @ LinkedIn
Scalable and Flexible Machine Learning With Scala @ LinkedInScalable and Flexible Machine Learning With Scala @ LinkedIn
Scalable and Flexible Machine Learning With Scala @ LinkedIn
 
Svcc Groovy Testing
Svcc Groovy TestingSvcc Groovy Testing
Svcc Groovy Testing
 
Java 7 Whats New(), Whats Next() from Oredev
Java 7 Whats New(), Whats Next() from OredevJava 7 Whats New(), Whats Next() from Oredev
Java 7 Whats New(), Whats Next() from Oredev
 
55 new things in Java 7 - Devoxx France
55 new things in Java 7 - Devoxx France55 new things in Java 7 - Devoxx France
55 new things in Java 7 - Devoxx France
 
GTAC Boosting your Testing Productivity with Groovy
GTAC Boosting your Testing Productivity with GroovyGTAC Boosting your Testing Productivity with Groovy
GTAC Boosting your Testing Productivity with Groovy
 
Apache Groovy: the language and the ecosystem
Apache Groovy: the language and the ecosystemApache Groovy: the language and the ecosystem
Apache Groovy: the language and the ecosystem
 
Lambda Chops - Recipes for Simpler, More Expressive Code
Lambda Chops - Recipes for Simpler, More Expressive CodeLambda Chops - Recipes for Simpler, More Expressive Code
Lambda Chops - Recipes for Simpler, More Expressive Code
 
Apache Groovy's Metaprogramming Options and You
Apache Groovy's Metaprogramming Options and YouApache Groovy's Metaprogramming Options and You
Apache Groovy's Metaprogramming Options and You
 
Having Fun with Play
Having Fun with PlayHaving Fun with Play
Having Fun with Play
 
Java
JavaJava
Java
 
Back to the future with Java 7 (Geekout June/2011)
Back to the future with Java 7 (Geekout June/2011)Back to the future with Java 7 (Geekout June/2011)
Back to the future with Java 7 (Geekout June/2011)
 
Java
JavaJava
Java
 
Refactoring In Tdd The Missing Part
Refactoring In Tdd The Missing PartRefactoring In Tdd The Missing Part
Refactoring In Tdd The Missing Part
 
Groovy Introduction - JAX Germany - 2008
Groovy Introduction - JAX Germany - 2008Groovy Introduction - JAX Germany - 2008
Groovy Introduction - JAX Germany - 2008
 

Mais de Paul King

groovy databases
groovy databasesgroovy databases
groovy databasesPaul King
 
tictactoe groovy
tictactoe groovytictactoe groovy
tictactoe groovyPaul King
 
Make Testing Groovy
Make Testing GroovyMake Testing Groovy
Make Testing GroovyPaul King
 
Agile Testing Practices
Agile Testing PracticesAgile Testing Practices
Agile Testing PracticesPaul King
 
Dynamic Language Practices
Dynamic Language PracticesDynamic Language Practices
Dynamic Language PracticesPaul King
 
Groovy Power Features
Groovy Power FeaturesGroovy Power Features
Groovy Power FeaturesPaul King
 
Make Your Testing Groovy
Make Your Testing GroovyMake Your Testing Groovy
Make Your Testing GroovyPaul King
 
Groovy Testing Sep2009
Groovy Testing Sep2009Groovy Testing Sep2009
Groovy Testing Sep2009Paul King
 
Craig Smith & Paul King Agile Tool Hacking Taking Your Agile Development ...
Craig Smith & Paul King   Agile Tool Hacking   Taking Your Agile Development ...Craig Smith & Paul King   Agile Tool Hacking   Taking Your Agile Development ...
Craig Smith & Paul King Agile Tool Hacking Taking Your Agile Development ...Paul King
 
Groovy Tutorial
Groovy TutorialGroovy Tutorial
Groovy TutorialPaul King
 
XML and Web Services with Groovy
XML and Web Services with GroovyXML and Web Services with Groovy
XML and Web Services with GroovyPaul King
 

Mais de Paul King (11)

groovy databases
groovy databasesgroovy databases
groovy databases
 
tictactoe groovy
tictactoe groovytictactoe groovy
tictactoe groovy
 
Make Testing Groovy
Make Testing GroovyMake Testing Groovy
Make Testing Groovy
 
Agile Testing Practices
Agile Testing PracticesAgile Testing Practices
Agile Testing Practices
 
Dynamic Language Practices
Dynamic Language PracticesDynamic Language Practices
Dynamic Language Practices
 
Groovy Power Features
Groovy Power FeaturesGroovy Power Features
Groovy Power Features
 
Make Your Testing Groovy
Make Your Testing GroovyMake Your Testing Groovy
Make Your Testing Groovy
 
Groovy Testing Sep2009
Groovy Testing Sep2009Groovy Testing Sep2009
Groovy Testing Sep2009
 
Craig Smith & Paul King Agile Tool Hacking Taking Your Agile Development ...
Craig Smith & Paul King   Agile Tool Hacking   Taking Your Agile Development ...Craig Smith & Paul King   Agile Tool Hacking   Taking Your Agile Development ...
Craig Smith & Paul King Agile Tool Hacking Taking Your Agile Development ...
 
Groovy Tutorial
Groovy TutorialGroovy Tutorial
Groovy Tutorial
 
XML and Web Services with Groovy
XML and Web Services with GroovyXML and Web Services with Groovy
XML and Web Services with Groovy
 

Último

How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?XfilesPro
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerThousandEyes
 
Beyond Boundaries: Leveraging No-Code Solutions for Industry Innovation
Beyond Boundaries: Leveraging No-Code Solutions for Industry InnovationBeyond Boundaries: Leveraging No-Code Solutions for Industry Innovation
Beyond Boundaries: Leveraging No-Code Solutions for Industry InnovationSafe Software
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationRidwan Fadjar
 
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationFrom Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationSafe Software
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesSinan KOZAK
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsMemoori
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationMichael W. Hawkins
 
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024BookNet Canada
 
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking MenDelhi Call girls
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Alan Dix
 
Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Paola De la Torre
 
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphSIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphNeo4j
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticscarlostorres15106
 
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure serviceWhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure servicePooja Nehwal
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhisoniya singh
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Patryk Bandurski
 
Presentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreterPresentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreternaman860154
 
08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking Men08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking MenDelhi Call girls
 
Slack Application Development 101 Slides
Slack Application Development 101 SlidesSlack Application Development 101 Slides
Slack Application Development 101 Slidespraypatel2
 

Último (20)

How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?How to Remove Document Management Hurdles with X-Docs?
How to Remove Document Management Hurdles with X-Docs?
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected Worker
 
Beyond Boundaries: Leveraging No-Code Solutions for Industry Innovation
Beyond Boundaries: Leveraging No-Code Solutions for Industry InnovationBeyond Boundaries: Leveraging No-Code Solutions for Industry Innovation
Beyond Boundaries: Leveraging No-Code Solutions for Industry Innovation
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 Presentation
 
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationFrom Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen Frames
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial Buildings
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day Presentation
 
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
 
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
 
Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101
 
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphSIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
 
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure serviceWhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
WhatsApp 9892124323 ✓Call Girls In Kalyan ( Mumbai ) secure service
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
 
Presentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreterPresentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreter
 
08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking Men08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking Men
 
Slack Application Development 101 Slides
Slack Application Development 101 SlidesSlack Application Development 101 Slides
Slack Application Development 101 Slides
 

Atlassian Groovy Plugins

  • 1. 1
  • 2. Groovy Plugins Why you should 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 • Support for Testing DSLs and • Productivity BDD style tests • Clarity • Built-in assert, power asserts • Maintainability • Built-in testing • Quality • Built-in mocks • Fun • Metaprogramming eases testing pain points • Shareability 7
  • 8. 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: World of WarCraft... • http://confluence.atlassian.com/display/CONFDEV/ WoW+Macro+explanation 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
  • 17. ...Plugin Tutorial: World of WarCraft... package com.atlassian.confluence.plugins.wowplugin; ... public String getName() { import java.io.Serializable; return name; import java.util.Arrays; } import java.util.List; public String getSpec() { /** return spec; * Simple data holder for basic toon information } */ public final class Toon implements Comparable, Serializable public int getGearScore() { { return gearScore; private static final String[] CLASSES = { } "Warrior", "Paladin", public List getRecommendedRaids() { "Hunter", return recommendedRaids; "Rogue", } "Priest", "Death Knight", public String getClassName() { "Shaman", return className; "Mage", } "Warlock", "Unknown", // There is no class with ID 10. Weird. public int compareTo(Object o) "Druid" { }; Toon otherToon = (Toon) o; private final String name; if (otherToon.gearScore - gearScore != 0) private final String spec; return otherToon.gearScore - gearScore; private final int gearScore; private final List recommendedRaids; return name.compareTo(otherToon.name); private final String className; } public Toon(String name, int classId, String spec, int gearScore, String... recommendedRaids) private String toClassName(int classIndex) { { this.className = toClassName(classId - 1); if (classIndex < 0 || classIndex >= CLASSES.length) this.name = name; return "Unknown: " + classIndex + 1; this.spec = spec; else this.gearScore = gearScore; return CLASSES[classIndex]; this.recommendedRaids = Arrays.asList(recommendedRaids); } } } ... 17
  • 18. ...Plugin Tutorial: World of WarCraft... package com.atlassian.confluence.plugins.gwowplugin class Toon implements Serializable { private static final String[] CLASSES = [ "Warrior", "Paladin", "Hunter", "Rogue", "Priest", "Death Knight", "Shaman", "Mage", "Warlock", "Unknown", "Druid"] 83 -> 17 String name int classId String spec int gearScore def recommendedRaids String getClassName() { classId in 0..<CLASSES.length ? CLASSES[classId - 1] : "Unknown: " + classId } } 18
  • 19. ...Plugin Tutorial: World of WarCraft... package com.atlassian.confluence.plugins.wowplugin; ... ... public boolean isInline() { return false; } try { import com.atlassian.cache.Cache; url = String.format("http://xml.wow-heroes.com/xml-guild.php?z=%s&r=%s&g=%s", import com.atlassian.cache.CacheManager; public boolean hasBody() { return false; } URLEncoder.encode(zone, "UTF-8"), import com.atlassian.confluence.util.http.HttpResponse; URLEncoder.encode(realmName, "UTF-8"), import com.atlassian.confluence.util.http.HttpRetrievalService; public RenderMode getBodyRenderMode() { URLEncoder.encode(guildName, "UTF-8")); import com.atlassian.renderer.RenderContext; return RenderMode.NO_RENDER; } catch (UnsupportedEncodingException e) { import com.atlassian.renderer.v2.RenderMode; } throw new MacroException(e.getMessage(), e); import com.atlassian.renderer.v2.SubRenderer; } import com.atlassian.renderer.v2.macro.BaseMacro; public String execute(Map map, String s, RenderContext renderContext) throws MacroException { import com.atlassian.renderer.v2.macro.MacroException; String guildName = (String) map.get("guild"); Cache cache = cacheManager.getCache(this.getClass().getName() + ".toons"); import org.dom4j.Document; String realmName = (String) map.get("realm"); import org.dom4j.DocumentException; String zone = (String) map.get("zone"); if (cache.get(url) != null) import org.dom4j.Element; if (zone == null) zone = "us"; return (List<Toon>) cache.get(url); import org.dom4j.io.SAXReader; StringBuilder out = new StringBuilder("||Name||Class||Gear Score"); try { for (int i = 0; i < SHORT_RAIDS.length; i++) { import java.io.IOException; List<Toon> toons = retrieveAndParseFromWowArmory(url); out.append("||").append(SHORT_RAIDS[i].replace('/', 'n')); import java.io.InputStream; cache.put(url, toons); } import java.io.UnsupportedEncodingException; return toons; out.append("||n"); import java.net.URLEncoder; } import java.util.*; List<Toon> toons = retrieveToons(guildName, realmName, zone); catch (IOException e) { throw new MacroException("Unable to retrieve information for guild: " + guildName + ", " + e.toString()); /** for (Toon toon : toons) { } * Inserts a table of a guild's roster of 80s ranked by gear level, with recommended raid instances. The data for catch (DocumentException e) { * the macro is grabbed from http://wow-heroes.com. Results are cached for $DEFAULT_CACHE_LIFETIME to reduce out.append("| "); throw new MacroException("Unable to parse information for guild: " + guildName + ", " + e.toString()); * load on the server. try { } * <p/> } String url = String.format("http://xml.wow-heroes.com/index.php?zone=%s&server=%s&name=%s", * Usage: {guild-gear|realm=Nagrand|guild=A New Beginning|zone=us} URLEncoder.encode(zone, "UTF-8"), * <p/> URLEncoder.encode(realmName, "UTF-8"), private List<Toon> retrieveAndParseFromWowArmory(String url) throws IOException, DocumentException { * Problems: URLEncoder.encode(toon.getName(), "UTF-8")); List<Toon> toons = new ArrayList<Toon>(); * <p/> out.append("["); out.append(toon.getName()); HttpResponse response = httpRetrievalService.get(url); * * wow-heroes reports your main spec, but whatever gear you logged out in. So if you logged out in off-spec gear out.append("|"); out.append(url); out.append("]"); * your number will be wrong } InputStream responseStream = response.getResponse(); * * gear score != ability. l2play nub. catch (UnsupportedEncodingException e) { try { */ out.append(toon.getName()); SAXReader reader = new SAXReader(); public class GuildGearMacro extends BaseMacro { } Document doc = reader.read(responseStream); private HttpRetrievalService httpRetrievalService; List toonsXml = doc.selectNodes("//character"); private SubRenderer subRenderer; out.append(" | "); for (Object o : toonsXml) { private CacheManager cacheManager; out.append(toon.getClassName()); Element element = (Element) o; out.append(" ("); toons.add(new Toon(element.attributeValue("name"), Integer.parseInt(element.attributeValue("classId")), private static final String[] RAIDS = { out.append(toon.getSpec()); element.attributeValue("specName"), "Heroics", out.append(")"); Integer.parseInt(element.attributeValue("score")), element.attributeValue("suggest").split(";"))); "Naxxramas 10", // and OS10 out.append("|"); } "Naxxramas 25", // and OS25/EoE10 out.append(toon.getGearScore()); "Ulduar 10", // and EoE25 boolean found = false; Collections.sort(toons); "Onyxia 10", } "Ulduar 25", // and ToTCr10 for (String raid : RAIDS) { finally { "Onyxia 25", if (toon.getRecommendedRaids().contains(raid)) { responseStream.close(); "Trial of the Crusader 25", out.append("|(!)"); } "Icecrown Citadel 10" found = true; return toons; }; } else { } out.append("|").append(found ? "(x)" : "(/)"); private static final String[] SHORT_RAIDS = { } public void setHttpRetrievalService(HttpRetrievalService httpRetrievalService) { "H", } this.httpRetrievalService = httpRetrievalService; "Naxx10/OS10", out.append("|n"); } "Naxx25/OS25/EoE10", } "Uld10/EoE25", public void setSubRenderer(SubRenderer subRenderer) { "Ony10", return subRenderer.render(out.toString(), renderContext); this.subRenderer = subRenderer; "Uld25/TotCr10", } } "Ony25", private List<Toon> retrieveToons(String guildName, String realmName, String zone) }; "TotCr25", "IC" ... throws MacroException { String url = null; public void setCacheManager(CacheManager cacheManager) { } this.cacheManager = cacheManager; 19 ... }
  • 20. ...Plugin Tutorial: World of WarCraft... package com.atlassian.confluence.plugins.gwowplugin ... toons.each { toon -> import com.atlassian.cache.CacheManager def url = "http://xml.wow-heroes.com/index.php?zone=${enc zone}&server=${enc map.realm}&name=${enc toon.name}" import com.atlassian.confluence.util.http.HttpRetrievalService out.append("| [${toon.name}|${url}] | $toon.className ($toon.spec)| $toon.gearScore") import com.atlassian.renderer.RenderContext boolean found = false import com.atlassian.renderer.v2.RenderMode RAIDS.each { raid -> import com.atlassian.renderer.v2.SubRenderer if (raid in toon.recommendedRaids) { import com.atlassian.renderer.v2.macro.BaseMacro out.append("|(!)") import com.atlassian.renderer.v2.macro.MacroException found = true } else { /** out.append("|").append(found ? "(x)" : "(/)") * Inserts a table of a guild's roster of 80s ranked by gear level, with recommended raid } * instances. The data for the macro is grabbed from http://wow-heroes.com. Results are } 200 -> 90 * cached for $DEFAULT_CACHE_LIFETIME to reduce load on the server. out.append("|n") * <p/> } * Usage: {guild-gear:realm=Nagrand|guild=A New Beginning|zone=us} subRenderer.render(out.toString(), renderContext) */ } class GuildGearMacro extends BaseMacro { HttpRetrievalService httpRetrievalService private retrieveToons(String guildName, String realmName, String zone) throws MacroException { SubRenderer subRenderer def url = "http://xml.wow-heroes.com/xml-guild.php?z=${enc zone}&r=${enc realmName}&g=${enc guildName}" CacheManager cacheManager def cache = cacheManager.getCache(this.class.name + ".toons") if (!cache.get(url)) cache.put(url, retrieveAndParseFromWowArmory(url)) private static final String[] RAIDS = [ return cache.get(url) "Heroics", "Naxxramas 10", "Naxxramas 25", "Ulduar 10", "Onyxia 10", } "Ulduar 25", "Onyxia 25", "Trial of the Crusader 25", "Icecrown Citadel 10"] private static final String[] SHORT_RAIDS = [ private retrieveAndParseFromWowArmory(String url) { "H", "Naxx10/OS10", "Naxx25/OS25/EoE10", "Uld10/EoE25", "Ony10", def toons "Uld25/TotCr10", "Ony25", "TotCr25", "IC"] httpRetrievalService.get(url).response.withReader { reader -> toons = new XmlSlurper().parse(reader).guild.character.collect { boolean isInline() { false } new Toon( boolean hasBody() { false } name: it.@name, RenderMode getBodyRenderMode() { RenderMode.NO_RENDER } classId: it.@classId.toInteger(), spec: it.@specName, String execute(Map map, String s, RenderContext renderContext) throws MacroException { gearScore: it.@score.toInteger(), def zone = map.zone ?: "us" recommendedRaids: it.@suggest.toString().split(";")) def out = new StringBuilder("||Name||Class||Gear Score") } SHORT_RAIDS.each { out.append("||").append(it.replace('/', 'n')) } } out.append("||n") toons.sort{ a, b -> a.gearScore == b.gearScore ? a.name <=> b.name : a.gearScore <=> b.gearScore } } def toons = retrieveToons(map.guild, map.realm, zone) ... def enc(s) { URLEncoder.encode(s, 'UTF-8') } 20 }
  • 21. ...Plugin Tutorial: World of WarCraft... {groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us} 21
  • 22. ...Plugin Tutorial: World of WarCraft... > atlas-mvn clover2:setup test clover2:aggregate clover2:clover 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 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 the fly... Consider also non-coding alternatives to these plugins, e.g.: Supports Groovy and other languages in: http://wiki.angry.com.au/display/WOW/Wow-Heros+User+Macro Conditions, Post-Functions, Validators and Services 24
  • 25. ...Scripting on the fly... 25
  • 27. 27