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.

No comments: