atlassian groovy plugins

27
1

Upload: paul-king

Post on 14-Jan-2015

2.532 views

Category:

Technology


0 download

DESCRIPTION

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

TRANSCRIPT

Page 1: Atlassian Groovy Plugins

1

Page 2: Atlassian Groovy Plugins

Groovy Plugins

Why you should be developing

Atlassian plugins using Groovy

Dr Paul King, Director, ASERT

2

Page 3: Atlassian Groovy Plugins

What is Groovy?

3

“Groovy is like a super version of Java. It

can leverage Java's enterprise capabilities

but also has cool productivity features like

closures, DSL support, builders and dynamic typing.”

Groovy = Java – boiler plate code+ optional dynamic typing+ closures+ domain specific languages+ builders+ meta-programming

Page 4: Atlassian Groovy Plugins

What is Groovy?

4

Now free

Page 5: Atlassian Groovy Plugins

What is Groovy?

5

What alternative JVM language are you using or intending to use

http://www.leonardoborges.com/writings

http://it-republik.de/jaxenter/quickvote/results/1/poll/44

(translated using http://babelfish.yahoo.com)

Source: http://www.micropoll.com/akira/mpresult/501697-116746

http://www.java.net

http://www.jroller.com/scolebourne/entry/devoxx_2008_whiteboard_votes

Source: http://www.grailspodcast.com/

Page 6: Atlassian Groovy Plugins

Reason: Language Features

• Closures

• Runtime metaprogramming

• Compile-time metaprogramming

• Grape modules

• Builders

• DSL friendly

• Productivity

• Clarity

• Maintainability

• Quality

• Fun

• Shareability

6

Page 7: Atlassian Groovy Plugins

Reason: Testing

• Support for Testing DSLs and

BDD style tests

• Built-in assert, power asserts

• Built-in testing

• Built-in mocks

• Metaprogramming eases testing

pain points

7

• Productivity

• Clarity

• Maintainability

• Quality

• Fun

• Shareability

Page 8: Atlassian Groovy Plugins

Myth: Dynamic typing == No IDE support

• Completion through inference

• Code analysis

• Seamless debugging

• Seamless refactoring

• DSL completion

8

Page 9: Atlassian Groovy Plugins

Myth: Scripting == Non-professional

• Analysis tools

• Coverage tools

• Testing support

9

Page 10: Atlassian Groovy Plugins

Java

10

import java.util.List;import java.util.ArrayList;

class Erase {private List removeLongerThan(List strings, int length) {

List result = new ArrayList();for (int i = 0; i < strings.size(); i++) {

String s = (String) strings.get(i);if (s.length() <= length) {

result.add(s);}

}return result;

}public static void main(String[] args) {

List names = new ArrayList();names.add("Ted"); names.add("Fred");names.add("Jed"); names.add("Ned");System.out.println(names);Erase e = new Erase();List shortNames = e.removeLongerThan(names, 3);System.out.println(shortNames.size());for (int i = 0; i < shortNames.size(); i++) {

String s = (String) shortNames.get(i);System.out.println(s);

}}

}

names = ["Ted", "Fred", "Jed", "Ned"]println namesshortNames = names.findAll{ it.size() <= 3 }println shortNames.size()shortNames.each{ println it }

Groovy

Page 11: Atlassian Groovy Plugins

Java

11

Groovyimport org.w3c.dom.Document;import org.w3c.dom.NodeList;import org.w3c.dom.Node;import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.ParserConfigurationException;import java.io.File;import java.io.IOException;

public class FindYearsJava {public static void main(String[] args) {

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();try {

DocumentBuilder builder = builderFactory.newDocumentBuilder();Document document = builder.parse(new File("records.xml"));NodeList list = document.getElementsByTagName("car");for (int i = 0; i < list.getLength(); i++) {

Node n = list.item(i);Node year = n.getAttributes().getNamedItem("year");System.out.println("year = " + year.getTextContent());

}} catch (ParserConfigurationException e) {

e.printStackTrace();} catch (SAXException e) {

e.printStackTrace();} catch (IOException e) {

e.printStackTrace();}

}}

def p = new XmlParser()def records = p.parse("records.xml")records.car.each {

println "year = ${it.@year}"}

Page 12: Atlassian Groovy Plugins

Java

12

Groovy

@Immutable class Punter {String first, last

}

public final class Punter {private final String first;private final String last;

public String getFirst() {return first;

}

public String getLast() {return last;

}

@Overridepublic int hashCode() {

final int prime = 31;int result = 1;result = prime * result + ((first == null)

? 0 : first.hashCode());result = prime * result + ((last == null)

? 0 : last.hashCode());return result;

}

public Punter(String first, String last) {this.first = first;this.last = last;

}// ...

// ...@Overridepublic boolean equals(Object obj) {

if (this == obj)return true;

if (obj == null)return false;

if (getClass() != obj.getClass())return false;

Punter other = (Punter) obj;if (first == null) {

if (other.first != null)return false;

} else if (!first.equals(other.first))return false;

if (last == null) {if (other.last != null)

return false;} else if (!last.equals(other.last))

return false;return true;

}

@Overridepublic String toString() {

return "Punter(first:" + first+ ", last:" + last + ")";

}

}

