DOWNLOAD
 Oracle WebLogic Server
 Ant
   TAGS
java, weblogic, All
Developer: Java

Extending Oracle WebLogic Workshop 10 with Ant

by John Liptak

Learn how to extend the built-in features of Oracle WebLogic Workshop using a combination of generated and custom Ant build files.

Published July 2008

Oracle WebLogic Workshop has made major advances with version 10, particularly in the design-build-run development cycle. The productivity gains from using the publish capabilities of Workshop make the development process qualitatively better. Advanced products such as Oracle WebLogic Portal are very difficult, if not impossible, to use without the help of the IDE.

Unit testing (JUnit), continuous integration, and code coverage tools—made popular by Agile software development—have grown into being viewed as industry best practices for many development methodologies.

Being able to integrate customized command-line Ant capabilities is no longer optional for many development teams. You should not have to give up these incredibly valuable tools in order to use an IDE. The answer is not to choose one or the other but to use both techniques.

This article shows how to extend the built-in features of WebLogic Workshop using a combination of generated and custom Ant build files. Getting the best out of both of them is not difficult, requiring only a little additional work, planning, and the help of some of the newer features of Ant.

My minimal requirements for command-line use are the following:

  • The ability to use CruiseControl.

  • A unit test target that would test the entire application, not just a project. In addition to this target being necessary for CruiseControl, I wanted to continue the development practice of running all of the unit tests before checking into a shared branch in source code control.

  • The ability not to have to look at the generated or the custom build files.

  • The creation of test and production builds from the command line on a dedicated integration machine—not on a developer's personal workstation. Our build machines do not have the Workshop IDE installed; they have only the runtime support files.

  • The ability to continue to generate environment-specific files. As an in-house enterprise developer with an outsourced operations environment, it is important to us to automate the installation of new software versions.

Upgrading to WebLogic Workshop 10

When it came time to start our first WebLogic Workshop 10 application, there were enough changes from our older 8.1 applications that I had to discover new ways of integrating our build process. CruiseControl, in particular the junit task, was going to be a challenge.

Setting the classpath for the junit task with WebLogic Workshop 8.1 was fairly straightforward: Add the ./.workshop/output/<subproject> and all of our jar files to the classpath, and everything worked pretty well.

WebLogic Workshop 10's classpath structure is far more complicated. Figure 1 illustrates how the various components combine to make up the classpath.

Classpath Components
Figure 1. Classpath components

This new structure is far more functional. It allows a single project to have a single copy of a jar file in the APP-INF/lib. It also allows collections of shared libraries to be installed as WLS libraries.

There are obvious solutions to the problem, but they have major drawbacks:

  • Reverse engineer the complicated classpath generation done by Workshop. This is the least attractive option, since every time you add a project, facet, or library, you might find some part of the classpath generation you didn't account for. You also lose a lot of the productivity gains from your IDE if you have to reengineer every new feature you start using.

  • Edit the generated Ant scripts to add your additional targets. The problem with this solution is that you will have to re-edit the file every time you need to export the build.xml file.

I wanted to find a better way. The good news is, there is a better way, and once you know the principal technique, it's straightforward. Step 1 is to use Workshop to generate a base set of Ant build.xml files. I will review that procedure here so you can generate your own and follow along.

Exporting Workshop Ant scripts

Exporting your project to a series of Ant build files is documented here. In short, you need to export three things: the build file for the entire application (in an ear project); the build files for dependent library and web projects; and a metadata file.

The build.xml file generated for the ear project will call each of the sub-project's build.xml files. This will correctly build all of the necessary sub-projects and then assemble the ear file.

Now you have all of your generated build.xml files. Assuming that you don't like the obvious solutions for extending the build files, what should you do? I will describe using an Ant <import> pattern.

Adding an Application Project to the Workspace

The second step in supporting both the command-line Ant and the Workshop IDE approach is to make sure they don't step on each other's build files. Our team choose to create what we call the application project "above" the ear project in the workspace.

There are two main reasons for creating a separate application project. First, Workshop will not generate a build.xml file for that project, so we don't have to worry about Workshop generating any conflicting build instructions for the project. Second, it's a great place to put configuration files, WLST domain creation and configuration scripts, property files, deployment plans, and the like.

Because we have different applications being developed against different versions of Workshop, CruiseControl is configured to call a build.xml build script. Rather than have a different configuration for Workshop applications and those that pre-date the Eclipse-based version of Workshop, we can use our application project build script, which provides another level of redirection to resolve that conflict.

The Ant <import> Solution

Ant is a very popular build tool that has been evolving just like all of the other tools that we use. Version 1.6.5 introduced a new <import> feature that allows you to extend an existing build file. The usual explanation for this task is to allow you to refactor your build files and put the shared, non-changing definitions, properties, and tasks in a common file so that other build files can reuse them.

