MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Building Cocoa-Java Apps with Eclipse

by Mike Butler
04/22/2005

Eclipse is a gloriously powerful, open source IDE*1, which is a joy to use when working with Java. Xcode, on the other hand, comes free from Apple, but doesn't quite like having to deal with Java--they get on fine together, but Xcode just doesn't go out of its way to make it fun.

Xcode's first love is Objective-C, and it shows.

It makes sense, then, when writing Java based Cocoa Applications, to use Eclipse. But how? What does Eclipse know about the esoteric world of Cocoa-Java? Well, with a little help from Ant, the flexible build system, you can tell it everything it needs to know.

This article will detail a simple method of building, and debugging, bundled Cocoa-Java applications that doesn't rely on keeping synchronized Xcode and Eclipse projects. Due to limitations on space, I'm assuming that you are comfortable with all of the referenced technologies--Eclipse, Ant, Cocoa-Java, etc. Having said that, it's straightforward enough.

Starting Off

To keep things simple, we'll be working with the sample project, TextEdit, in the Java AppKit examples folder, as installed with the developer tools.

As mentioned before, Eclipse knows nothing about Cocoa-Java and Objective-C. One easy way to get around this is to use the command-line tool xcodebuild to generate the application bundle and compile the Objective-C main stub. Everything else can be done as normal by Ant and Eclipse--Java files processed and .jar'ed, and the application bundle populated with all of the necessary resource files. Using this method means that we don't have to keep an Xcode project in sync with the Eclipse project. Xcode will not be dealing with anything other than the Objective-C files.

Get the Xcode Side of Things Ready

  1. Make a copy of the Java TextEdit project and open it in Xcode.

  2. Instead of writing an Info.plist by hand, simply build the project and take Info.plist from the newly produced app in the build folder (build/TextEdit.app/Contents/Info.plist). Copy the info.plist to the main project directory where all of the source code is stored.

  3. Before closing Xcode, remove all of the non-Objective-C files *2--remember that Eclipse and Ant are going to look after everything else.

    Screenshot of the Project in Xcode after everything is deleted Screenshot of the project in Xcode after everything is deleted
  4. Open the TextEdit folder in the Finder and delete the build folder.

  5. Create a folder called Resources and move all of the required images, icons, nibs and lprojs into it--basically anything that should be in the /Contents/Resources folder of the bundled app.

    ScreenShot of files in Resources Folder Screenshot of files in the Resources folder

Getting Started with Eclipse

Create a new project in Eclipse and have it use the existing TextEdit files:

  1. From the menu bar, choose File -> New -> Project and select Java Project from the list of options. This will bring up the New Java Project Wizard.

  2. Enter the project name as "TextEdit."

  3. Choose "Create project at external location" and browse to the TextEdit folder. At this stage, Eclipse will indicate that it knows that there are files already available in this folder and that it will set itself up accordingly.

  4. Click Finish to create the project.

    Screenshot of the New Java Project Dialog Screenshot of the New Java Project Dialog

This will produce a project containing all of the files in the TextEdit folder. As only the Java files (and a few others, such as the Info.plist) are of interest inside of Eclipse, it may be useful to just turn on filtering in the Package Explorer to clean up the display a bit.

Open Controller.java and you will instantly notice that hundreds of errors are highlighted. At this point, Eclipse knows nothing about Cocoa, and therefore cannot find any of the Cocoa types referenced in Controller.java.

Tell Eclipse where to find the Cocoa classes:

  1. Select the TextEdit project in the Package Explorer.

  2. Choose Properties from the Project menu in the main menu bar.

  3. Choose Java Build Path from the list of options and switch to the Libraries tab.

  4. Click on the Add Class Folder button, and in the Edit Class Folder dialog choose Create New Folder.

  5. Enter "Cocoa" as the folder name and click the "< < Advanced" button.

  6. The advanced options allow you to link the new folder to an existing folder. Click on the "Link to folder in the file system" option and enter /System/Library/Java as the path.

  7. Hit OK to finish each of the nested dialogs that we've brought up. After a second or two, all of the errors should disappear, as Eclipse locates all of the Cocoa references.

    Screenshot of the Add Classes Folder dialog Screenshot of the Add Classes Folder dialog

Now that everything is in place, it is time to create the build file.

Related Reading

Mac OS X for Java Geeks
By Will Iverson

Eclipse comes bundled with Ant--a build system written in Java that is controlled via XML-based files. A dedicated editor is provided to help generate and edit the build files. You may find it useful to have the reference section of the Ant manual, which is part of the developer tools, open in your browser.

Getting Started with Ant

  1. Create a new file in the TextEdit project and call it build.xml, which is the default name for an Ant build file. The newly created file will display an Ant as part of its icon Ant File Icon, and when you open the file, the Ant Editor will be automatically invoked.

  2. Open the build.xml file and choose Content Assist from the Edit menu (normally mapped to the key combination Command-Space). Content Assist will display the available editing options for any given situation. Choose the Buildfile Template to create the stub of a new build file.

  3. Set the project name to "TextEdit" and the Description to anything you'd like.

  4. Set the name of the first target to "build" and the depends value to "jar".

  5. Save the changes.

  6. If it's not already visible, show the Ant View by choosing Window -> Show View -> Ant.

  7. Choose "Add Buildfiles..." Add Build Files Button Imagein the Ant View and select the newly created build.xml file in the TextEdit project. Double-click any of the targets to run them.

  8. Click on the Hide Internal Targets button Hide Internal Targets Buttonto view only the more interesting public targets.

You now have the skeleton of an Ant build file with a couple of targets defined and executable from the Ant View. Unfortunately, we don't have enough space to go through the build file line by line, so in the popular tradition, here's one we made earlier, along with its property file *3.

All of the targets in the build file are the normal targets that you'd find in any Ant build file; they clean, compile and .jar as you'd expect. The more interesting targets are described below.

<!- - - - - - - - - - - - - - - - - - - 
	target: createAppPackage  - 
	Create the Application bundle if isn't already there
- - - - - - - - - - - - - - - - - - ->
<target name="createAppPackage" unless="packageAlreadyExists" 
	depends="checkIfPackageExists" >
	
	<exec executable="xcodebuild" />
	<delete dir="${build.dir}/TextEdit.build"/>
	<delete file="${app.info.plist}" />
	
</target>

The createAppPackage target is only executed if TextEdit.app doesn't already exist in the build folder. If it's not there, the command-line tool xcodebuild is called to create the application bundle, compile the Objective-C main, and produce the executable stub in TextEdit.app/Contents/MacOS. In other words, it automatically does the hard part for us. The last operation of this target is to remove the automatically generated Info.plist. Later on in the build, a new Info.plist file will be copied into the correct location.

Sometime after the application bundle has been constructed, the populateBundle target is called. This target has only two simple jobs--copy in the new Info.plist and copy in the contents of the Resource folder.

<target name="populateBundle">
	<copy file="Info.plist" tofile="${app.info.plist}" />
	<copy todir="${app.resources.dir}">
		<fileset dir="Resources"/>
	</copy>
</target>

The .jar target is the final target that we'll take a look at. The only difference between this and the standard .jar target is that the resulting .jar file, TextEdit.jar, is stored in the Contents/Resource/Java folder of the application bundle.

<!- - - - - - - - - - - - - - - - - - - 
	target: jar  - 
	build the Jar file and store it in the Java folder
	in the Application Bundle
- - - - - - - - - - - - - - - - - - ->
<target name="jar" depends="compile">
	<jar destfile="${app.java.dir}/TextEdit.jar"
       basedir="${classes.dir}"></jar>
</target>

That's it--we have a completed build system that allows us to build a Cocoa-Java app from within Eclipse. Double-click the build file in the Ant View to produce an executable version of TextEdit. Run the build file a number of times after editing some of the Java files and notice that none of the steps are repeated unnecessarily. And finally, add a few new Java files to the Eclipse project and have them automatically included in the build without having to touch the Xcode project.

Debugging Cocoa-Java Applications with Eclipse

Eclipse provides an excellent Java debugger with features that Xcode can only dream of at the moment. Luckily, it only requires a couple of VMOptions in the Info.plist to make use of this debugger.

The VMOptions we need to provide are the same options that appear in the debugger console when the Xcode debugger is first launched.

-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,address=8000

These options tell the VM running the Cocoa-Java app to wait for a debugger on a particular socket. Once the VM is waiting, we just have to tell the Eclipse bugger to go talk to that socket.

Getting Started

  1. Add the VMOptions to the Info.plist file. *4

    <key>Java</key>
    <dict>
    	<key>VMOptions</key>
    	<string>
        -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y
        address=8000
      </string>
    </dict>
  2. Add a breakpoint to the first line of applicationDidFinishLaunching() in Controller.java.

  3. Perform a clean build and then run the application. If the options have been added correctly, the app will just bounce in the dock indefinitely as it waits for the debugger.

  4. From the main menu bar in Eclipse, choose "Run -> Debug...". This will bring up a dialog allowing you to generate and save a debug configuration.

  5. Choose Remote Java Application from the list of configurations and click on New.

  6. Enter "TextEdit" (or whatever you like) as the name of the configuration.

  7. In the Connect options tab, enter "TextEdit" as the project. The defaults for Host and Port should be fine at localhost and 8000.

    Screenshot of the 'Debug...' Dialog Screenshot of the "Debug..." dialog
  8. If you'd like to have easy access to this debugger configuration, switch to the Common tab and check the option "Display in favorites menu".

  9. Finally, click on the Debug button. The app should stop bouncing in the dock, Eclipse should switch to the Java perspective, and the whole thing should come to halt at the breakpoint that we set earlier in Controller.java.

Automating Build-And-Debug

Obviously, it wouldn't be much fun going through all of those steps every time we wanted to debug our application. Let's extend the build file to automatically add the VMOptions for us.

The Info.plist file is simply an XML file; it's not particularly well designed, but it's still an XML file. The easiest way to manipulate XML is with XSLT--a language specifically designed for this purpose. Don't worry if you are not familiar with XSLT; there's plenty of help freely available on the internet and we'll keep the script relatively simple.

First, we'll add a couple of new targets to the build file.

<target name="buildAndDebug"
        depends="build, addDebugInfo, run"
        description="--> Build a debugable version">
</target>

This is the main public target that will be used to run a debug-friendly version of TextEdit. It builds upon the results of the normal build target, but goes a little further by invoking the two new targets, addDebugInfo and run.

<target name="addDebugInfo" >
	<xslt style="addDebugInfo.xsl"
        in="Info.plist"
        out="${app.info.plist}" />
</target>

This is the target that gets most of the work done. It passes the original clean Info.plist file through an XSLT script called addDebugInfo.xsl, which is stored in the root directory of the project. The output of the XSLT script overwrites the Info.plist file inside of the application bundle.

<target name="run" >
	<exec executable="open">
		<arg value="-a"/>
		<arg value="${app.path}"/>
	</exec>
</target>

Finally, the run target. This simple target uses the command-line tool open to launch the application that we've just built.

That's the build file complete. Now let's take a look at the XSLT script. First, some background on XSL. XSL stands for Extensible Stylesheet Language. It is made up of XSLT, which performs the transformation of an XML document to another format; XPath, for referencing sections of an XML document; and XSL-FO for describing display information. XSL, unlike C or Java, is not a procedural language. An XSLT script consists of a series of rules defining how particular patterns should be dealt with. When the script is invoked, the XML tree of the document being transformed is traversed and the most appropriate rule is applied to each node of the tree.

For the purpose of adding debug options to the Info.plist file, we want to perform the following rules. It's worth noting that these rules do not cover every possible situation, but they do provide a good starting point.

  1. All of the original entries should be copied exactly.

  2. If the VMOptions entry does not already exist, then add it.

  3. Existing VMOption entries should be preserved, and debug options added if not already present.

The completed XSL script can be found here and what follows is a description of the templates that directly perform the rules described above.

  1. All of the original entries should be copied exactly.

    <xsl:template match="node()">
      <xsl:copy>
        <xsl:copy-of select="@*" />
        <xsl:apply-templates />
      </xsl:copy>
    </xsl:template>
    

    This is a common operation in XSL scripts. The template (or rule) above can be applied to any node (match=node()) that doesn't have a more specific rule defined for it. The template makes a copy of the current node (xsl:copy) along with all of its attributes (xsl:copy-of select="@*"). The flow of the script continues on to process each of its children via the xsl:apply-templates.

  2. If the VMOptions entry does not already exist, then add it.

    <xsl:template match="dict">
     
     <xsl:copy> 
      <xsl:copy-of select="@*" />
      <xsl:if test = 
      "generate-id() = $firstDictID and 
       false() = (//key='VMOptions')">
        <key>Java</key>
        <dict>
          <key>VMOptions</key>
         <string><xsl:value-of select="$debugCommand"/></string>
        </dict>
      </xsl:if>		
      <xsl:apply-templates/>
    </xsl:copy>
    </xsl:template>

    Each Info.plist file contains a <dict> element as a child of the root <plist> element. This dict element ultimately holds all of the information in the Info.plist.

    The template above copies dict elements (match="dict") in the same manner as the first template. However, it also checks to see if the current node is the root dict element (if test="generate-id() = $firstDictID). If it is the root dict element, a check is made to see if there are any <key> elements whose value is VMOptions ( and false() = (//key='VMOptions')"). If no VMOption can be found, then the block of code that we previously added by hand (<key>Java</key>...) is inserted into the Info.plist file.

  3. Existing VMOption entries should be preserved, and debug options added if not already present.

    <xsl:template match="string">
      <xsl:copy>
       <xsl:value-of select="."/>
    
    <xsl:if test="preceding-sibling::*[1] = 'VMOptions' and
            false = contains(., '-Xdebug' )">
         <xsl:text> </xsl:text>
    <xsl:value-of select="$debugCommand"/>
       </xsl:if>
    
     </xsl:copy>	 	
    </xsl:template>

    The value for VMOptions is stored in a <string>. For normal string elements, this final template copies the string (xsl:copy) and its contents (xsl:value-of select=".").
    If the preceding key element had a value of VMOptions (if test="preceding-sibling::*[1] = 'VMOptions'), then this node is the string node associated with a VMOptions key--so try to add the debugger options. However, the debugger options only need to be added if they are not already present, so first a quick check to see if the -Xdebug option is already present(and false() = contains(., '-Xdebug' )). If -Xdebug wasn't found, then the debugger options are appended to the existing options (xsl:value-of select="$debugCommand"), separated by a single space (<xsl:text> </xsl:text>).

And that's it! After the XSL script has been downloaded, and the new build targets added to build.xml, perform a clean before running the buildAndDebug target. As before, the TextEdit icon should just bounce in the dock as it waits for the debugger to connect. Now, as TextEdit was the last thing to be debugged, you can simply click the Debug icon Debug Icon in the Toolbar in the main toolbar to re-run the TextEdit Remote-Java Debugger configuration.

Final Thoughts

To sum up, we've covered building a Cocoa-Java app in Eclipse using Ant. Once the build file is in place, it takes little or no maintenance effort and can be used to build other Cocoa-Java apps just by editing the properties file.

We've also covered how to debug a Cocoa-Java application using Eclipse's excellent Java debugger. Again, the XSL script is not tied to the sample project and can be used to work with most Cocoa-Java apps.

If you haven't tried Eclipse before, you'll be amazed at how enjoyable it is. Try it, and you'll never want to go back!

Notes

*1: Actually, it's much more than just an IDE, but that's beyond the scope of this article.

*2: If you build the project without the Java files and try to use the Info.plist generated, now it won't be as useful. As soon as you remove all of the Java files, Xcode will mark the Info.plist as not needing Java. You can add the missing fields by hand or by setting the Needs Java, Root Directory, and Path options of the Cocoa Java Specific section of the target.

*3: The property file should simply be stored in the same folder as the build file.

*4 : If you are using an intelligent XML editor to edit the Info.plist file, it is imperative that the debug options do not get automatically wrapped onto a second line, as they may not be interpreted correctly.

Mike Butler worked for 7 years as a software developer with Apple and has just recently launched a Mac Localisation Tools Development company called TripleSpin.


Return to MacDevCenter.com.

Copyright © 2009 O'Reilly Media, Inc.