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.
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.
Make a copy of the Java TextEdit project and open it in Xcode.
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.
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 deletedOpen the TextEdit folder in the Finder and delete the build folder.
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 the Resources folderCreate a new project in Eclipse and have it use the existing TextEdit files:
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.
Enter the project name as "TextEdit."
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.
Click Finish to create the project.
Screenshot of the New Java Project DialogThis 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:
Select the TextEdit project in the Package Explorer.
Choose Properties from the Project menu in the main menu bar.
Choose Java Build Path from the list of options and switch to the Libraries tab.
Click on the Add Class Folder button, and in the Edit Class Folder dialog choose Create New Folder.
Enter "Cocoa" as the folder name and click the "< < Advanced" button.
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.
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 dialogNow that everything is in place, it is time to create the build file.
|
Related Reading
Mac OS X for Java Geeks |
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
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
, and when you
open the file, the Ant Editor will be automatically invoked.
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.
Set the project name to "TextEdit" and the Description to anything you'd like.
Set the name of the first target to "build" and the depends value to "jar".
Save the changes.
If it's not already visible, show the Ant View by choosing Window -> Show View -> Ant.
Choose "Add Buildfiles..."
in the Ant View and
select the newly created build.xml file in the TextEdit
project. Double-click any of the targets to run them.
Click on the Hide Internal Targets button
to 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.
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>
Add a breakpoint to the first line of
applicationDidFinishLaunching() in
Controller.java.
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.
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.
Choose Remote Java Application from the list of configurations and click on New.
Enter "TextEdit" (or whatever you like) as the name of the configuration.
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..." dialogIf you'd like to have easy access to this debugger configuration, switch to the Common tab and check the option "Display in favorites menu".
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.
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.
All of the original entries should be copied exactly.
If the VMOptions entry does not already exist, then add it.
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.
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.
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.
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
in the main toolbar to re-run the TextEdit Remote-Java Debugger configuration.
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!
*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.