Tuesday, August 08, 2006

Groovy Monkey: Eclipse Icons Script Pt 4/4: Rapid Prototyping with DOM Plugins

In the first and second postings of this series I showed how to implement a script that checks out the icons folders from all of the Eclipse Projects from the CVS server at dev.eclipse.org. The third installment had to do with breaking up and using library scripts through the Runner DOM to create code reuse and refactoring. This installment will now attempt to do what the third did, but with a prototype DOM plugin developed right in the workspace of your current Eclipse instance. The part I think that is exciting is that we will write a plugin and then use some simple Groovy Monkey scripts to dynamically update the DOM from within the current running Eclipse instance without restarting the workbench. Before I continue, one note of caution, this is still a bit of a work in progress. The plugins that will be dynamically swapped out are the simple type that you would use as a DOM and shouldn't contain much in the way of state, unless you are going through the trouble to make sure that you can hotswap out your plugins. The subject of how to make your plugins hotswappable is a whole subject onto itself, so we are going to sidestep it by keeping the DOM plugins simple.

This article is going to take you step by step through an example using the getEclipseIcons.gm script that was developed in previous articles in this series and the IncludeLocalBundle PDE JUnit test case that I wrote in net.sf.groovyMonkey.tests.

So to begin lets start with the example script that was created before we modified it to use library scripts in the last posting.

--- Came wiffling through the eclipsey wood ---
/*
* Menu: Get Eclipse Icons
* Kudos: ervinja
* License: EPL 1.0
* DOM: http://groovy-monkey.sourceforge.net/update/plugins/net.sf.groovyMonkey.dom
* Include-Bundle: org.eclipse.team.cvs.core
* Include-Bundle: org.eclipse.team.cvs.ui
*/
import org.eclipse.core.resources.IProject
import org.eclipse.core.resources.IResource
import org.eclipse.core.runtime.SubProgressMonitor
import org.eclipse.team.internal.ccvs.core.CVSProviderPlugin
import org.eclipse.team.internal.ccvs.core.ICVSRemoteFolder
import org.eclipse.team.internal.ccvs.ui.operations.CheckoutIntoOperation
import org.eclipse.team.internal.ccvs.ui.operations.DisconnectOperation

// Here we find the desired repository location that has already been configured
// in the CVS Respository Explorer.
def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
if( monitor.isCanceled() )
return
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}

// Here we query all the remote repository top level projects for sub-folders
// called icons and then store them into a list for later use.
def members = repositoryLoc.members( null, false, null )
monitor.beginTask( '', 2 * members.size() )
def iconFolders = []
for( member in members )
{
member.fetchChildren()
if( monitor.isCanceled() )
return
if( !member.childExists( 'icons' ) )
{
monitor.worked( 1 )
continue
}
def icons = member.getFolder( 'icons' )
iconFolders.add( icons )
monitor.worked( 1 )
}

// Check out those icon folders under a sub-folder of the target project called
// icons and place each remote icon folder in a sub-folder of icons that corresponds
// to its project name.
def targetProject = workspace.getRoot().getProject( 'GroovyMonkeyExamples' )
iconFolders.each
{ folder ->
if( monitor.isCanceled() )
return
def targetFolder = targetProject.getFolder( 'icons' ).getFolder( folder.getRemoteParent().getRepositoryRelativePath() )
new CheckoutIntoOperation( null, folder, targetFolder, true ).execute( new SubProgressMonitor( monitor, 1 ) )
monitor.worked( 1 )
}

// Clear off the CVS cruft.
jface.syncExec
{
new DisconnectOperation( null, [ targetProject ].toArray( new IProject[0] ), true ).run()
}

// Build the eclipse-icons.zip file using AntBuilder
def baseDir = targetProject.getFolder( 'icons' )
def destFile = targetProject.getFile( 'eclipse-icons.zip' )
if( destFile.exists() )
destFile.delete( true, null )

def ant = new AntBuilder()
ant.zip( basedir:"${baseDir.getRawLocation()}", destfile:"${destFile.getRawLocation()}" )

