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.

Monday, August 07, 2006

Groovy Monkey: Eclipse Icons Script Pt 3/4: Using library scripts

In my previous posts on the Eclipse Icons Script ( here and here ) I wrote a functional Groovy Monkey script that would query the remote Eclipse projects in CVS to get the contents of all their 'icons' folders. The script would then clean up the CVS team cruft and then package it all as a zip file called 'eclipse-icons.zip'. The next question to ask is, how can I reuse some of this work? I mean there are more than one occasion where I would like to check projects out of CVS and the like. You could cut and paste those sections in to new scripts. While cut and paste is a programmer's best friend, avoiding duplication should be a mantra followed under penalty of death. So why don't we investigate some reuse?

To faciliate reuse Groovy Monkey has two mechanisms. One is the Runner DOM which allows you to invoke other Groovy Monkey scripts in your workspace and the second is developing a DOM plugin that can be invoked from your Groovy Monkey script using the script binding. This blog entry will describe how to use the Runner DOM.

One of the big advantages of using monkey scripts to reuse code is that it can be shared far more easily than with plugins. A library script can be posted to the web directly in a blog or web page and just as easily shared. In fact if you compensate for the workspace paths and the script names, you can cut and paste these scripts and run them immediately within your Eclipse workbench. All this without having to run the Update Manager and probably having to restart Eclipse.

I am going to paste the original script with some comments delimiting the various steps the script does to accomplish its task. These delimited sections would seem to be good candidates for separate library scripts.


--- 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! ---


One note before we start breaking the script up into component library scripts, is the issue of where to store the scripts. In Groovy Monkey when a script is loaded under the monkey folder of a project and contains a 'Menu:' metadata tag that is not blank, only then will it show up in the 'Groovy Monkey' menu. This is important because library scripts often need parameters passed into them for them to work and there is no mechanism for setting parameters using an action kicked off by a menu item. So as long as the library script does not have a 'Menu:' metadata tag set, this requirement is satisfied. Since library scripts are not meant to be invoked from the menu, you are free to place them where you like in the workspace. I like to put them under a lib folder under my 'GroovyMonkeyExamples' project, but you are not held to this.

As I look at the first part of the script that gets the ICVSRepositoryLocation instance associated with dev.eclipse.org, I realize I really don't like how I did it. So I go ahead and pop off a script to test the getLocation() method output and see if it is in a form I like.


--- Came wiffling through the eclipsey wood ---
/*
* Menu: List Repositories
* 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
*/
import org.eclipse.team.internal.ccvs.core.CVSProviderPlugin

def plugin = CVSProviderPlugin.getPlugin()
for( location in plugin.getKnownRepositories() )
{
if( monitor.isCanceled() )
return
out.println 'location.getLocation( true ): ' + location.getLocation( true )
out.println 'location.getLocation( false ): ' + location.getLocation( false )
}

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


When running that script I found no real difference with the output format, but since the boolean parameter is named display, I will set it to true hoping that means it matches the string shown in the CVS Repository Explorer. I am going to use the above script as the model for the library script. I copy it into the 'GroovyMonkeyExamples/lib' folder and rename it getKnownRepository.gm. The first modification I make is to remove the 'Menu:' metadata tag.

With a library script usually you have to pass in some parameters and Groovy Monkey does this via the script binding when invoked. You can use the BSFFunctions class that is put in the binding under the variable name bsf to check if something has been put into the binding. In fact the following is probably a good practice just for documentation. I named the following script getKnownRepository.gm


--- Came wiffling through the eclipsey wood ---
/*
* 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
*/
import org.apache.commons.lang.Validate
import org.eclipse.team.internal.ccvs.core.CVSProviderPlugin

Validate.notNull( bsf.lookupBean( 'cvsLocation' ), 'cvsLocation string describing the CVS location must be set' )

def plugin = CVSProviderPlugin.getPlugin()
for( location in plugin.getKnownRepositories() )
{
if( monitor.isCanceled() )
return
if( location.getLocation( true ) == "$cvsLocation" )
return location
}
return null

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


So now we modify the original script to invoke it. We have made it clear that we must put in a value in the binding for cvsLocation. Here is how it is done.


def map = [ cvsLocation : ':pserver:anonymous@dev.eclipse.org:/home/eclipse' ]
def repositoryLoc = runnerDOM.runScript( '/GroovyMonkeyExamples/lib/getKnownRepository.gm', map )


