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.