// Refresh the targetProject so that eclipse-icons.zip shows up in the Navigator and Package Explorer.
targetProject.refreshLocal( IResource.DEPTH_INFINITE, null )

monitor.done()
--- And burbled as it ran! ---


We want to create a DOM that can wrap calls to Eclipse's CVS API and provide a simplified interface for our scripts. One definite advantage to doing this is that we can develop the DOM plugin using the PDE and all of the wonderous advantages of the JDT Editor like autocompletion and the like. Another is that while the Runner DOM is useful, there is nothing like being able to make a direct method call on an object for clarity.

Setup DOM Plugin Project: net.sf.groovyMonkey.dom.cvs


The first step is that we need an Eclipse Project in our workspace in which to begin work on this new DOM. Of course this is a plugin project, so we use the 'New Plug-in Project' wizard. I name this project net.sf.groovyMonkey.dom.cvs since I am working on Groovy Monkey projects, however, you can name it whatever you like. I use all the default settings and avoid using the templates since Groovy Monkey does not have a wizard for creating a DOM Plugin Project. Of course, there is nothing that says that your DOM cannot have UI elements and therefore you might choose to use a template for your project, but at this time I am electing for simplicity.

After we have our project in the workspace, we must do some configuration work.

First make your plugin depend upon the net.sf.groovyMonkey plugin project by opening the Manifest editor and adding it to the list of required plugins.

Second add an extension of the extension point net.sf.groovyMonkey.dom using the Extensions page of the Manifest editor. Right click the net.sf.groovyMonkey.dom extension in the view and select 'New > updateSite'. This is what is used to display your DOM in the Outline view of the Groovy Monkey editor and the InstalledDOMs view. If you don't know exactly what it should be yet, this is fine, just select something that is likely to make sense. In my case I put in 'http://groovy-monkey.sourceforge.net/update/net.sf.groovyMonkey.dom.cvsdom'. To be honest I have yet to really use update sites to update my DOM plugins, so don't be surprised if it does not quite work. I tend to use the DOM plugins in my workspace and also have a default set of doms that come with Groovy Monkey included. If you try and it does/doesn't work, drop me a line ( jervin@completecomputing.com ) and let me know the results. As you will see from what follows, it may not be completely necessary.

Next right click the net.sf.groovyMonkey.dom node again and this time select 'New > dom'. Under this new node you see a few fields to fill in and we will go through them one by one.
  1. The first field is variableName and it is required. The variableName field is the name under which your dom will be put in the scripts binding, so it is important to select something easy to type and more importantly a name that is relatively unique. I know that the uniqueness part is a bit tricky and perhaps this is something that could use a tool to enhance or help. At the very least a warning in the Error Log that Eclipse has a DOM Plugins that define variables of the same name. Of course conflicting names may not be a problem if your scripts do not reference the DOMs with the same variable name. In this case I use cvsDOM. I am using the DOM postfix convention, so as to hopefully leave the binding and the script namespace relatively clean. Even if you should override the variable name inside your script's scope, you can use the bsf ( never override ) variable to access the BSFFunctions class which can allow you to look up the bound object by name.
  2. The next field specifies the class that will implement the IMonkeyDOMFactory interface to create instances of the DOM objects that the script will use. I click the class link and use the wizard to create a class called DOMFactory in the net.sf.groovymonkey.dom.cvsdom package that implements IMonkeyDOMFactory. I am not using a name like CVSDOMFactory since I think that the package that it exists in is obvious enough and I am not going to put another DOM object in that package, for now at least.
  3. The next field called 'id' is an optional field where you can put a human friendly name on this DOM, I think 'CVS DOM' is friendly enough.
  4. Finally there is the optional field called 'resource' and it is used to have you enter in the full class name of the object that the DOM Factory is supposed to return in the method getDOMRoot(). This field is preferred by the Groovy Monkey Outline page to assist the user in knowing what types and methods they have available. So it is highly recommended that you set it, since otherwise the Outline page content provider will call getDOMRoot() and perform reflection on it to determine what is being returned. It is kind of hard to know what to put in there before we create it, so we leave it blank for now.
Now we have the class DOMFactory that has a constructor and a method called getDOMRoot(). It is probably best practice to not do too much in the constructor and indeed in this class in general, since it could be instanciated numberous times and getDOMRoot() could be invoked a number of times from outside the context of the script. One thing though, do not return null from the getDOMRoot() method. So to make life easier on me, I create the class net.sf.groovymonkey.dom.cvs.CVSDOM and have the getDOMRoot() method just return a new instance of this class. Now that we have the type, go back to the manifest editor and put in that value into the resource field. Believe it or not we are now done with both the manifest editor and the DOM Factory class. This is all the scaffolding that is needed to put a DOM from a plugin into Groovy Monkey. Groovy Monkey searches for all net.sf.groovyMonkey.dom extensions and then grabs all the information we provided. You could in fact already use this as a plugin for your Eclipse workbench, of course the DOM object doesn't do anything yet, but that is beside the point, right? ;) You could use self hosting and make sure that the Groovy Monkey plugins are used in the runtime instance and check it out for yourself, but that is alot of trouble no and I said I will show you how to do it without headaches.

Setup Project to be loaded at will in current Eclipse Workspace


I just made a strong claim, that you can already install this DOM in your workbench and use it. It is an even stronger claim given that I made the promise that I will show you how to do it dynamically and without restarting your workbench and without having to spawn a self hosted runtime workbench instance. To make this promise come true, we are going to have to do a few things.

1. Create a monkey folder, for a script I am going to provide, under the project, in my case net.sf.groovyMonkey.dom.cvs.
2. Create a lib folder for the library scripts I am going to provide.
3. Copy the following script into your clipboard and use the 'Groovy Monkey > Paste New Script' menu command to put it into your workspace. You are going to have to move it to your project under the monkey folder manually, perhaps there is an opportunity for an enhancement here. I put in under my net.sf.groovyMonkey.dom.cvs project under the monkey folder and called it installDOM.gm.

--- Came wiffling through the eclipsey wood ---
/*
* Menu: Install/Update > CVS DOM
* Kudos: James E. Ervin
* License: EPL 1.0
* DOM: http://groovy-monkey.sourceforge.net/update/net.sf.groovyMonkey.dom
*/
import java.io.File
import org.apache.commons.io.FileUtils

def plugin = 'net.sf.groovyMonkey.dom.cvs'

// If this bundle is already installed, remove it
runnerDOM.runScript( "${plugin}/lib/uninstall.gm", [ pluginToUninstall:plugin ] )

// Build and export the bundle jar
bundlerDOM.createDeployDir()
jface.syncExec
{
bundlerDOM.buildPluginJar( workspace.getRoot().getProject( plugin ) )
}

// Grab the current version of the plugin to be able to identify the jar file.
def bundleVersion = runnerDOM.runScript( "${plugin}/lib/getBundleVersion.gm", [ 'plugin':plugin ] )

// Install and start the new bundle.
def context = bundleDOM.context()
def installedBundle = context.installBundle( "file:" + bundlerDOM.getDeployDir() + "/plugins/" + plugin + "_" + bundleVersion + ".jar" )
installedBundle.start()

--- And burbled as it ran! ---

* Note: If you are not calling your project 'net.sf.groovyMonkey.dom.cvs' remember to change the plugin variable above to the correct project name.
** Note: Be real careful with bunderDOM. By default it wants to deploy plugins to '/tmp/deployedBundles/plugins' and will want to delete and recreate the directory each time you call createDeployDir() on it. I forgot about this and set the deploy dir to my eclipse install plugins directory and *really* regretted it.
*** Note: When you restart eclipse, since the path '/tmp/deployedBundles' is not an Eclipse Extension Location, it be loaded when eclipse is restarted. I think while you are developing a DOM plugin this could be a feature, however, if you want it to persist just copy it from the deploy directory ( default: '/tmp/deployedBundles/plugins' ) to an Eclipse Extension Location or easier yet, into your Eclipse Install plugins directory.

4. Copy the following script into the clipboard and use the 'Groovy Monkey > Paste New Script' menu command to place it into your workspace. Once again move it manually into the project under the lib folder. There is a difference here, since the script above makes a call on it by directly referencing it as uninstall.gm, it is important you name it as such or you go back and change the installDOM.gm script to refer to the correct library script. I put it into my net.sf.groovyMonkey.dom.cvs project under lib and with the name uninstall.gm.

