1
1
Groovy Plugins
Why you should be developing
Atlassian plugins using Groovy

Dr Paul King, Director, ASERT




                                 2
                                     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
                                                                                                 3
What is Groovy?




 Now free




                  4
                  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
                                                                                                                                                                                                                     5
Reason: Language Features
• Closures
                                 • Productivity
• Runtime metaprogramming
                                 • Clarity
• Compile-time metaprogramming
                                 • Maintainability
• Grape modules
                                 • Quality
• Builders
                                 • Fun
• DSL friendly



                                                     6
                                                         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
                                                           7
Myth: Dynamic typing == No IDE support
• Completion through inference
• Code analysis
• Seamless debugging
• Seamless refactoring
• DSL completion




                                          8
                                              8
Myth: Scripting == Non-professional
• Analysis tools
• Coverage tools
• Testing support




                                       9
                                           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)	
  {
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  result.add(s);
                                                                                                           names	
  =	
  ["Ted",	
  "Fred",	
  "Jed",	
  "Ned"]
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  }                                                          println	
  names
	
  	
  	
  	
  	
  	
  	
  	
  }
	
  	
  	
  	
  	
  	
  	
  	
  return	
  result;                                                          shortNames	
  =	
  names.findAll{	
  it.size()	
  <=	
  3	
  }
	
  	
  	
  	
  }
	
  	
  	
  	
  public	
  static	
  void	
  main(String[]	
  args)	
  {                                    println	
  shortNames.size()
	
  	
  	
  	
  	
  	
  	
  	
  List	
  names	
  =	
  new	
  ArrayList();
	
  	
  	
  	
  	
  	
  	
  	
  names.add("Ted");	
  names.add("Fred");
                                                                                                           shortNames.each{	
  println	
  it	
  }
	
  	
  	
  	
  	
  	
  	
  	
  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
                                                                                                                                                                             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)	
  {
	
  	
  	
  	
  	
  	
  	
  	
  DocumentBuilderFactory	
  builderFactory	
  =	
  DocumentBuilderFactory.newInstance();
                                                                                                                                     records.car.each	
  {
	
  	
  	
  	
  	
  	
  	
  	
  try	
  {
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  DocumentBuilder	
  builder	
  =	
  builderFactory.newDocumentBuilder();
                                                                                                                                     	
  	
  	
  	
  println	
  "year	
  =	
  ${it.@year}"
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  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
                                                                                                                                                                                              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
                                                                                                                                                                                                                                               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
                                                                                                13
Groovy
@Grab('com.google.collections:google-­‐collections:1.0')
import	
  com.google.common.collect.HashBiMap                                                   @Grab('org.gcontracts:gcontracts:1.0.2')                           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

                                                                                                	
  	
  	
  @Requires({	
  delimiter	
  in	
  ['.',	
  ',',	
  '	
  ']	
  })
          @Grab('org.codehaus.gpars:gpars:0.10')                                                	
  	
  	
  @Ensures({	
  result	
  ==	
  first+delimiter+last	
  })
          import	
  groovyx.gpars.agent.Agent                                                   	
  	
  	
  String	
  getName(String	
  delimiter)	
  {
                                                                                                	
  	
  	
  	
  	
  	
  first	
  +	
  delimiter	
  +	
  last
          withPool(5)	
  {
                                                                                                	
  	
  	
  }
          	
  	
  	
  	
  def	
  nums	
  =	
  1..100000
                                                                                                }
          	
  	
  	
  	
  println	
  nums.parallel.
          	
  	
  	
  	
  	
  	
  	
  	
  map{	
  it	
  **	
  2	
  }.
                                                                                                new	
  Person(first:	
  'John',	
  last:	
  'Smith').getName('.')
          	
  	
  	
  	
  	
  	
  	
  	
  filter{	
  it	
  %	
  7	
  ==	
  it	
  %	
  5	
  }.
          	
  	
  	
  	
  	
  	
  	
  	
  filter{	
  it	
  %	
  3	
  ==	
  0	
  }.
          	
  	
  	
  	
  	
  	
  	
  	
  reduce{	
  a,	
  b	
  -­‐>	
  a	
  +	
  b	
  }
          }
                                           Groovy and Gpars both OSGi compliant
                                                                                                                                                                               14
                                                                                                                                                                                14
Plugin Tutorial: World of WarCraft...
• http://confluence.atlassian.com/display/CONFDEV/
  WoW+Macro+explanation




                                                    15
                                                     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
                                                                           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
                                                                                                                                                                                                                                                          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
                                                                                                                                                             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.*;                                                                                                                                                  catch (IOException e) {
                                                                              List<Toon> toons = retrieveToons(guildName, realmName, zone);
                                                                                                                                                                           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 out.append("|"); out.append(url); out.append("]");
                                                                                        logged out in off-spec gear
 * 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",
             "TotCr25",                                                   private List<Toon> retrieveToons(String guildName, String realmName, String zone)        public void setCacheManager(CacheManager cacheManager) {
             "IC"                                                                 throws MacroException {                                                              this.cacheManager = cacheManager;
    };                                                                        String url = null;                                                                   }
    ...                                                               ...                                                                                      }

                                                                                                                                                                                                                                                                                 19
                                                                                                                                                                                                                                                                                       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
                                                                                                                                                                                                                                                                                                                     20
...Plugin Tutorial: World of WarCraft...
  {groovy-wow-item:1624}   {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us}




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




                                                                  22
                                                                   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    	
  	
  	
  	
  	
  }
	
  	
  	
  	
  	
  	
  	
  	
  "Hunter"	
  	
  	
  |	
  	
  3                          	
  	
  	
  	
  	
  when	
  'that	
  flyer	
  completes	
  a	
  segment	
  worth	
  500	
  points',	
  {
	
  	
  	
  	
  	
  	
  	
  	
  "Rogue"	
  	
  	
  	
  |	
  	
  4                       	
  	
  	
  	
  	
  	
  	
  	
  	
  flyer.fly(new	
  Segment(500))
                                                                                        	
  	
  	
  	
  	
  }
	
  	
  	
  	
  	
  	
  	
  	
  "Priest"	
  	
  	
  |	
  	
  5                          	
  	
  	
  	
  	
  then	
  'that	
  flyer	
  has	
  a	
  new	
  rewards	
  balance	
  of	
  2000	
  points',	
  {
                                                                                        	
  	
  	
  	
  	
  	
  	
  	
  	
  flyer.pointsBalance.shouldBe	
  2000
	
  	
  	
  	
  }                                                                       	
  	
  	
  	
  	
  }
}•                                                                                      	
  }



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

                                                                                                                                                                                                                 23
                                                                                                                                                                                                                  23