At the same time, I was refactoring some old code to use dependency injection with Spring, and it occurred to me that I could use the import feature to inject the generated build.xml into our build process.

That's exactly what we did:

  • Create a second build file

  • <import> the build file generated by Workshop:

              <import file="./build.xml" />
             
    


  • Use any of targets in the generated build file as targets:

    <target name="unitTest" depends="build">
      <echo>Did the imported build.  Moving on...</echo>
    ...
    
  • Use any of the properties and task types created by Workshop.

The ear project

With the exception of the target name unitTest and the name of the Ant file buildCustom.xml, the following is an exact copy of the contents of the generated file's build target for the ear project:
...
<for-each-project-in-build-order>
  <if>
    <not><equals arg1="${.project.name}" 
                    arg2="${project.name}"/></not>
    <then>
      <antex
        antfile="${.project.dir}/buildCustom.xml"
        dir="${.project.dir}"
        target="unitTest"
        inheritUserProps="false"
        inheritAll="false"
        inheritRefs="false">
        <property name="wl.home" 
                     value="${wl.home}"/>
        <property name="patch.home" 
                     value="${patch.home}"/>
        <property name="workspace" 
                     value="${workspace}"/>
        <property name="echo.metadata" 
                     value="${echo.metadata}"/>
        <property name="init.typedefs.executed" 
                     value="true"/>
        <propertyset>
          <propertyref regex="${path.vars.regex}"/>
        </propertyset>
      </antex>
    </then>
  </if>
</for-each-project-in-build-order>
...


This task does exactly what it looks like it does: It loops through all of the projects that the ear depends on and performs their unit tests.

Sub-projects

The ear sub-projects follow a similar pattern. Now that we have a unitTest target being called, we now need to define it:

<target name="unitTest" depends="build">
  <echo level="info" >
    Did the imported build.  
    Moving on...
  </echo>
  <for-each-java-src-path>
    <if>
      <available file="${.java.src.dir}"/>
      <then>
      <echo  level="info" >
        JUnit tests for ${.java.src.dir} 
        built to directory ${.java.src.output} 
       </echo>
         <junit printsummary="yes" 
                  haltonfailure="on" 
                  reloading="false">
              <classpath>
                <path refid="java.classpath"/>
              </classpath>
              <formatter type="plain" 
                            usefile="false"/>
              <formatter type="xml"/>
              <batchtest 
                todir="../wls10app/build/unitTest">
                  <fileset 
                    dir="${.java.src.output}" 
                    includes="**/*Test.class"/>
              </batchtest>
          </junit>
    </then>
  </if>
 </for-each-java-src-path>
 ...

Back in our application project, we can call the Workshop generated build target like this:

    <target name="build" depends="prepare">
        <ant dir="${earProject.dir}" 
          antfile="build.xml"
            target="build" inheritRefs="false" />
    </target>

And our newly created custom target like this:

    <target name="unitTest" depends="prepare">
        <mkdir dir="./build/unitTest" />
        <ant dir="${earProject.dir}" 
          antfile="buildCustom.xml"
            target="unitTest" inheritRefs="false" />
    </target>

Continuous Integration with CruiseControl

CruiseControl is a popular open-source continuous integration framework. CruiseControl will monitor your source-code control system and perform a build, run tests, and publish any results after each check-in. This dramatically improves your development process because everyone sees the status of the source code. As such, it serves as both a positive and negative process control. Everyone knows who is checking in code, writing unit tests, breaking the build, and fixing the build. It becomes impossible for a developer to say that they are 80-percent complete with a task if they have not checked in any code or unit tests.

In my workplace, we have been using CruiseControl since version 1. We have also adopted a build label pattern of <release name>-<iteration number>-<build number> so that the combination of all of the build notices gives a temporal overview of what has changed in the code base. That same information is available in the reporting server built into CruiseControl.

CruiseControl has advanced since that article was written, so here is a sample configuration file that integrates multiple applications using the project plug-in. It allows us to define the general process of checking for changes in source code control, building, testing, and publishing the results of a build process once. We can then control multiple applications by simply accouning for what is different about each application:

<cruisecontrol>
  <property environment="env" toupper="true" />

  <property name="reportdir"  
            value="${env.CCDIR}/webapps/cruisecontrol"/>
  <property name="projectdir" 
            value="/projects/javaapps/${project.name}"/>
  <property name="testdir"    
            value="${projectdir}/build/unitTest"/>
  <property name="logdir"     
            value="/projects/javaapps/buildlogs/${project.name}" />

  <property name="cvs_tag"    
            value="HEAD"/>

  <property name="builder.name"  
            value="John Liptak" />
  <property name="builder.email" 
            value="John.Liptak@companyname.com" />
  <property name="builder2.email" 
            value="Other.Person@companyname.com" />
  <property name="builder3.email" 
            value="Other.Person2@companyname.com" />

  <plugin name="cvs" 
          cvsroot="your cvs root here"
          module="${project.name}" 
          property="fileschanged"
          propertyondelete="filesdelted"
          tag="${cvs_tag}" />

  <plugin name="ant" 
          antscript="/projects/javaapps/app2/buildcc.bat" 
          antWorkingDir="${projectdir}"
          target="CCbuildAndTest"
          uselogger="true"
              usequiet="true">
              <property name="cvs_tag" value="${cvs_tag}" />
  </plugin>

  <plugin name="htmlemail"
            buildresultsurl="http://yourCCserver/${project.name}"
            mailhost="mailgate.qintra.com"
            returnaddress="${builder.email}"
            returnname="${builder.name}"
            subjectprefix="[BUILD ${project.name}]"
            defaultsuffix="@companyname.com"
            reportsuccess="always
            spamwhilebroken="true"
            xsldir="${reportdir}/xsl"
            css="${reportdir}/css/cruisecontrol.css" />

  <plugin name="currentbuildstatuslistener"
            file="${logdir}/buildstatus.html"/>

  <plugin name="labelincrementer" classname="net.sourceforge
.cruisecontrol.labelincrementers.CVSLabelIncrementer"/>

  <plugin name="project.apptype1.type" 
          classname="net.sourceforge.cruisecontrol.ProjectConfig">
      <bootstrappers>
         <cvsbootstrapper 
         localWorkingCopy="/projects/javaapps/${project.name}" 
         file="build.xml" />
      </bootstrappers>
        <listeners>
            <currentbuildstatuslistener/>
        </listeners>
        <log dir="${logdir}" encoding="ISO-8859-1">
            <merge dir="${testdir}"/>
        </log>
        <modificationset quietperiod="60">
            <cvs/>
        </modificationset>
        <schedule interval="3600">
            <ant/>
        </schedule>
        <publishers>
            <htmlemail logdir="${logdir}" >
                <always  address="${builder.email}"/>
                <failure address="${builder.email}"/>
                <always  address="${builder2.email}"/>
                <failure address="${builder2.email}"/>
            </htmlemail>
        </publishers>
   </plugin>

  <project.apptype1.type 
      name="app1" buildafterfailed="false">
  </project.apptype1.type>
  <project.apptype1.type 
      name="app2" buildafterfailed="false">
  </project.apptype1.type>
  <project.apptype1.type 
      name="app2" buildafterfailed="false">
  </project.apptype1.type>
  <project.apptype1.type 
      name="app3" buildafterfailed="false">
  </project.apptype1.type>
  <project.apptype1.type 
      name="app4" buildafterfailed="false">
  </project.apptype1.type>
  <project.apptype1.type
      name="wls10app" buildafterfailed="false">
      <property name="builder2.email" 
                   value="Another.Person@companyname.com" />
      <log dir="${logdir}" encoding="ISO-8859-1">
            <merge dir="${projectdir}/appdir/build/unitTest" />
      </log>
  </project.apptype1.type>
  <project.apptype1.type name="app5" buildafterfailed="false">
      <property name="cvs_tag" value="branch_name" />
      <property name="builder2.email" 
                value="John.Liptak@companyname.com" />
      <log dir="${logdir}" encoding="ISO-8859-1">
            <merge dir="${projectdir}/builds/unitTest" />
      </log>
  </project.apptype1.type>
</cruisecontrol>


A critical portion of the Ant task definition is specifying the destination for the output in a place where CruiseControl expects it to be. In this example, ../wls10app/build/unitTest is the common location for all of the unit test output.

It's important to have the merge directory, ${projectdir}/builds/unitTest, in the CruiseControl configuration match the value in the build file so that reporting includes all of the unit tests. This allows the unit tests of all of the sub-projects to be combined into a single report from CruiseControl.

Another important configuration issue is the Ant target that CruiseControl will call—in my case CCbuildAndTest. Choosing a good location for that target is also important.

Generating Environment Specific Files (using <macrodef>)

We use two steps to generate our environment-specific deployment files. Because Ant does not allow the user to redefine properties once they are set, we create properties for each supported environment with a unique name prefix. For example, buildEnv.test1.JAVA_HOME might have a value of /opt/bea/home10.0.1/jrockit_150_11. Rather than repeat all of the property, fileset, and other variables assignments over and over again in our build file, we use the <macrodef> feature of Ant:

<target name="envTokenFilters" 
        depends="tokenFilter">
  <macrodef name="env.macro">
    <attribute name="envFile" />
    <attribute name="envSuffix" />
    <sequential>
       <!-- Define properties per environment -->
       <property 
          name="buildTypeFile.@{envSuffix}"
          location="${config.env}/@{envFile}.properties" />
       <dirname 
          property="buildTypeFile.@{envSuffix}.dirname"
          file="${buildTypeFile.@{envSuffix}}" />
       <basename 
          property="buildTypeFile.@{envSuffix}.basename"
          file="${buildTypeFile.@{envSuffix}}" />
       <available 
          file="${buildTypeFile.@{envSuffix}}"
          property="buildTypeFile.@{envSuffix}.present"/>
       <property 
          file="${buildTypeFile.@{envSuffix}}" 
          prefix="buildEnv.@{envSuffix}" />

       <!-- Define token filterset per environment -->
       <filterset id="tokenFilter.@{envSuffix}" 
          begintoken="@" endtoken="@">
           <filtersfile 
             file="${commonBuildPropsFile}"/>
           <filtersfile 
             file="${buildTypeFile.@{envSuffix}}" />
       </filterset>

       <!-- Define filelist per environment -->
       <filelist id="tokenFilterFiles.@{envSuffix}"
          dir="${buildTypeFile.@{envSuffix}.dirname}"
          files="${buildTypeFile.@{envSuffix}.basename}" />
       </sequential>
   </macrodef>

   <env.macro envFile="devUnix" envSuffix="dev"/>
   <env.macro envFile="integration" envSuffix="int"/>
   <env.macro envFile="integration2" envSuffix="int2"/>
   <env.macro envFile="itv" envSuffix="test"/>
   <env.macro envFile="itv2" envSuffix="test2"/>
   <env.macro envFile="itv3" envSuffix="test3"/>
   <env.macro envFile="e2e" envSuffix="e2etest"/>
   <env.macro envFile="production" envSuffix="prod"/>

</target>


This is followed by a similar target (all of the dependency checking is removed for space reasons, but you would want that in a real build script) that does the mapping for each of the environment types to generate the output files:

<macrodef name="copyPropertyFiles.macro">
    <attribute name="envSuffix" default=""/>
    <sequential>
        <!-- Perform token replacements -->
        <copy overwrite="true" 
            todir="${dist.config}" >
            <fileset 
                dir="${config.dir}/properties">
                <patternset 
                    refid="propertyFiles" />
            </fileset>
            <filterset 
                refid="tokenFilter@{envSuffix}" />
            <mapper type="glob" 
                from="*" 
                to="*@{envSuffix}"/>
        </copy>
        <!-- A real build file has many copies of this 
               copy block. -->

        </macrodef>

        <copyPropertyFiles.macro/>
        <copyPropertyFiles.macro envSuffix=".dev"/>
        <copyPropertyFiles.macro envSuffix=".int"/>
        <copyPropertyFiles.macro envSuffix=".int2"/>
        <copyPropertyFiles.macro envSuffix=".test"/>
        <copyPropertyFiles.macro envSuffix=".test2"/>
        <copyPropertyFiles.macro envSuffix=".test3"/>
        <copyPropertyFiles.macro envSuffix=".e2etest"/>
        <copyPropertyFiles.macro envSuffix=".prod"/>


This way, all of the environment-specific files can be generated into a single build. This makes both the testers and the operations people happy because there is a single build for all of the environments.

If you add the same processing to WLST scripts to generate and configure your domain, you can get very close to a touchless install process. Other than a manual step to input the various WebLogic, database, and other credentials to keep your SOX auditors at bay, there should be almost nothing to do by hand for an install.

Limitations

This solution does have a few limitations that you need to take into consideration:

  • Duplicate class names in different projects will confuse the reporting from CruiseControl. This is not a significant limitation because it would confuse the developers too, so we just don't allow that in our projects.
  • All of our JUnit classes end in Test. We chose this so that the test classes would show up next to their definition in the navigator view. Other people like having an entire source tree with the same directory structure so that the test classes look like they are in the same package but can be excluded from production builds.
  • Concurrent processing of custom Ant tasks is not supported by Oracle at this time. Some Ant tasks assume that they have exclusive access to your temp directory and will fail if more than one build is executing, even from a different workspace and/or project.

Conclusion

This article showed you how to extend the generated build files from WebLogic Workshop to your advantage using Ant's <import> and <macrodef> features. The <import> technique shows how to generate the necessary classpath for using the <junit> task. Other classpath-based processing can be integrated with this technique.

Creating an application project that is above the ear project in the build process allows us to integrate CruiseControl. It also provides a place for environment specific files—such as property files, WLST scripts, and deployment plans—to be generated.

Careful setup of build processes gives you both the rich feature set that comes from using command-line Ant and the advanced IDE features found in WebLogic Workshop. John Liptak is a senior software engineer with Qwest who has been developing in Java since 1997.