--- Came wiffling through the eclipsey wood ---
/*
* Kudos: James E. Ervin
* License: EPL 1.0
* DOM: http://groovy-monkey.sourceforge.net/update/net.sf.groovyMonkey.dom
*/
import org.apache.commons.lang.Validate

Validate.notNull( bsf.lookupBean( 'pluginToUninstall' ), 'pluginToUninstall must be set' )

for( plugin in bundleDOM.context().getBundles() )
{
if( plugin.getSymbolicName().equals( pluginToUninstall ) )
plugin.uninstall()
}
--- And burbled as it ran! ---


5. Once again copy the following script into the clipboard and use the 'Groovy Monkey > Paste New Script' menu command to place it into your workspace. Move it manually into the project under the lib folder. Since the main script makes a call on it by directly referencing it as getBundleVersion.gm, it is important you name it as such or you go back and change the installDOM.gm script to refer to the correct library script. I put it into my net.sf.groovyMonkey.dom.cvs project under lib and with the name getBundleVersion.gm.

--- Came wiffling through the eclipsey wood ---
/*
* Kudos: James E. Ervin
* License: EPL 1.0
* DOM: http://groovy-monkey.sourceforge.net/update/net.sf.groovyMonkey.dom
*/
import java.util.jar.Manifest
import org.apache.commons.lang.Validate

Validate.notNull( bsf.lookupBean( 'plugin' ), 'plugin must be set' )

def file = workspace.getRoot().getProject( plugin ).getFile( 'META-INF/MANIFEST.MF' )
def input = file.getContents()
try
{
def manifest = new Manifest( input )
def attributes = manifest.getMainAttributes()
return attributes.getValue( 'Bundle-Version' )
}
finally
{
input.close()
}

--- And burbled as it ran! ---


6. Just to be able to show that it works as expected, bring up the 'Installed DOMs' view by 'Window > Show View > Other > Groovy Monkey > Installed DOMs'. Look at the list, unless you have done some other work before following this script, it should not include an entry for your project, in my case no net.sf.groovyMonkey.dom.cvs DOM plugin installed.

Run the script installDOM.gm either by right click menu command 'Run Script' from the Groovy Monkey Editor or by 'Groovy Monkey > Install > CVS DOM'. Now go back to the 'Installed DOMs' view and it should be there. Go ahead and write a quick script to check it out if you like, of course we haven't added anything to it yet.

Start work on CVS DOM object


Well the initial idea is to replace sections of the original getEclipseIcons.gm script with method calls on this new CVS DOM object. So lets add a method called getKnownRepository that takes the arguments of the script that we used to refactor the script in the last article. To accomplish this we will have to add org.eclipse.team.cvs.core bundle as a Required Plug-in in the manifest editor. The code is as follows:

package net.sf.groovymonkey.dom.cvsdom;
import static org.eclipse.team.internal.ccvs.core.CVSProviderPlugin.getPlugin;
import org.eclipse.team.internal.ccvs.core.ICVSRepositoryLocation;

public class CVSDOM
{
public ICVSRepositoryLocation getKnownRepository( final String locationString )
{
for( ICVSRepositoryLocation location : getPlugin().getKnownRepositories() )
{
if( location.getLocation( true ).equals( locationString ) )
return location;
}
return null;
}
}

* Note: I am using Java 5.0 for this example and if you are running Groovy Monkey, your Eclipse instance needs to be run in Java 5.0 too. This should not be a large problem since Eclipse can be run with one JRE install, but your code that you are developing can be run with another. There are also all sorts of nice settings in the JDT to allow you to use Java 5.0, but force the code to be valid for 1.4, 1.3, etc...
** Note: Of course you can rewrite this to be 1.4 compliant if you wish.
*** Note: Lastly remember that since your DOM will not be invoked in a seperate thread ( i.e. Eclipse Job ), if you suspect that the operation will take too long, by all means pass in a progress monitor to the method and make use of it.