Scripting on the fly...




                         24
                          24
...Scripting on the fly...




                            25
                             25
...Scripting on the fly




                         26
                          26
27
 27

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

  • 1.
  • 2.
    Groovy Plugins Why youshould be developing Atlassian plugins using Groovy Dr Paul King, Director, ASERT 2 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 3
  • 4.
    What is Groovy? Now free 4 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 5
  • 6.
    Reason: Language Features •Closures • Productivity • Runtime metaprogramming • Clarity • Compile-time metaprogramming • Maintainability • Grape modules • Quality • Builders • Fun • DSL friendly 6 6
  • 7.
    Reason: Testing • Supportfor Testing DSLs and • Productivity BDD style tests • Clarity • Built-in assert, power asserts • Maintainability • Built-in testing • Quality • Built-in mocks • Fun • Metaprogramming eases testing pain points • Shareability 7 7
  • 8.
    Myth: Dynamic typing== No IDE support • Completion through inference • Code analysis • Seamless debugging • Seamless refactoring • DSL completion 8 8
  • 9.
    Myth: Scripting ==Non-professional • Analysis tools • Coverage tools • Testing support 9 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)  {                                result.add(s); names  =  ["Ted",  "Fred",  "Jed",  "Ned"]                        } println  names                }                return  result; shortNames  =  names.findAll{  it.size()  <=  3  }        }        public  static  void  main(String[]  args)  { println  shortNames.size()                List  names  =  new  ArrayList();                names.add("Ted");  names.add("Fred"); shortNames.each{  println  it  }                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 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)  {                DocumentBuilderFactory  builderFactory  =  DocumentBuilderFactory.newInstance(); records.car.each  {                try  {                        DocumentBuilder  builder  =  builderFactory.newDocumentBuilder();        println  "year  =  ${it.@year}"                        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 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 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 13
  • 14.
    Groovy @Grab('com.google.collections:google-­‐collections:1.0') import  com.google.common.collect.HashBiMap @Grab('org.gcontracts:gcontracts:1.0.2') 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      @Requires({  delimiter  in  ['.',  ',',  '  ']  }) @Grab('org.codehaus.gpars:gpars:0.10')      @Ensures({  result  ==  first+delimiter+last  }) import  groovyx.gpars.agent.Agent      String  getName(String  delimiter)  {            first  +  delimiter  +  last withPool(5)  {      }        def  nums  =  1..100000 }        println  nums.parallel.                map{  it  **  2  }. new  Person(first:  'John',  last:  'Smith').getName('.')                filter{  it  %  7  ==  it  %  5  }.                filter{  it  %  3  ==  0  }.                reduce{  a,  b  -­‐>  a  +  b  } } Groovy and Gpars both OSGi compliant 14 14
  • 15.
    Plugin Tutorial: Worldof WarCraft... • http://confluence.atlassian.com/display/CONFDEV/ WoW+Macro+explanation 15 15
  • 16.
    ...Plugin Tutorial: Worldof WarCraft... • Normal instructions for gmaven: http://gmaven.codehaus.org/    ...    <plugin>          <groupId>org.codehaus.gmaven</groupId>          <artifactId>gmaven-­‐plugin</artifactId>          <version>1.2</version>          <configuration>...</configuration>          <executions>...</executions>          <dependencies>...</dependencies>    </plugin>    ... 16 16
  • 17.
    ...Plugin Tutorial: Worldof WarCraft... package  com.atlassian.confluence.plugins.wowplugin; ...        public  String  getName()  { import  java.io.Serializable;                return  name; import  java.util.Arrays;        } import  java.util.List;        public  String  getSpec()  { /**                return  spec; *  Simple  data  holder  for  basic  toon  information        } */ public  final  class  Toon  implements  Comparable,  Serializable        public  int  getGearScore()  { {                return  gearScore;        private  static  final  String[]  CLASSES  =  {        }                        "Warrior",                        "Paladin",        public  List  getRecommendedRaids()  {                        "Hunter",                return  recommendedRaids;                        "Rogue",        }                        "Priest",                        "Death  Knight",        public  String  getClassName()  {                        "Shaman",                return  className;                        "Mage",        }                        "Warlock",                        "Unknown",  //  There  is  no  class  with  ID  10.  Weird.        public  int  compareTo(Object  o)                        "Druid"        {        };                Toon  otherToon  =  (Toon)  o;        private  final  String  name;                if  (otherToon.gearScore  -­‐  gearScore  !=  0)        private  final  String  spec;                        return  otherToon.gearScore  -­‐  gearScore;        private  final  int  gearScore;        private  final  List  recommendedRaids;                return  name.compareTo(otherToon.name);        private  final  String  className;        }        public  Toon(String  name,  int  classId,  String  spec,  int  gearScore,  String...  recommendedRaids)        private  String  toClassName(int  classIndex)        {        {                this.className  =  toClassName(classId  -­‐  1);                if  (classIndex  <  0  ||  classIndex  >=  CLASSES.length)                this.name  =  name;                        return  "Unknown:  "  +  classIndex  +  1;                this.spec  =  spec;                else                this.gearScore  =  gearScore;                        return  CLASSES[classIndex];                this.recommendedRaids  =  Arrays.asList(recommendedRaids);        }        } } ... 17 17
  • 18.
    ...Plugin Tutorial: Worldof WarCraft... package  com.atlassian.confluence.plugins.gwowplugin class  Toon  implements  Serializable  {        private  static  final  String[]  CLASSES  =  [                "Warrior",  "Paladin",  "Hunter",  "Rogue",  "Priest",                "Death  Knight",  "Shaman",  "Mage",  "Warlock",  "Unknown",  "Druid"] 83 -> 17        String  name        int  classId        String  spec        int  gearScore        def  recommendedRaids        String  getClassName()  {                classId  in  0..<CLASSES.length  ?  CLASSES[classId  -­‐  1]  :  "Unknown:  "  +  classId        } } 18 18
  • 19.
    ...Plugin Tutorial: Worldof WarCraft... package com.atlassian.confluence.plugins.wowplugin; ... ... public boolean isInline() { return false; } try { import com.atlassian.cache.Cache; url = String.format("http://xml.wow-heroes.com/xml-guild.php?z=%s&r=%s&g=%s", import com.atlassian.cache.CacheManager; public boolean hasBody() { return false; } URLEncoder.encode(zone, "UTF-8"), import com.atlassian.confluence.util.http.HttpResponse; URLEncoder.encode(realmName, "UTF-8"), import com.atlassian.confluence.util.http.HttpRetrievalService; public RenderMode getBodyRenderMode() { URLEncoder.encode(guildName, "UTF-8")); import com.atlassian.renderer.RenderContext; return RenderMode.NO_RENDER; } catch (UnsupportedEncodingException e) { import com.atlassian.renderer.v2.RenderMode; } throw new MacroException(e.getMessage(), e); import com.atlassian.renderer.v2.SubRenderer; } import com.atlassian.renderer.v2.macro.BaseMacro; public String execute(Map map, String s, RenderContext renderContext) throws MacroException { import com.atlassian.renderer.v2.macro.MacroException; String guildName = (String) map.get("guild"); Cache cache = cacheManager.getCache(this.getClass().getName() + ".toons"); import org.dom4j.Document; String realmName = (String) map.get("realm"); import org.dom4j.DocumentException; String zone = (String) map.get("zone"); if (cache.get(url) != null) import org.dom4j.Element; if (zone == null) zone = "us"; return (List<Toon>) cache.get(url); import org.dom4j.io.SAXReader; StringBuilder out = new StringBuilder("||Name||Class||Gear Score"); try { for (int i = 0; i < SHORT_RAIDS.length; i++) { import java.io.IOException; List<Toon> toons = retrieveAndParseFromWowArmory(url); out.append("||").append(SHORT_RAIDS[i].replace('/', 'n')); import java.io.InputStream; cache.put(url, toons); } import java.io.UnsupportedEncodingException; return toons; out.append("||n"); import java.net.URLEncoder; } import java.util.*; catch (IOException e) { List<Toon> toons = retrieveToons(guildName, realmName, zone); 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 out.append("|"); out.append(url); out.append("]"); logged out in off-spec gear * 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", "TotCr25", private List<Toon> retrieveToons(String guildName, String realmName, String zone) public void setCacheManager(CacheManager cacheManager) { "IC" throws MacroException { this.cacheManager = cacheManager; }; String url = null; } ... ... } 19 19
  • 20.
    ...Plugin Tutorial: Worldof WarCraft... package  com.atlassian.confluence.plugins.gwowplugin ...        toons.each  {  toon  -­‐> import  com.atlassian.cache.CacheManager            def  url  =  "http://xml.wow-­‐heroes.com/index.php?zone=${enc  zone}&server=${enc  map.realm}&name=${enc  toon.name}" import  com.atlassian.confluence.util.http.HttpRetrievalService            out.append("|  [${toon.name}|${url}]  |  $toon.className  ($toon.spec)|  $toon.gearScore") import  com.atlassian.renderer.RenderContext            boolean  found  =  false import  com.atlassian.renderer.v2.RenderMode            RAIDS.each  {  raid  -­‐> import  com.atlassian.renderer.v2.SubRenderer                if  (raid  in  toon.recommendedRaids)  { import  com.atlassian.renderer.v2.macro.BaseMacro                    out.append("|(!)") import  com.atlassian.renderer.v2.macro.MacroException                    found  =  true                }  else  { /**                    out.append("|").append(found  ?  "(x)"  :  "(/)")  *  Inserts  a  table  of  a  guild's  roster  of  80s  ranked  by  gear  level,  with  recommended  raid                }  *  instances.  The  data  for  the  macro  is  grabbed  from  http://wow-­‐heroes.com.  Results  are            } 200 -> 90  *  cached  for  $DEFAULT_CACHE_LIFETIME  to  reduce  load  on  the  server.            out.append("|n")  *  <p/>        }  *  Usage:  {guild-­‐gear:realm=Nagrand|guild=A  New  Beginning|zone=us}        subRenderer.render(out.toString(),  renderContext)  */    } class  GuildGearMacro  extends  BaseMacro  {    HttpRetrievalService  httpRetrievalService    private  retrieveToons(String  guildName,  String  realmName,  String  zone)  throws  MacroException  {    SubRenderer  subRenderer        def  url  =  "http://xml.wow-­‐heroes.com/xml-­‐guild.php?z=${enc  zone}&r=${enc  realmName}&g=${enc  guildName}"    CacheManager  cacheManager        def  cache  =  cacheManager.getCache(this.class.name  +  ".toons")        if  (!cache.get(url))  cache.put(url,  retrieveAndParseFromWowArmory(url))    private  static  final  String[]  RAIDS  =  [        return  cache.get(url)                "Heroics",  "Naxxramas  10",  "Naxxramas  25",  "Ulduar  10",  "Onyxia  10",    }                "Ulduar  25",  "Onyxia  25",  "Trial  of  the  Crusader  25",  "Icecrown  Citadel  10"]    private  static  final  String[]  SHORT_RAIDS  =  [    private  retrieveAndParseFromWowArmory(String  url)  {                "H",  "Naxx10/OS10",  "Naxx25/OS25/EoE10",  "Uld10/EoE25",  "Ony10",        def  toons                "Uld25/TotCr10",  "Ony25",  "TotCr25",  "IC"]        httpRetrievalService.get(url).response.withReader  {  reader  -­‐>            toons  =  new  XmlSlurper().parse(reader).guild.character.collect  {    boolean  isInline()  {  false  }                new  Toon(    boolean  hasBody()  {  false  }                    name:                          it.@name,    RenderMode  getBodyRenderMode()  {  RenderMode.NO_RENDER  }                    classId:                    it.@classId.toInteger(),                    spec:                          it.@specName,    String  execute(Map  map,  String  s,  RenderContext  renderContext)  throws  MacroException  {                    gearScore:                it.@score.toInteger(),        def  zone  =  map.zone  ?:  "us"                    recommendedRaids:  it.@suggest.toString().split(";"))        def  out  =  new  StringBuilder("||Name||Class||Gear  Score")            }        SHORT_RAIDS.each  {  out.append("||").append(it.replace('/',  'n'))  }        }        out.append("||n")        toons.sort{  a,  b  -­‐>  a.gearScore  ==  b.gearScore  ?  a.name  <=>  b.name  :  a.gearScore  <=>  b.gearScore  }    }        def  toons  =  retrieveToons(map.guild,  map.realm,  zone) ...    def  enc(s)  {  URLEncoder.encode(s,  'UTF-­‐8')  } } 20 20
  • 21.
    ...Plugin Tutorial: Worldof WarCraft... {groovy-wow-item:1624} {groovy-guild-gear:realm=Kirin Tor|guild=Faceroll Syndicate|zone=us} 21 21
  • 22.
    ...Plugin Tutorial: Worldof WarCraft... > atlas-mvn clover2:setup test clover2:aggregate clover2:clover 22 22
  • 23.
    ...Plugin Tutorial: Worldof WarCraft narrative  'segment  flown',  { package  com.atlassian.confluence.plugins.gwowplugin        as_a  'frequent  flyer'        i_want  'to  accrue  rewards  points  for  every  segment  I  fly' class  ToonSpec  extends  spock.lang.Specification  {        so_that  'I  can  receive  free  flights  for  my  dedication  to  the  airline'        def  "successful  name  of  Toon  given  classId"()  { } scenario  'segment  flown',  {                given:        given  'a  frequent  flyer  with  a  rewards  balance  of  1500  points'                def  t  =  new  Toon(classId:  thisClassId)        when  'that  flyer  completes  a  segment  worth  500  points'        then  'that  flyer  has  a  new  rewards  balance  of  2000  points'                expect: }                t.className  ==  name scenario  'segment  flown',  {          given  'a  frequent  flyer  with  a  rewards  balance  of  1500  points',  {                where:                  flyer  =  new  FrequentFlyer(1500)                name              |    thisClassId          }                "Hunter"      |    3          when  'that  flyer  completes  a  segment  worth  500  points',  {                "Rogue"        |    4                  flyer.fly(new  Segment(500))          }                "Priest"      |    5          then  'that  flyer  has  a  new  rewards  balance  of  2000  points',  {                  flyer.pointsBalance.shouldBe  2000        }          } }•  } • Testing with Spock • Or Cucumber, EasyB, JBehave, 23 23
  • 24.
    Scripting on thefly... 24 24
  • 25.
    ...Scripting on thefly... 25 25
  • 26.
  • 27.