Notice that we specify the path in the workspace to the script as a string and that we pass in a java.util.Map instance that specifies those values we wish to set/override in the targetted script's binding. The script is started within its own Eclipse Job and this script is stopped while the other waits to complete and give us a return value. This is all there is to it. Remember that you must specify the full workspace relative path to the target script, this could cause some trouble when you are renaming or moving scripts around in your workspace. It would be nice to include the sort of refactoring support that exists in Eclipse, but since Groovy Monkey is designed to support multiple scripting languages, this presents a bit of a challenge to implement.

Now we rewrite the next section to a script that takes an ICVSRepositoryLocation and a string called subfolder that can be set blank. It does the work of the original section of the script with the addition that if the subfolder parameter is set blank, the top level project is returned instead. This is so you can use the script to just check out all the remote projects. I called this script getRepositoryResources.gm.

--- Came wiffling through the eclipsey wood ---
/*
* 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.apache.commons.lang.Validate
import org.apache.commons.lang.StringUtils

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

def members = repositoryLoc.members( null, false, null )
monitor.beginTask( 'Getting Remote CVS Resources', members.size() )
def folders = []
for( member in members )
{
if( monitor.isCanceled() )
return null
monitor.subTask( "${member.getName()}" )
member.fetchChildren()
if( StringUtils.isBlank( "$subfolder" ) )
{
folders.add( member )
monitor.worked( 1 )
continue
}
if( !member.childExists( "$subfolder" ) )
{
monitor.worked( 1 )
continue
}
def icons = member.getFolder( "$subfolder" )
folders.add( icons )
monitor.worked( 1 )
}
monitor.done()
return folders

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


Now here is the check out script, which I called checkOut.gm.

--- Came wiffling through the eclipsey wood ---
/*
* 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.apache.commons.lang.Validate
import org.apache.commons.lang.StringUtils
import org.eclipse.core.resources.IProject
import org.eclipse.core.resources.IResource
import org.eclipse.core.runtime.SubProgressMonitor
import org.eclipse.team.internal.ccvs.ui.operations.CheckoutIntoOperation
import org.eclipse.team.internal.ccvs.ui.operations.DisconnectOperation

Validate.notNull( bsf.lookupBean( 'remoteResources' ), 'remoteResources must be set' )
Validate.notNull( bsf.lookupBean( 'target' ), 'target must be set' )
Validate.notNull( bsf.lookupBean( 'disconnect' ), 'disconnect must be set' )

monitor.beginTask( 'Check out from CVS', remoteResources.size() )
remoteResources.each
{ folder ->
if( monitor.isCanceled() )
return
def targetFolder = target.getFolder( folder.getRemoteParent().getRepositoryRelativePath() )
new CheckoutIntoOperation( null, folder, targetFolder, true ).execute( new SubProgressMonitor( monitor, 1 ) )
monitor.worked( 1 )
}

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

monitor.done()

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


Now here is the script to build the zip file, which I called buildZip.gm.

--- Came wiffling through the eclipsey wood ---
/*
* 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
*/
import org.apache.commons.lang.Validate
import org.apache.commons.lang.StringUtils
import org.eclipse.core.resources.IProject
import org.eclipse.core.resources.IResource
import org.eclipse.core.runtime.SubProgressMonitor

Validate.notNull( bsf.lookupBean( 'srcFolder' ), 'srcFolder must be set' )
Validate.notNull( bsf.lookupBean( 'destFolder' ), 'destFolder must be set' )
Validate.notNull( bsf.lookupBean( 'zipName' ), 'zipName must be set' )
Validate.notNull( bsf.lookupBean( 'replace' ), 'replace must be set' )

monitor.beginTask( 'Building Zip', -1 )
def destFile = destFolder.getFile( "$zipName" )
if( destFile.exists() )
{
if( replace == false )
{
monitor.done()
return
}
destFile.delete( true, null )
}

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

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

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


Here is the original script, rewritten to use the library scripts. Note that your paths and script names could be different from these, adjust accordingly.

--- 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.IResource
import org.eclipse.core.runtime.SubProgressMonitor

def libDir = '/GroovyMonkeyExamples/lib/'
monitor.beginTask( 'Get Eclipse Icons', -1 )
// Here we find the desired repository location that has already been configured
// in the CVS Respository Explorer.
def map = [ cvsLocation : ':pserver:anonymous@dev.eclipse.org:/home/eclipse' ]
def repositoryLoc = runnerDOM.runScript( libDir + 'getKnownRepository.gm', map )
if( repositoryLoc == null )
throw new RuntimeException( 'Error repositoryLoc has not been configured correctly: ' + map )

// 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.
map = [ 'repositoryLoc':repositoryLoc, subfolder:'icons' ]
def iconFolders = runnerDOM.runScript( libDir + 'getRepositoryResources.gm', map )
if( iconFolders == null monitor.isCanceled() )
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' )
map = [ remoteResources:iconFolders, target:targetProject.getFolder( 'icons' ), disconnect:true ]
runnerDOM.runScript( libDir + 'checkOut.gm', map )
if( monitor.isCanceled() )
return

// Build the eclipse-icons.zip file using AntBuilder
map = [ srcFolder:targetProject.getFolder( 'icons' ), destFolder:targetProject, zipName: 'eclipse-icons.zip', replace:true ]
runnerDOM.runScript( libDir + 'buildZip.gm', map )

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


I hope you found this little exercise useful, because I have. For one I think there is some more work to do to remove some scaffolding, like maybe a convience validate DOM or method to reduce the need to keep calling Validate.notNull() for instance. I would also like to reduce some of the overhead in calling the Runner DOM, but I am not sure how yet to accomplish it.

Groovy Monkey: Eclipse Icons Script in Groovy Monkey Pt 2/4

In my previous post I began work on writing a script to checkout all the available icon files from the Eclipse Platform project as a test and demonstration of Groovy Monkey. I would like to continue on and begin demonstrating how you can rewrite some of that code to be reused as a Groovy Monkey library script or as a DOM plugin prototype. I would like to but after reading the original article that inspired me again, I realized I have a little more work to do. I need to remove all the CVS cruft from the workspace and then package the whole thing up as a zip file that could be published to a website.

Now before I dive into the subject, I would like to take a moment to point out a feature of Groovy Monkey that was inherited from Eclipse Monkey. In my previous posting I have a number of versions of the "Get Eclipse Icons" script and I have been working on this script from more than one location. It is easy in Groovy Monkey to grab scripts and to dump them into your workspace. Select the text of the script beginning with:


--- Came wiffling through the eclipsey wood ---


And ending with:


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


Copy into the clipboard and then go over to your Eclipse workbench and select "Groovy Monkey > Paste New Script". The script should be dumped into your workspace into the GroovyMonkeyExamples or Groovy Monkey Scripts project under the monkey folder. You can directly invoke it from the Groovy Monkey menu or can proceed to edit it.

Now back to the job at hand. We could just delete the CVS folders and files and then have Eclipse update the workspace, but there is a command in Eclipse that will disconnect a project from CVS and so why not try and use it?

I know there is a Disconnect popup menu command from the Package Explorer view and so I know to begin looking through the plugin.xml files of Eclipse plugins I suspect. From the previous work on this script I suspect the org.eclipse.team.cvs plugins, so lets start there. Since the command I would like to automate has a graphical component, I start with the org.eclipse.team.cvs.ui plugin. Since I know that Eclipse likes to use plugin/bundle.properties to put the names of UI commands in, I start there. Sure enough "UnmanageFolder.label=&Disconnect..." seems to be what I am looking for. In the plugin.xml this maps to the class org.eclipse.team.internal.ccvs.ui.actions.UnmanageAction. Lets start there.

In the UnmanageAction class, it seems as though all it does is do some configuration , then query the user to see if they are sure they wish to disconnect the project and then use the DisconnectOperation class to actually do the work. So it looks as though all we must do is to instanciate the DisconnectOperation and then invoke run, lets go for it.

One note, if you have already run the script and checked out the icons, it would probably be better to split this off into another script so as to avoid having to recheckout everything.


--- 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.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

def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
if( monitor.isCanceled() )
return
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}

def targetProject = workspace.getRoot().getProject( 'GroovyMonkeyExamples' )
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 )
}
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 )
}
jface.syncExec
{
new DisconnectOperation( null, [ targetProject ].toArray( new IProject[0] ), true ).run()
}
monitor.done()
--- And burbled as it ran! ---


Now we have a script that checks out icons and then removes CVS cruft. So that just leaves us with the need to package it up as a zip. I don't know of any Eclipse code in particular to support this so I am going to use the Groovy Ant scripting support to do this quickly. Afterwords I am going to have the script tell the workspace to refresh itself.


--- 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

def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
if( monitor.isCanceled() )
return
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}

def targetProject = workspace.getRoot().getProject( 'GroovyMonkeyExamples' )
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 )
}
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 )
}
jface.syncExec
{
new DisconnectOperation( null, [ targetProject ].toArray( new IProject[0] ), true ).run()
}

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()}" )

targetProject.refreshLocal( IResource.DEPTH_INFINITE, null )

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


Now we have a script that accomplishes the entirety of the original script. Now to some of you this script is probably pretty good and you would use it as written. Now me, with my ADD and lazy mind, does not like it. I would like to be able to reuse some of this code in other scripts and quite frankly it is too complex and ugly. So my next couple of blog entries are going to deal with this issue. First we are going to rewrite the script so that it can invoke other Groovy Monkey scripts and create reuse that way and second we are going to start work on doing a DOM plugin prototype that I think you will find more dynamic and fun that doing straight Eclipse plugin work. So hopefully you will stick around for them.

Friday, August 04, 2006

Groovy Monkey: Eclipse Icons script in Groovy Monkey Pt 1/4

Since I have been working on Groovy Monkey, I have been trying to reuse the icons that are provided by the Eclipse Platform. Why? Well Eclipse does have its look and feel and where concepts in Groovy Monkey are similar to those in Eclipse, why not make it look similar? Besides I am lazy and the thought of creating a bunch of icons does not float my boat.

So I took particular interest in what Mr. Ben Walding posted. He wrote a script that would goto cvs and check out the icon folders from the Eclipse projects themselves and place them on your local system. This is something I have considered myself, though I thought more about using your local install of the Eclipse SDK and extracting them.

Still what he wrote was a Shell script and if it can work for you, use it. It did get me to thinking, why not try and rewrite it as a Groovy Monkey script so that you can run it in Eclipse and dump it into your workspace? This could be a really nice testcase for Groovy Monkey.

So I am going to be updating this post based on my results in attempting to write it in Groovy Monkey. I mean I am writing this as I am trying to write the script and so do not be surprised if there are a few dead ends and the like.

The first step is to setup Eclipse for this. We need to add the repository location :pserver:anonymous@dev.eclipse.org:/home/eclipse to the CVS Repository Explorer. Validate the connection by opening the HEAD branch under the explorer and browse around a bit just to make sure it is working. So now we should have access to the repositories to do a checkout.

The next step is a bit tricky since the Eclipse Team people have yet to publish a public API for CVS, so now we have to fumble around and find what we want. I start by looking at the Eclipse Help under the "Platform Plugin Developer Guide" and check out what is there under "Team Support." On that page I see the following blurb of text:

<extension
point="org.eclipse.team.core.repository">
<repository
class="org.eclipse.team.internal.ccvs.core.CVSTeamProvider"
id="org.eclipse.team.cvs.core.cvsprovider">
</repository>
</extension>


The specifics are not as important as the fact that I have found my first clue, use Ctrl-Shift-T to open the class CVSTeamProvider and check it out. As I scan the API and the documentation on Repository Providers, I realize that they are mapped directly to projects and I am not sure that is what I want to do. Still one thing did catch my eye as a first target of a Groovy Monkey script. The RepositoryProvider class has static methods to allow you to find out what Repository Providers exist currently in your workspace. So I create my new Groovy Monkey Script using the "File > New > Other > Groovy Monkey > New Groovy Monkey Script" menu item to start the wizard. I name it getEclipseIcons.gm and here it is.


--- 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.core
*/
import org.eclipse.team.core.RepositoryProvider
for( providerID in RepositoryProvider.getAllProviderTypeIds() )
{
out.println "providerID: ${providerID}"
}
--- And burbled as it ran! ---


In the Monkey output console I get the following:

providerID: org.eclipse.team.cvs.core.cvsnature
providerID: org.eclipse.pde.core.BinaryRepositoryProvider
providerID: org.polarion.team.svn.core.svnnature


So now I know that I have three providers, one for cvs, another one that the PDE is providing and my subversion provider.

Well the above script is interesting and does highlight in a simple way some of the power of Groovy Script, it does not get me closer to what I want. So I go open up the CVSRepositoryProvider in the Editor and use the "Show In > Package Explorer" pop-up menu command to show me where the code is located. I see it is in the org.eclipse.team.cvs.core plugin and I notice the CVSProviderPlugin class.

The CVSProviderPlugin class seems more interesting and I begin to explore by rewriting the script to the following:

--- 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
*/
import org.eclipse.team.internal.ccvs.core.CVSProviderPlugin

def plugin = CVSProviderPlugin.getPlugin()
for( location in plugin.getKnownRepositories() )
{
out.println "Repository Location: ${location.dump()}"
}
--- And burbled as it ran! ---

The output in the console is:

Repository Location: <org.eclipse.team.internal.ccvs.core.connection.CVSRepositoryLocation@63adab5c method=org.eclipse.team.internal.ccvs.core.connection.PServerConnectionMethod@262e12 user=anonymous password=null host=dev.eclipse.org port=0 root=/home/eclipse userFixed=true passwordFixed=false allowCaching=false serverPlatform=1 previousAuthenticationFailed=false>
Repository Location: <org.eclipse.team.internal.ccvs.core.connection.CVSRepositoryLocation@553087e method=org.eclipse.team.internal.ccvs.core.connection.PServerConnectionMethod@262e12 user=anonymous password=null host=dev.eclipse.org port=0 root=/cvsroot/technology userFixed=true passwordFixed=false allowCaching=false serverPlatform=0 previousAuthenticationFailed=false>
Repository Location: <org.eclipse.team.internal.ccvs.core.connection.CVSRepositoryLocation@6142ea28 method=org.eclipse.team.internal.ccvs.core.connection.ExtConnectionMethod@7b51d2 user=xxxxx password=null host=groovy-monkey.cvs.sourceforge.net port=0 root=/cvsroot/groovy-monkey userFixed=true passwordFixed=false allowCaching=false serverPlatform=0 previousAuthenticationFailed=false>


Now this is looking like it can be useful.

On second thought, maybe not, as I scan the Eclipse Platform code a bit more, it really looks like what I want is in the org.eclipse.team.cvs.ui plugin and that I want to try out is the CheckoutIntoOperation. CheckoutIntoOperation requires that I
get some IRemoteFolder(s) and well the above script is useful after all. So I rewrite the script as follows:

--- 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.team.internal.ccvs.core.CVSProviderPlugin
import org.eclipse.team.internal.ccvs.ui.operations.CheckoutIntoOperation

def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
out.println "Repository Location: ${location.getRootDirectory()}"
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}
for( member in repositoryLoc.members( null, false, null ) )
{
out.println "Repository Path: ${member.getRepositoryRelativePath()}"
}

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

I get the following output from running this modified script:

Repository Location: /home/eclipse
Repository Location: /cvsroot/technology
Repository Location: /cvsroot/groovy-monkey
Repository Path: CVSROOT
Repository Path: cdt-contrib
Repository Path: eclipse-project-home
Repository Path: eclipse-project-website
Repository Path: equinox-incubator
Repository Path: jdt-core-home
Repository Path: jdt-debug-home
Repository Path: jdt-doc-home
Repository Path: jdt-ui-home
Repository Path: org.apache.ant
Repository Path: org.apache.lucene
Repository Path: org.apache.xerces
Repository Path: org.eclipse.ant.core
Repository Path: org.eclipse.ant.optional.junit
Repository Path: org.eclipse.ant.tests.core
Repository Path: org.eclipse.ant.tests.ui
Repository Path: org.eclipse.ant.ui
Repository Path: org.eclipse.compare
Repository Path: org.eclipse.compare.examples
Repository Path: org.eclipse.compare.examples.xml
Repository Path: org.eclipse.compare.tests
Repository Path: org.eclipse.core.applicationrunner
Repository Path: org.eclipse.core.boot
Repository Path: org.eclipse.core.commands
Repository Path: org.eclipse.core.components
Repository Path: org.eclipse.core.contenttype
Repository Path: org.eclipse.core.expressions
...


Now this is starting to look like the script described in Mr Walding's blog posting. We have a hint that we want to use the CheckoutIntoOperation and we now have access to the remote folders.

Since each of these CVS operations can take some time to complete, I need to have a way to allow the user to monitor progress and cancel the script if they desire. Therefore next step I realize I should do is start using the monitor variable. I also want to begin to check the individual eclipse projects for icons. I know, I think, that Eclipse CVS projects like to put all their icon files into a top level directory called icons, so I am going to check for them.


--- 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.team.internal.ccvs.core.CVSProviderPlugin
import org.eclipse.team.internal.ccvs.ui.operations.CheckoutIntoOperation

def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
out.println "Repository Location: ${location.getRootDirectory()}"
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}
monitor.beginTask( '', members.size() )
for( member in repositoryLoc.members( null, false, null ) )
{
if( monitor.isCanceled() )
return
if( !member.childExists( 'icons' ) )
{
monitor.worked( 1 )
continue
}
out.println "Repository Path: ${member.getRepositoryRelativePath()}"
}
monitor.done()
--- And burbled as it ran! ---


I get a surprising result from the script above, it doesn't work. I fumble about the API and notice an interesting method called fetchChildren(). I am guessing that information is cached in these classes only on request, so I add the call.


--- 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.team.internal.ccvs.core.CVSProviderPlugin
import org.eclipse.team.internal.ccvs.ui.operations.CheckoutIntoOperation

def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
out.println "Repository Location: ${location.getRootDirectory()}"
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}
monitor.beginTask( '', members.size() )
for( member in repositoryLoc.members( null, false, null ) )
{
member.fetchChildren()
if( monitor.isCanceled() )
return
if( !member.childExists( 'icons' ) )
{
monitor.worked( 1 )
continue
}
out.println "Repository Path: ${member.getRepositoryRelativePath()}"
}
monitor.done()
--- And burbled as it ran! ---


Well the script now does what I expected to. So to complete this exercise I collect those repository resources that correspond to icon folders and place them into a list called iconFolders and prepare to make the call to CheckoutIntoOperation. I did have one more intermediate step here, I tried to check out all the icons into the same set of folders, but the CheckoutIntoOperation would wipe the directory for each project. So we check them out into a project that already exists in my workspace called GroovyMonkeyExamples and into a folder called icons. We place the icon folders into subdirectories under icons that correspond to their remote repository path. To complete we instanciate the CheckoutIntoOperation and invoke its execute() method.


--- 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.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

def plugin = CVSProviderPlugin.getPlugin()
def repositoryLoc
for( location in plugin.getKnownRepositories() )
{
if( monitor.isCanceled() )
return
if( location.getRootDirectory() == '/home/eclipse' )
repositoryLoc = location
}

def targetProject = workspace.getRoot().getProject( 'GroovyMonkeyExamples' )
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 )
}
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 )
}
monitor.done()

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


This now gets what we want. We check all the Eclipse projects for icons and get them all. It can be left for homework to try and automate the task to disconnect the project from CVS and to package it up into a zip archive.

There are a few notes to make here. First, this is a quick prototype and there may very well be far more efficient ways to accomplish this. In fact, you could write a seperate script to grab all the icon folders and print them out so that you can paste them into this script and check only those remote projects that have icons. A second note to make is, well this prototype only took a couple of hours to accomplish and that was with my less than ideal setup ( and I do mean less than ideal ). Imagine the time it would have taken using self hosting or deploying plugins. The last note is that of course it would be been easier to install cvs on my system and script commands to call it directly, but that would have been cheating since the whole point was to demonstrate how you could use Groovy Monkey to navigate the Eclipse Platform API world directly and easily.

So we do have a functional script, but it is not very asthetically pleasing ( at least to me ) and the ability to check out remote resources from CVS into my workspace is probably something I want to do again. So in future blog entries I want to demonstrate how we can split off some functions into seperate libraries scripts that can be reused or how you can begin to prototype some plugins by starting to write the function as DOM in your workspace and be able to runtime deploy it into your current Eclipse instance.

External Link

Hello there

This is my first post to my blog Iacobus.

I am James Ervin and I think I will be blogging mostly on issues relating to Software Engineering in particular with regards to Eclipse, Java and dynamic languages like Groovy. I am also the author of the Groovy Monkey scripting tool for Eclipse, I encourage you to check it out at http://docs.codehaus.org/display/GROOVY/Groovy+Monkey

And while you are there by all means check out the Groovy scripting language.