Top Banner
1
27

Atlassian Groovy Plugins

Jan 14, 2015

Download

Technology

Paul King

Using Groovy to write plugins for Atlassian produces such as Jira and Confluence
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
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