Page 13: Atlassian Groovy Plugins

Java

13

Groovy

@InheritConstructors

class CustomException

extends RuntimeException { }

public class CustomException extends RuntimeException {

public CustomException() {

super();

}

public CustomException(String message) {

super(message);

}

public CustomException(String message, Throwable cause) {

super(message, cause);

}

public CustomException(Throwable cause) {

super(cause);

}

}

Page 14: Atlassian Groovy Plugins

14

@Grab('org.gcontracts:gcontracts:1.1.1')import org.gcontracts.annotations.*

@Invariant({ first != null && last != null })class Person {

String first, last

@Requires({ delimiter in ['.', ',', ' '] })@Ensures({ result == first+delimiter+last })String getName(String delimiter) {

first + delimiter + last}

}

new Person(first: 'John', last: 'Smith').getName('.')

Groovy

@Grab('org.codehaus.gpars:gpars:0.10')import groovyx.gpars.agent.Agent

withPool(5) {def nums = 1..100000println nums.parallel.

map{ it ** 2 }.filter{ it % 7 == it % 5 }.filter{ it % 3 == 0 }.reduce{ a, b -> a + b }

}

@Grab('com.google.collections:google-collections:1.0')import com.google.common.collect.HashBiMap

HashBiMap fruit =[grape:'purple', lemon:'yellow', lime:'green']

assert fruit.lemon == 'yellow'assert fruit.inverse().yellow == 'lemon'

Groovy and Gpars both OSGi compliant

Groovy 1.8+

Page 15: Atlassian Groovy Plugins

Plugin Tutorial: World of WarCraft...

• http://confluence.atlassian.com/display/CONFDEV/

WoW+Macro+explanation

15

Page 16: Atlassian Groovy Plugins

• Normal instructions for gmaven:

http://gmaven.codehaus.org/

16

...Plugin Tutorial: World of WarCraft...

...

<plugin><groupId>org.codehaus.gmaven</groupId><artifactId>gmaven-plugin</artifactId><version>1.2</version><configuration>...</configuration><executions>...</executions><dependencies>...</dependencies>

</plugin>...

Page 17: Atlassian Groovy Plugins

17

...Plugin Tutorial: World of WarCraft...package com.atlassian.confluence.plugins.wowplugin;

import java.io.Serializable;import java.util.Arrays;import java.util.List;

/*** Simple data holder for basic toon information*/public final class Toon implements Comparable, Serializable{

private static final String[] CLASSES = {"Warrior","Paladin","Hunter","Rogue","Priest","Death Knight","Shaman","Mage","Warlock","Unknown", // There is no class with ID 10. Weird."Druid"

};

private final String name;private final String spec;private final int gearScore;private final List recommendedRaids;private final String className;

public Toon(String name, int classId, String spec, int gearScore, String... recommendedRaids){

this.className = toClassName(classId - 1);this.name = name;this.spec = spec;this.gearScore = gearScore;this.recommendedRaids = Arrays.asList(recommendedRaids);

}...

...public String getName() {

return name;}

public String getSpec() {return spec;

}

public int getGearScore() {return gearScore;

}

public List getRecommendedRaids() {return recommendedRaids;

}

public String getClassName() {return className;

}

public int compareTo(Object o){

Toon otherToon = (Toon) o;

if (otherToon.gearScore - gearScore != 0)return otherToon.gearScore - gearScore;

return name.compareTo(otherToon.name);}

private String toClassName(int classIndex){

if (classIndex < 0 || classIndex >= CLASSES.length)return "Unknown: " + classIndex + 1;

elsereturn CLASSES[classIndex];

}}

Page 18: Atlassian Groovy Plugins

18

...Plugin Tutorial: World of WarCraft...

package com.atlassian.confluence.plugins.gwowplugin

class Toon implements Serializable {private static final String[] CLASSES = [

"Warrior", "Paladin", "Hunter", "Rogue", "Priest","Death Knight", "Shaman", "Mage", "Warlock", "Unknown", "Druid"]

String nameint classIdString specint gearScoredef recommendedRaids

String getClassName() {classId in 0..<CLASSES.length ? CLASSES[classId - 1] : "Unknown: " + classId

}}

83 -> 17

Page 19: Atlassian Groovy Plugins

19

...Plugin Tutorial: World of WarCraft...package com.atlassian.confluence.plugins.wowplugin;

import com.atlassian.cache.Cache;

import com.atlassian.cache.CacheManager;

import com.atlassian.confluence.util.http.HttpResponse;

import com.atlassian.confluence.util.http.HttpRetrievalService;

import com.atlassian.renderer.RenderContext;

import com.atlassian.renderer.v2.RenderMode;

import com.atlassian.renderer.v2.SubRenderer;

import com.atlassian.renderer.v2.macro.BaseMacro;

import com.atlassian.renderer.v2.macro.MacroException;

import org.dom4j.Document;

import org.dom4j.DocumentException;

import org.dom4j.Element;

import org.dom4j.io.SAXReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.UnsupportedEncodingException;

import java.net.URLEncoder;

import java.util.*;

/**

* Inserts a table of a guild's roster of 80s ranked by gear level, with recommended raid instances. The data for

* the macro is grabbed from http://wow-heroes.com. Results are cached for $DEFAULT_CACHE_LIFETIME to reduce

* load on the server.

* <p/>

* Usage: {guild-gear|realm=Nagrand|guild=A New Beginning|zone=us}

* <p/>

* Problems:

* <p/>

* * wow-heroes reports your main spec, but whatever gear you logged out in. So if you logged out in off-spec gear

* your number will be wrong

* * gear score != ability. l2play nub.

*/

public class GuildGearMacro extends BaseMacro {

private HttpRetrievalService httpRetrievalService;

private SubRenderer subRenderer;

private CacheManager cacheManager;

private static final String[] RAIDS = {

"Heroics",

"Naxxramas 10", // and OS10

"Naxxramas 25", // and OS25/EoE10

"Ulduar 10", // and EoE25

"Onyxia 10",

"Ulduar 25", // and ToTCr10

"Onyxia 25",

"Trial of the Crusader 25",

"Icecrown Citadel 10"

};

private static final String[] SHORT_RAIDS = {

"H",

"Naxx10/OS10",

"Naxx25/OS25/EoE10",

"Uld10/EoE25",

"Ony10",

"Uld25/TotCr10",

"Ony25",

"TotCr25",

"IC"

};

...

...

public boolean isInline() { return false; }

public boolean hasBody() { return false; }

public RenderMode getBodyRenderMode() {

return RenderMode.NO_RENDER;

}

public String execute(Map map, String s, RenderContext renderContext) throws MacroException {

String guildName = (String) map.get("guild");

String realmName = (String) map.get("realm");

String zone = (String) map.get("zone");

if (zone == null) zone = "us";

StringBuilder out = new StringBuilder("||Name||Class||Gear Score");

for (int i = 0; i < SHORT_RAIDS.length; i++) {

out.append("||").append(SHORT_RAIDS[i].replace('/', '\n'));

}

out.append("||\n");

List<Toon> toons = retrieveToons(guildName, realmName, zone);

for (Toon toon : toons) {

out.append("| ");

try {

String url = String.format("http://xml.wow-heroes.com/index.php?zone=%s&server=%s&name=%s",

URLEncoder.encode(zone, "UTF-8"),

URLEncoder.encode(realmName, "UTF-8"),

URLEncoder.encode(toon.getName(), "UTF-8"));

out.append("["); out.append(toon.getName());

out.append("|"); out.append(url); out.append("]");

}

catch (UnsupportedEncodingException e) {

out.append(toon.getName());

}

out.append(" | ");

out.append(toon.getClassName());

out.append(" (");

out.append(toon.getSpec());

out.append(")");

out.append("|");

out.append(toon.getGearScore());

boolean found = false;

for (String raid : RAIDS) {

if (toon.getRecommendedRaids().contains(raid)) {

out.append("|(!)");

found = true;

} else {

out.append("|").append(found ? "(x)" : "(/)");

}

}

out.append("|\n");

}

return subRenderer.render(out.toString(), renderContext);

}

private List<Toon> retrieveToons(String guildName, String realmName, String zone)

throws MacroException {

String url = null;

...

...

try {

url = String.format("http://xml.wow-heroes.com/xml-guild.php?z=%s&r=%s&g=%s",

URLEncoder.encode(zone, "UTF-8"),

URLEncoder.encode(realmName, "UTF-8"),

URLEncoder.encode(guildName, "UTF-8"));

} catch (UnsupportedEncodingException e) {

throw new MacroException(e.getMessage(), e);

}

Cache cache = cacheManager.getCache(this.getClass().getName() + ".toons");

if (cache.get(url) != null)

return (List<Toon>) cache.get(url);

try {

List<Toon> toons = retrieveAndParseFromWowArmory(url);

cache.put(url, toons);

return toons;

}

catch (IOException e) {

throw new MacroException("Unable to retrieve information for guild: " + guildName + ", " + e.toString());

}

catch (DocumentException e) {

throw new MacroException("Unable to parse information for guild: " + guildName + ", " + e.toString());

}

}

private List<Toon> retrieveAndParseFromWowArmory(String url) throws IOException, DocumentException {

List<Toon> toons = new ArrayList<Toon>();

HttpResponse response = httpRetrievalService.get(url);

InputStream responseStream = response.getResponse();

try {

SAXReader reader = new SAXReader();

Document doc = reader.read(responseStream);

List toonsXml = doc.selectNodes("//character");

for (Object o : toonsXml) {

Element element = (Element) o;

toons.add(new Toon(element.attributeValue("name"), Integer.parseInt(element.attributeValue("classId")),

element.attributeValue("specName"),

Integer.parseInt(element.attributeValue("score")), element.attributeValue("suggest").split(";")));

}

Collections.sort(toons);

}

finally {

responseStream.close();

}

return toons;

}

public void setHttpRetrievalService(HttpRetrievalService httpRetrievalService) {

this.httpRetrievalService = httpRetrievalService;

}

public void setSubRenderer(SubRenderer subRenderer) {

this.subRenderer = subRenderer;

}

public void setCacheManager(CacheManager cacheManager) {

this.cacheManager = cacheManager;

}

}

Page 20: Atlassian Groovy Plugins

20

...Plugin Tutorial: World of WarCraft...package com.atlassian.confluence.plugins.gwowplugin

import com.atlassian.cache.CacheManagerimport com.atlassian.confluence.util.http.HttpRetrievalServiceimport com.atlassian.renderer.RenderContextimport com.atlassian.renderer.v2.RenderModeimport com.atlassian.renderer.v2.SubRendererimport com.atlassian.renderer.v2.macro.BaseMacroimport com.atlassian.renderer.v2.macro.MacroException

/*** Inserts a table of a guild's roster of 80s ranked by gear level, with recommended raid* instances. The data for the macro is grabbed from http://wow-heroes.com. Results are* cached for $DEFAULT_CACHE_LIFETIME to reduce load on the server.* <p/>* Usage: {guild-gear:realm=Nagrand|guild=A New Beginning|zone=us}*/class GuildGearMacro extends BaseMacro {HttpRetrievalService httpRetrievalServiceSubRenderer subRendererCacheManager cacheManager

private static final String[] RAIDS = ["Heroics", "Naxxramas 10", "Naxxramas 25", "Ulduar 10", "Onyxia 10","Ulduar 25", "Onyxia 25", "Trial of the Crusader 25", "Icecrown Citadel 10"]

private static final String[] SHORT_RAIDS = ["H", "Naxx10/OS10", "Naxx25/OS25/EoE10", "Uld10/EoE25", "Ony10","Uld25/TotCr10", "Ony25", "TotCr25", "IC"]

boolean isInline() { false }boolean hasBody() { false }RenderMode getBodyRenderMode() { RenderMode.NO_RENDER }

String execute(Map map, String s, RenderContext renderContext) throws MacroException {def zone = map.zone ?: "us"def out = new StringBuilder("||Name||Class||Gear Score")SHORT_RAIDS.each { out.append("||").append(it.replace('/', '\n')) }out.append("||\n")

def toons = retrieveToons(map.guild, map.realm, zone)...

...toons.each { toon ->

def url = "http://xml.wow-heroes.com/index.php?zone=${enc zone}&server=${enc map.realm}&name=${enc toon.name}"out.append("| [${toon.name}|${url}] | $toon.className ($toon.spec)| $toon.gearScore")boolean found = falseRAIDS.each { raid ->

if (raid in toon.recommendedRaids) {out.append("|(!)")found = true

} else {out.append("|").append(found ? "(x)" : "(/)")

}}out.append("|\n")

}subRenderer.render(out.toString(), renderContext)

}

private retrieveToons(String guildName, String realmName, String zone) throws MacroException {def url = "http://xml.wow-heroes.com/xml-guild.php?z=${enc zone}&r=${enc realmName}&g=${enc guildName}"def cache = cacheManager.getCache(this.class.name + ".toons")if (!cache.get(url)) cache.put(url, retrieveAndParseFromWowArmory(url))return cache.get(url)

}

private retrieveAndParseFromWowArmory(String url) {def toonshttpRetrievalService.get(url).response.withReader { reader ->

toons = new XmlSlurper().parse(reader).guild.character.collect {new Toon(

name: it.@name,classId: [email protected](),spec: it.@specName,gearScore: [email protected](),recommendedRaids: [email protected]().split(";"))

}}toons.sort{ a, b -> a.gearScore == b.gearScore ? a.name <=> b.name : a.gearScore <=> b.gearScore }

}

def enc(s) { URLEncoder.encode(s, 'UTF-8') }}

200 -> 90

Page 21: Atlassian Groovy Plugins

21

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

Page 22: Atlassian Groovy Plugins

22

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

Page 23: Atlassian Groovy Plugins

• Testing with Spock • Or Cucumber, EasyB, JBehave,

Robot Framework, JUnit, TestNg23

...Plugin Tutorial: World of WarCraft

package com.atlassian.confluence.plugins.gwowplugin

class ToonSpec extends spock.lang.Specification {def "successful name of Toon given classId"() {

given:def t = new Toon(classId: thisClassId)

expect:t.className == name

where:name | thisClassId"Hunter" | 3"Rogue" | 4"Priest" | 5

}}

narrative 'segment flown', {as_a 'frequent flyer'i_want 'to accrue rewards points for every segment I fly'so_that 'I can receive free flights for my dedication to the airline'

}

scenario 'segment flown', {given 'a frequent flyer with a rewards balance of 1500 points'when 'that flyer completes a segment worth 500 points'then 'that flyer has a new rewards balance of 2000 points'

}

scenario 'segment flown', {given 'a frequent flyer with a rewards balance of 1500 points', {

flyer = new FrequentFlyer(1500)}when 'that flyer completes a segment worth 500 points', {

flyer.fly(new Segment(500))}then 'that flyer has a new rewards balance of 2000 points', {

flyer.pointsBalance.shouldBe 2000}

}

Page 24: Atlassian Groovy Plugins

24

Scripting on the fly...

Consider also non-coding alternatives to these plugins, e.g.:

http://wiki.angry.com.au/display/WOW/Wow-Heros+User+Macro

Supports Groovy and other languages in:

Conditions, Post-Functions, Validators and Services

Page 25: Atlassian Groovy Plugins

25

...Scripting on the fly...

Page 26: Atlassian Groovy Plugins

26

...Scripting on the fly

Page 27: Atlassian Groovy Plugins

27