|
||||
|
CC's Build System Other Information
|
Jelly Scripting HintsMaven extensibility is it's greatest strength, unfortunately there is not alot of documentation for Jelly scripting to get you started. If you start by following what Maven standards there are and developing some for your own development group, scripting will be more straight foward. For starters, many of the definitions that you will configure in your project POM files are available to Jelly, including defaults that you may not set. They may also be dereferenced in project.property files, such as this snippet from one of our property files:ccScm.cvs.module=Projects/${pom.name} ccScm.map.file=${basedir}/../Builds/version_map.xml serverDir=/maven/repository ccJnlp.remote.path=${serverDir}/${pom.groupId}/jnlp In the above example, ${pom.name} is from the <name> element in the POM, ${basedir} is a core Maven variable that points to the POM's parent directory, and ${pom.groupId} comes from the <groupId> element in the POM. This example is inherited from a parent POM since it is in it's directory, but these variables change for each POM that is used for building. Some elements in the POM cannot be accessed so easily since there may be many elements of the type you wish to access, such as the dependencies of a project. In that case, you need to do some Jelly scripting. Cut and paste the following code into a maven.xml file in your project, then run the arttest goal: <goal name="arttest"> <j:forEach var="lib" items="${pom.artifacts}"> <j:set var="dep" value="${lib.dependency}"/> <j:set var="name" value="${lib.file.name}"/> <j:set var="parent" value="${lib.file.parent}"/> <j:set var="artifact" value="${dep.artifact}"/> <j:set var="id" value="${dep.id}"/> <j:set var="group" value="${dep.artifactDirectory}"/> <j:set var="version" value="${dep.version}"/> <j:set var="type" value="${dep.type}"/> <j:set var="depname" value="${dep.artifactId}"/> <ant:echo>LIB: ${lib}</ant:echo> <ant:echo>DEP: ${dep}</ant:echo> <ant:echo>NAME: ${name}</ant:echo> <ant:echo>PARENT: ${parent}</ant:echo> <ant:echo>ARTIFACT: ${artifact}</ant:echo> <ant:echo>ID: ${id}</ant:echo> <ant:echo>GROUP: ${group}</ant:echo> <ant:echo>VERSION: ${version}</ant:echo> <ant:echo>TYPE: ${type}</ant:echo> <ant:echo>DEPNAME: ${depname}</ant:echo> <ant:echo></ant:echo> </j:forEach> </goal> I never found a listing of all the available variables that can be retrieved, but trial and error helped me determine those listed. For some reason, the dependencies are not accessed through the variable, ${pom.dependencies} as one would expect, but as you can see from the first j:set statement, they are accessed through ${pom.artifacts}. The rest of the variables are more straight forward in their naming. Once you have retrieved the variable you are after, you can assign the variable to another property, branch on a value, or do any other programming tasks that you'd expect. When I first started writing plugins, I would use the above two methods to retrieve variables and pass it on to Java classes, but I later learned that you can access what is called the 'Maven context' from the Java classes directly. The Maven context is essentially the environment that Maven is running in on a project, including it's variables. To do that, you need to import the MavenJellyContext and Project classes, pass in the Project with a setter from Jelly script, get it's context, and access it's variables: The instantiation of your plugin class: <ccscm:rewriteprojectfile See the project class that you pass in is just the ${pom} variable. The Java code in your class: import org.apache.maven.jelly.MavenJellyContext; import org.apache.maven.project.Project; private Project project; MavenJellyContext c = project.getContext(); The above code is for a list properties such as repository list properties (I got the idea from the artifact plugin) for retrieving property settings for dependencies and specified versions: ccScm.dep.list=framework,security ccScm.dep.ver.framework=1.44 ccScm.dep.ver.security=2.3 Properties are defined for each listed dependency. Obviously a scheme like this could be expanded for more properties per dependency or for other types of properties. See the ccscm:writeprojectfile plugin goal. Using Java: Since I started mentioning Java use from Jelly, but I haven't explained it elsewhere, I'll start here. Jelly scripting is used in both maven.xml files and the plugin.jelly file used when creating a new plugin, but I don't think it is possible to use Java classes as part of a maven.xml file as with a plugin. java classes can be instantiated and used within Jelly scripts, including maven.xml files(see below), but with plugins, the Java classes are part of the plugin. A plugin is started just like any other project, with project.xml and project.properties files, along with a src and xdocs directory in the top level of the project directory for the new plugin. Then a plugin.jelly file is created for the Jelly scripts and a plugin.properties file for default values used by the plugin code. The project.xml file should have the following build section: <build> <sourceDirectory>src/java</sourceDirectory> <resources> <resource> <directory>${basedir}</directory> <includes> <include>plugin.jelly</include> <include>plugin.properties</include> <include>project.properties</include> <include>project.xml</include> </includes> </resource> </resources> </build> The plugin.jelly file needs to define a bean for the Java class you will be using: <define:taglib uri="ccscm"> <define:jellybean name="createfilecatalog" className="org.concord.scm.cvs.CreateFileCatalog" method="run" /> </define:taglib> Multiple beans can be defined in the taglib section. Basically, the bean is being named and the class name is being defined. Notice the method; I believe 'run' is the standard naming convention, but this can be any method name. To call the bean, it is referenced by it's tag and name: <ccscm:createfilecatalog inputCatalog="${ccScm.status.file}" outputCatalog="${ccScm.catalog.file}" cvsRoot="${cvsroot}" cvsRsh="${env.CVS_RSH}" cvsServer="${ccScm.cvs.server}" cvsRepo="${ccScm.cvs.repo}" cvsModule="${ccScm.cvs.module}" cvsTagName="${ccScm.cvs.tagname}" verbose="${ccScm.verbose}" /> All of the properties listed need to have setters in the class as Maven will create a new instance of the class and call the setters with the same names(case-insensitive). Then the main method that was defined for the bean is called. Here's part of the Java bean: package org.concord.scm.cvs; import java.io.*; import java.util.ArrayList; import org.apache.oro.text.perl.Perl5Util; import org.jdom.Element; import org.jdom.Document; import org.apache.maven.util.MD5Sum; /** * The main bean to be called whenever we want to create a file * catalog from a cvs status file. * * @author <a href="mailto:eblack@concord.org">Eric Black</a> */ public class CreateFileCatalog { private String workingDir; private String inputCatalog; private String outputCatalog; private String cvsRoot; private String cvsRsh; private String cvsServer; private String cvsRepo; private String cvsModule; private String cvsTagName; private boolean verbose; public void run() throws Exception { : ; main class : : } /** * @param inputCatalog The input catalog file name to set. */ public void setInputCatalog( String inputCatalog ) { this.inputCatalog = inputCatalog; } : : More setters : : } The plugin.properties is especially helpful in that it is used for defining default values for the plugin. It isn't really necessary for the definitions to be listed if there is no default value, but it's a much better place to look that scrambling through the plugin.jelly file for varaible usage. The format is the same as the project.properties file. Jelly Core: The following example demonstrates the use of the "new", "arg", "invoke", and "invokeStatic" tags of the Jelly Core tag library. I needed the date stamps that Maven used for the SNAPSHOT artifacts, so I looked at the artifact plugin code and converted the following code from DefaultArtifactDeployer.java: public final static DateFormat SNAPSHOT_SIGNATURE_FMT = to this jelly scripting: 1: <j:new className="java.util.Date" var="timestamp" /> I know it looks ugly, but it's relatively simple. In the first line, the new tag is creating a new instance of a Date object and assigning it to the variable name "timestamp". The second line is doing the same thing for a SimpleDateFormat object that is assigned to the variable "format", but we are adding a parameter that is a String object and has that date format string. That is what the arg tag is doing in the third line. In the fifth line, we're using invokeStatic to create an object (rather than using the new tag) and call one of it's method's directly. To do that on objects that we created using the new tag, we use the invoke tag, which you see applied to the 'format' object on line 8 to set the time zone and on line 11 to format our timestamp. So, to summarize: new - creates a new java object and associates it with a jelly variable arg - an attribute of the new, invoke, and invokeStatic tags to send arguments to methods of an object invoke - a method call on a java object created from the new tag invokeStatic - a method call that invokes a static method on a class without instantating it This example is how to use the string variables in Jelly and demonstrates the way to use the core Jelly tags set, useList, forEach, the Ant fileScanner and fileset tags, and the Util file tag. It was a solution to getting the basename of a file and manipulating it in a list. <!-- Use the file scanner to get the names of the files --> <ant:fileScanner var="jnlpFileset"> <ant:fileset dir="${maven.jnlp.dir}"> <ant:include name="*.jar" /> </ant:fileset> </ant:fileScanner> <!-- Create a list since the fileScanner is not a static list --> <j:useList var="jarFileList" /> <!-- This is where we create a list of the filenames without the extension --> <j:forEach var="jnlpFile" items="${jnlpFileset.iterator()}" trim="yes"> <j:set var="index" value="${jnlpFile.name.lastIndexOf('.')}" /> <j:set var="basename" value="${jnlpFile.name.substring(0,index)}" /> <!-- Assigning is only done to prevent system output string --> <j:set var="dummy" value="${jarFileList.add(basename)}" /> </j:forEach> I'm not sure where the translation is from the file names from the fileset to an actual file java object, but it is required to be able to get the name with jnlpFile.name. When I wanted to do this on a single file that I already had the full path for, I had to create a file object directly using the Jelly Util file tag: <u:file var="catalog" name="${ccScm.catalog.file}" /> <j:set var="index" value="${catalog.name.lastIndexOf( '.' )}" /> <j:set var="catBasename" value="${catalog.name.substring( 0, index )}" /> <j:set var="catExtension" value="${catalog.name.substring( index )}" /> I could have just the set a variable to the file name instead, but then I would have had to worry about the file path and the path separators: <j:set var="test" value="${ccScm.catalog.file}" /> <j:set var="tindex" value="${test.lastIndexOf( '.' )}" /> <j:set var="tcatBasename" value="${test.substring( 0, tindex )}" /> <j:set var="tcatExtension" value="${test.substring( tindex )}" /> <j:set var="pathsep" value="/" /> <j:set var="tindex" value="${test.lastIndexOf( '/' )}" /> <j:if test="${empty(tindex)}"> <j:set var="tindex" value="${test.lastIndexOf( '\' )}" /> <j:set var="pathsep" value="\" /> </j:if> <j:if test="${!empty(tindex)}"> <j:set var="tcatPath" value="${tcatBasename.substring( 0, tindex )}" /> <j:set var="tcatBasename" value="${tcatBasename.substring( tindex )}" /> <j:set var="tcatBasename" value="${tcatBasename.replaceAll( pathsep, '' )}" /> </j:if> The substring for the basename will have the last path separator in it. In the last j:set line above, note I used replaceAll rather than just replace since it doesn't seem to be implemented. I don't know which string methods are implemented or not and I haven't been able to find a list for it, just painfully slow trial and error. Also, in the line before the replaceAll one, I initially was going to increment tindex to get past the path separator, but without creating a new Integer object, I couldn't figure out how to implement an arithmetic operation in Jelly. Jelly Ant: If you go to the above link, you will see the tags that are specific to Jelly for using Ant tags. In addition to this small list, most Ant tasks work in Jelly, but apparently not all as I have discovered during my scripting (the Ant library is version 1.5, which doesn't have all the current tasks). IMPORTANT: Ant properties can not be redefined once they are set, so use Maven properties where possible. This little example demonstrates the problem. If we try to use the Ant basename task, the only way to get the variable is to set the property, here named 'jBasename': <ant:basename property="jBasename" file="${jnlpFile}" suffix="jar" /> The problem is that it gets set once, but you can never reset the value of 'jBasename' through the rest of the script, so you couldn't use this task in a loop. To solve this problem when I came across it, I used a regexp mapper task, since I wanted to copy the basename of a file to the basename + date + extension (see mapper example). Later, I found an example that used a java.io.File instance that I described in the Jelly Core section. The echo task is the most used specified one of the following ways: <ant::echo message="Hello" /> or this way to specify format with whitespace: <ant:echo> ${jnlpFile} </ant:echo> The fileScanner task is a Jelly specific task and is useful for creating a fileset in a list: <ant:fileScanner var="jnlpFileset"> <ant:fileset dir="${maven.jnlp.dir}"> <ant:include name="*.jar" /> </ant:fileset> </ant:fileScanner> <j:forEach var="jnlpFile" items="${jnlpFileset.iterator()}" trim="yes"> <ant:echo> ${jnlpFile} </ant:echo> </j:foreach> Note: If you use this same file set later, it will do a re-read, so it does not hold static data The globmapper task doesn't work, use mapper task with the 'type' set to 'glob' (I assume this is true of all the mapping shortcut tasks): <ant:copy todir="${maven.jnlp.dir}"> <ant:mapper type="glob" from="*" to="*.bak"/> <ant:fileset dir="${maven.jnlp.dir}"> <ant:fileset include="*.jar"/> </ant:fileset> </ant:copy> or use the regexp mapper: <ant:mapper type="regexp" from="^(.*)\.jar$$" to="\1-${formattedDate}.jar"/> or slightly more complicated (have to include jnlp files in fileset): <ant:mapper type="regexp" from="^(.*)\.([jnlp|jar]+)$$" to="\1-${formattedDate}.\2"/> For some reason, the regexp count quantifiers {n,m}, {n,}, and {n} didn't work although they are implemented in Sun's java.util.regex and Apache's regexp and ORO apis used by Ant. I don't know how the Ant tasks are made available to Jelly, so maybe the Jelly implementation is faulty. Anyway, you can use Jelly:core tags such as forEach, if, and set to specify the Ant include parameter, but at least one include needs to be in the fileset or you may end up with a build error: <ant:copy todir="${maven.jnlp.dir}"> <ant:mapper type="glob" from="*" to="*.bak"/> <ant:fileset dir="${maven.jnlp.dir}"> <j:forEach var="artifact" items="${pom.artifacts}">
<ant:fileset
include="*.jar"/></ant:fileset> </ant:copy> I guess the Jelly Core tags are executed/parsed first to make this work. |
|||
All Contents Copyright
© 2005, The Concord Consortium.
All Rights Reserved. Privacy Policy
Last modified: Friday, 03-Jun-2005 16:48:10 EDT