Now that is kind of neat, so lets use this. What we can't? The other version of the DOM was installed first? Well try running the 'Groovy Monkey > Install > CVS DOM' feature again. Now go back to the 'Installed DOMs' view, does it look the same? Open up the cvsDOM node, open up the CVSDOM class node and now check the listed methods. Your new getKnownRepository() method now shows up.

For people who have been working with Eclipse and developing plugins, this should pique your interest. We are using the OSGi runtime as it was intended, we hotswapped out our net.sf.groovyMonkey.dom.cvs plugin at runtime. If you keep the DOM plugin simple, this should work again and again, with no need to restart the workbench or having to mess with version numbers. To learn more about this goto OSGi and get a copy of the specification.

So now to use it in our script, we open up the getEclipseIcons.gm script in our Groovy Monkey Editor and then right click to bring up the menu option 'Add DOM to Script'. Select the new DOM to be added, in my case here it is net.sf.groovyMonkey.dom.cvs, and click ok. Now rewrite the following section of the script:

// Here we find the desired repository location that has already been configured
// in the CVS Respository Explorer.
def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
if( monitor.isCanceled() )
return
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}

as:

// Here we find the desired repository location that has already been configured
// in the CVS Respository Explorer.
def cvsRepository = ':pserver:anonymous@dev.eclipse.org:/home/eclipse'
def repositoryLoc = cvsDOM.getKnownRepository( cvsRepository )
Validate.notNull( repositoryLoc, "Error could not find the repository ${cvsRepository}, has it been added to the CVS Repository Explorer?" )

The last part just makes sure that the value is not null as a check.

You can continue on to replace the other sections of code with the DOM. Down below is the final version of the class:

package net.sf.groovymonkey.dom.cvsdom;
import static org.eclipse.core.runtime.SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK;
import static org.eclipse.swt.widgets.Display.getCurrent;
import static org.eclipse.swt.widgets.Display.getDefault;
import static org.eclipse.team.internal.ccvs.core.CVSProviderPlugin.getPlugin;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.team.internal.ccvs.core.CVSException;
import org.eclipse.team.internal.ccvs.core.ICVSRemoteFolder;
import org.eclipse.team.internal.ccvs.core.ICVSRemoteResource;
import org.eclipse.team.internal.ccvs.core.ICVSRepositoryLocation;
import org.eclipse.team.internal.ccvs.core.resources.RemoteFolder;
import org.eclipse.team.internal.ccvs.ui.operations.CheckoutIntoOperation;
import org.eclipse.team.internal.ccvs.ui.operations.DisconnectOperation;

public class CVSDOM
{
public ICVSRepositoryLocation getKnownRepository( final String locationString )
{
for( ICVSRepositoryLocation location : getPlugin().getKnownRepositories() )
{
if( location.getLocation( true ).equals( locationString ) )
return location;
}
return null;
}
public List<> getRepositoryResources( final IProgressMonitor progressMonitor,
final ICVSRepositoryLocation location,
final String subfolder )
throws CVSException
{
final IProgressMonitor monitor = progressMonitor == null ? new NullProgressMonitor() : progressMonitor;
final List<> folders = new ArrayList<>();
final ICVSRemoteResource[] members = location.members( null, false, null );
monitor.beginTask( "Getting Remote CVS Resources", members.length );
for( final ICVSRemoteResource member : members )
{
if( monitor.isCanceled() )
return null;
monitor.subTask( member.getName() );
final RemoteFolder folder = ( RemoteFolder )member;
folder.fetchChildren( new SubProgressMonitor( monitor, PREPEND_MAIN_LABEL_TO_SUBTASK ) );
if( StringUtils.isBlank( subfolder ) )
{
folders.add( member );
monitor.worked( 1 );
continue;
}
if( !folder.childExists( subfolder ) )
{
monitor.worked( 1 );
continue;
}
folders.add( ( ICVSRemoteResource )folder.getFolder( subfolder ) );
monitor.worked( 1 );
}
monitor.done();
return folders;
}
public CVSDOM checkOut( final IProgressMonitor progressMonitor,
final List<> remoteResources,
final IFolder target,
final boolean disconnect )
throws CVSException, InterruptedException, InvocationTargetException
{
final IProgressMonitor monitor = progressMonitor == null ? new NullProgressMonitor() : progressMonitor;
monitor.beginTask( "Checking out into target: " + target.getFullPath(), remoteResources.size() );
for( final ICVSRemoteResource folder : remoteResources )
{
if( monitor.isCanceled() )
return this;
if( !( folder instanceof ICVSRemoteFolder ) )
continue;
final IFolder targetFolder = target.getFolder( folder.getRemoteParent().getRepositoryRelativePath() );
new CheckoutIntoOperation( null, ( ICVSRemoteFolder )folder, targetFolder, true ).execute( new SubProgressMonitor( monitor, 1 ) );
monitor.worked( 1 );
}
if( !disconnect )
return this;
disconnect( target.getProject() );
monitor.done();
return this;
}
public CVSDOM disconnect( final IProject project )
{
if( getCurrent() == null )
{
final Runnable runnable = new Runnable()
{
public void run()
{
disconnect( project );
}

};
getDefault().syncExec( runnable );
return this;
}
try
{
new DisconnectOperation( null, new IProject[] { project }, true ).run();
}
catch( final InvocationTargetException e )
{
throw new RuntimeException( e );
}
catch( final InterruptedException e )
{
throw new RuntimeException( e );
}
return this;
}
}


Now here is the final version of the script to the new DOM:

--- Came wiffling through the eclipsey wood ---
/*
* Menu: Get Eclipse Icons > Refactored DOM
* Kudos: ervinja
* License: EPL 1.0
* DOM: http://groovy-monkey.sourceforge.net/update/plugins/net.sf.groovyMonkey.dom
* Include-Bundle: org.eclipse.team.cvs.core
* Include-Bundle: org.eclipse.team.cvs.ui
* DOM: http://groovy-monkey.sourceforge.net/update/net.sf.groovyMonkey.dom.cvs
*/
import org.apache.commons.lang.Validate
import org.eclipse.core.resources.IResource
import org.eclipse.core.runtime.SubProgressMonitor

// Here we find the desired repository location that has already been configured
// in the CVS Respository Explorer.
def cvsRepository = ':pserver:anonymous@dev.eclipse.org:/home/eclipse'
def repositoryLoc = cvsDOM.getKnownRepository( cvsRepository )
Validate.notNull( repositoryLoc, "Error could not find the repository ${cvsRepository}, has it been added to the CVS Repository Explorer?" )

// Here we query all the remote repository top level projects for sub-folders
// called icons and then store them into a list for later use.
def iconFolders = cvsDOM.getRepositoryResources( new SubProgressMonitor( monitor, 1 ), repositoryLoc, 'icons' )
if( iconFolders == null )
return

// Check out those icon folders under a sub-folder of the target project called
// icons and place each remote icon folder in a sub-folder of icons that corresponds
// to its project name.
def targetProject = workspace.getRoot().getProject( 'GroovyMonkeyExamples' )
cvsDOM.checkOut( new SubProgressMonitor( monitor, 1 ), iconFolders, targetProject.getFolder( 'icons' ), true )

// Build the eclipse-icons.zip file using AntBuilder
def baseDir = targetProject.getFolder( 'icons' )
def destFile = targetProject.getFile( 'eclipse-icons.zip' )
if( destFile.exists() )
destFile.delete( true, null )
def ant = new AntBuilder()
ant.zip( basedir:"${baseDir.getRawLocation()}", destfile:"${destFile.getRawLocation()}" )
// Refresh the targetProject so that eclipse-icons.zip shows up in the Navigator and Package Explorer.
targetProject.refreshLocal( IResource.DEPTH_INFINITE, null )
monitor.done()
--- And burbled as it ran! ---


If we like the results, we can then deploy the DOM plugin for real to an update site for others to use. Once I figure out a little bit more of this blog API and figure out how to upload files, I will include the source files for the scripts and the net.sf.groovyMonkey.dom.cvs DOM plugin.

I hope once again this has proven helpful and will encourage you to experiment more with Eclipse using Groovy Monkey.

No comments: