| No Recruitment Agencies If we ever want you, we'll call you. Calling us first will guarantee you are blacklisted. |
We're hiring!
Get your CV's in while the irons hot.
Funky ... you learn something new every day:
/**
* {@inheritDoc}
*/
Slap that on an inherited method and it'll do what you expect. Cool.
Well I decided that seen as nobody else outside of Atlassian has yet braved the challenge of FishEye plugin writing, I thought that I would give it a go. Here's a rather crude guide to how I did.
Day 1
Step 1 - Decide on a plugin to write
This is much harder than I originally anticipated ... at least finding a useful one is! Confluence does general information, JIRA does issues/workflows and Bamboo does cause & effect - each of these products can be abused to do various things that they weren't originally intended to.
We've made Confluence a website and JIRA a time manager – Bamboo has also been extended by others into a repository manager which tags & releases successful builds. Unfortunately FishEye is a far more focussed tool - one that was significantly enhanced by Crucible, leaving the scope for extensions somewhat limited; aside from the academic exercise of trying creating plugins.
Anyway, I eventually settled on a reporting plugin which provides a framework for alternative views on the repository data. For example: compare the commit ratio's of the various members of a group of a period of time - by size and by quantity.
Step 2 - Creating the shell
My first POM was essentially my standard Confluence POM but stripped back to to just the servlet-api and junit dependencies - I then manually add in the jar's from the fisheye distribution to enable compilation from within the IDE, however this is far from perfect.
Then Jonathan Nolen pointed me at a more useful one, and then I found another:
- http://svn.atlassian.com/svn/public/atlassian/studio/crucible-streams-servlet/trunk/
- http://svn.atlassian.com/svn/public/atlassian/studio/applinks-fisheye-plugin/trunk/pom.xml
Both of these use FishEye 1.4.2, and I'm needing to compile against v1.5.1. After a quick word via Jonathan we now have 1.5.1 here: https://maven.atlassian.com/public/com/atlassian/fisheye/fisheye-jar/1.5.1/
Step 3 - Understand the API
Well, at least try to begin to understand the API so that I can start to produce something more useful than a Hello World! So I went on the hunt for the information that I needed to be able to write a plugin to do anything useful, that is:
- Static APIs in the JavaDoc
- Documentation about the component architecture (spring) and the components available
- Real-world examples of the modules being used
- Source to get me going when I needed to delve a little deeper
Unfortunately, the javadoc was sparse to say the least - certainly nothing in there that was any use.
Day 2
Step 4 - Debugging FishEye plugins
If you add the following to the bin/run.sh file it will cause it to listen on port 5005 for a debugger, allowing you to connect your IDE:
export FISHEYE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"
Step 5 - Blood, Sweat and Beers
Well, working with FishEye isn't exactly a holiday; in fact it's quite the opposite as it's enough to drive to (more) drink - so to help, here are a few notes that I've got from working with it for a while:
Static APIs & Spring
- AppConfig.getsConfig() gets you the RootConfig object which contains the system configuration, including the repository details.
- You can get hold of Spring components with the @Autowired annontation, you can also use a standard setter.
There isn't any Java source available, even to commercial licensees - this causes problems when it comes to finding out what you can do. With a good IDE, a reasonable debugger and a large helping of luck/experience you can mitigate this – although you will find your swear jar filling at an alarming rate!
Plugin State Aware
Servlet, RPC & Spring Component modules don't have the StateAware methods called on enable/disable. To be more specific, none of them are called. The best I've found you can get so far, is to call enabled() from the constructor method:
public class MyServlet extends HttpServlet implements StateAware { public HelloWorldServlet() { enabled(); } // Implementation of API: StateAware }
This is like building a house of cards I agree, and I appeal to the FishEye developers to have the StateAware interface respected like it is in Confluence.
WebWork & XWork Hacking
I've hacked around through APIs many times before, and David Peterson came up with the Confluence [Conveyor library] which is an elegant way of overriding actions. So my thoughts turned to attempting the following:
- Override an action, just to replace the result (and hopefully serve the resulting JSP from inside the plugin).
- Create a new action which has the same look and feel as the rest of FishEye
- Look at linking it in via the user interface dynamically
Man - what have I let myself in for? ... Well after a day of hacking ... hell! I've had to seriously overhaul the Conveyor library, and effectively turn it inside out. However I now have the ability to add my own actions!
Brilliant ... unfortunately the result files (*.jsp / *.vm) still need to be sourced from the root classloader (i.e. you have to put them in the content directory of the webapp). I spent a few hours digging into hacking up the velocity classloader on the fly, but it just doesn't make sense ... instead you will want to copy Confluence by adding something like this to velocity.properties:
# Plugin subsystem resource.loader=wwfile,wwclass,confplugin # dynamic plugin classpath loader (for plugin resources) confplugin.resource.loader.description=Confluence Dynamic Plugin classpath loader confplugin.resource.loader.class=com.atlassian.velocity.DynamicPluginResourceLoader # set caching on for resource loaders (see com.opensymphony.webwork.views.velocity.VelocityManager) # comment in these lines to add template caching (faster) wwfile.resource.loader.cache=true wwclass.resource.loader.cache=true confplugin.resource.loader.cache=true
The class file will look something like this (although it will need to get the pluginAccessor from Spring another way, as ContainerManager doesn't exist):
public class DynamicPluginResourceLoader extends ResourceLoader { private PluginAccessor pluginAccessor; public void init(ExtendedProperties extendedProperties) { // do nothing } public InputStream getResourceStream(String name) throws ResourceNotFoundException { while (name.startsWith("/") && name.length() > 1) name = name.substring(1); if (pluginAccessor == null) { pluginAccessor = (PluginAccessor) ContainerManager.getComponent("pluginAccessor"); if (pluginAccessor == null) throw new ResourceNotFoundException("No plugin manager."); } return pluginAccessor.getDynamicResourceAsStream(name); } public boolean isSourceModified(Resource resource) { return false; // copied from org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader } public long getLastModified(Resource resource) { return 0; // copied from org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader } }
I've not dug into what you'd need for JSP / Freemarker result support.
Decoration ... or the lack of
Unfortunately, FishEye doesn't use sitemesh - so decorating up is more than a pain. I've settled for the fact that I'm going to have to mimic the UI and it wont just fit in automatically. Sitemesh will also open the door to theme plugins.
Step 6?
I'm going to try and figure out how the charting stuff works in FishEye and replicate that with my own data ... that should be interesting!
We'll see how that goes.
Well, someone else did enter a FishEye Plugin - actually it's an Eclipse plugin that talks anonymous to the remote API - but I continued with in my pursuit of getting actions rendering out in FishEye, making custom reports/pages a possibility.
Read on to see how far I got, or view the results
Day 3
Step 6 - Finish working around the subsystem limitations
There were a few more areas which needed tidying up before I could get on with the nitty-gritty of actually implementing the plugin logic. After all, this is what plugin developers should really be spending their time on.
Delegation of Action Instanciation
As the FishEye's SpringObjectFactory doesn't look through the plugin class loaders to load actions (and I tried registering my actions in spring too, which didn't work and which was needless overhead IMO), I needed to override this with my own ConveyorObjectFactory which tried the original factory first, but if it came up blank it would try through my local class loader too.
This works a treat, but unfortunately leaves the newly instanciated objects unwired by Spring.
Spring Components & Autowiring
I found the lack of a ContainerManager (Confluence) / ComponentManager (JIRA) a pain because I couldn't just call ContainerManager.autowireComponent( myAction ) to wire it up. I needed to burrow into the SpringObjectFactory and grab the ApplictionContext, then reuse that in the ConveyorObjectFactory to autowire actions created using the factory.
This enabled me to have actions which were constructed inside of the plugin and routed appropriately to the result ... unfortunately the resources from the VelocityResult or ServletDispatcherResult looked up their resources less gracefully, requiring the VM or JSP files to be on the main classpath - damn. I spent a number of hours looking into creating a ConveyorVelocityResult which intercepted the resource loading, but the amount of time spent on doing so, as well as the messy/hacky result just wasn't worth it. So ...
Self-extracting Plugin Resources
Resigning myself to the fact that resources had to go in $FISHEYE_INST/content I wrote a routine that looked through my plugin for resources and extracted them into a unique location in that path. Unfortunately, as we don't have any plugin unload/uninstall events or callbacks, I am unable to clean up those extracted files – however they won't cause any problems sitting there.
If you use the same methodology, please be careful about overwriting files. With that done - yahoo! We're in business!
I have my hello world action working.
Step 7 - Styling it up
Unfortunately, FishEye doesn't use sitemesh and lots of the interface elements are hardcoded. For example, the tabs in the top right hand corner are determined by $FISHEYE_INST/content/WEB-INF/tags/header.tag which all the tabs hardcoded, and that tag is reused in (for example) $FISHEYE_INST/content/WEB-INF/jsp/chart.jsp. You will need JSP Source access to be able to walk through this with me.
This method works well and quite elegantly, if you assume that any changes will be made by someone who has carte-blanche over modifications to any source or configuration file in the system (Java, JSP, XML or otherwise). This falls down quite quickly if you impose the plugin subsystem restrictions on it ... more web resource module usage please!
Work Around
This was to include the FishEye CSS files and mimic some aspects of the HTML output to generate things like the blue FishEye bar at the top, and the breadcrumbs below it. It works quite well!
Step 8 - Creating a Workflow
Now that I'm able to run xwork/webwork actions properly - I was able to get my plugin up and running in under an hour doing what I wanted, thanks to the ground work of the logic being done. The main logic looks along the lines of:
public class DevReportManagerImpl implements DevReportManager { private final Logger log = Logger.getLogger( getClass() ); private RootConfig rootConfig; // Components public RootConfig getRootConfig() { if (rootConfig == null) { rootConfig = AppConfig.getsConfig(); } return rootConfig; } public void setRootConfig(RootConfig rootConfig) { this.rootConfig = rootConfig; } // API: DevReportManager @SuppressWarnings("unchecked") public Map<String, List<ChangeSet>> getChangesPerUser(String repository, RecentChangesParams params) throws RepositoryHandle.StateException, DbException { // I have no idea why I have to do this Disposer.pushThreadInstance(); try { RepositoryHandle rh = getRepositoryHandle( repository ); RepositoryEngine re = rh.acquireEngine(); RevisionCache rc = re.getRevisionCache(); if (params == null) { params = new RecentChangesParams(); } Map<String, List<ChangeSet>> results = new HashMap<String, List<ChangeSet>>(); for (String username : rc.findAuthors( params.getPath() )) { params.setConstraint( new TermQuery3(CommonSchema.E_AUTHOR_TO_REVID, username, null) ); List<ChangeSet> changeSets = (List<ChangeSet>) rc.findRecentChangeSets( params ); results.put(username, changeSets); } return results; } finally { // I have no idea why I have to do this Disposer.popThreadInstance(); } } public RepositoryHandle getRepositoryHandle(String repository) { RepositoryHandle rh = getRootConfig().getRepositoryManager().getRepository( repository ); if (rh == null) { log.warn("Unable to find the repository: "+ repository); } return rh; } public List<RepositoryHandle> getHandles() { return getRootConfig().getRepositoryManager().getHandles(); } }
Step 9 - Bring it up to scratch
To make the plugin releasable and usable by others, there's a few things that are needed.
Globally Usable POM
I wanted a pom.xml which could be downloaded by anyone and build everything needed, so I had to get to create one.
Tests
Adding unit tests for the custom functionality (I haven't created tests for the conveyor stuff though).
Check in & Acceptance Test
This resulted in my first release of the plugin, and some screenshots.
Tada!
Let me know what you think.
Adaptavist are going to be attending the AUG and are pleased to announce the informal "after party" which will take the form of a pub crawl. As this incarnation of the Atlassian User Group is developer focused, we are getting together to talk about Confluence & JIRA in the enterprise. We've personally invited some of our customers that we think will be in the area, however anyone is welcome!
We're suggesting that those who aren't interested in the developer tools to just meet us at the venue 7pm. Then again, AUGs are always interesting - you never know what you might learn! If you're interested please respond to this comment so we can gauge numbers.
Proof of what can happen if a wife or girlfriend drags her husband or boyfriend along shopping. This letter was recently sent by Tesco's Head Office to a customer in Oxford:
Dear Mrs. Murray,
While we thank you for your valued custom and use of the Tesco Loyalty Card, the Manager of our store in Banbury is considering banning you and your family from shopping with us, unless your husband stops his antics.
Below is a list of offences over the past few months all verified by our surveillance
- June 15: Took 24 boxes of condoms and randomly put them in people's trolleys when they weren't looking.
- July 2: Set all the alarm clocks in Housewares to go off at 5-minute intervals.
- July 7: Made a trail of tomato juice on the floor leading to feminine products aisle.
- July 19: Walked up to an employee and told her in an official tone, 'Code 3' in housewares..... and watched what happened.
- August 14: Moved a 'CAUTION - WET FLOOR' sign to a carpeted area.
- September 15: Set up a tent in the outdoor clothing department and told shoppers he'd invite them in if they would bring sausages and a Calor gas stove.
- September 23: When the Deputy Manager asked if she could help him, he began to cry and asked, 'Why can't you people just leave me alone?'
- October 4: Looked right into the security camera; used it as a mirror, picked his nose, and ate it.
- November 10: While appearing to be choosing kitchen knives in the House wares aisle asked an assistant if he knew where the antidepressants were.
- December 3: Darted around the store suspiciously, loudly humming the 'Mission Impossible' theme.
- December 6: In the kitchenware aisle, practised the 'Madonna look' using different size funnels.
- December 18: Hid in a clothing rack and when people browsed, yelled 'PICK ME!' 'PICK ME!'
- December 21: When an announcement came over the loud speaker, assumed the foetal position and screamed 'NO! NO! It's those voices again.'
And; last, but not least:
14. December 23: Went into a fitting room, shut the door, waited a while; then yelled, very loudly, 'There is no toilet paper in here.'
Yours sincerely,
Charles Brown
Store Manager
Can you tell it's Friday?
After the success of the London AUG a few days ago, we are onto the one in Frankfurt.
It'll just be me this time, hanging around to help any developers struggling to write plugins and generally manage with the Atlassian products when sat from the outside.
If there's anything specific people would like answers on, please let me know by commenting here. For example:
- How do I customise the labels page?
- How do the labels work?
- Can I customise the search results page?
- What are the ways to categorise content, and how can I ensure performance after scaling?
- What plugins are best to do x/y/z?
- How do I mitigate the security risk posed by security flaw A in Atlassian product B?
Hope to see you there!

