Runtime Issues

Successful compilation! Are we done yet? There's one more thing we need to do to successfully run the project in Xcode, but don't take my word for it. Go ahead and run it to see for yourself.

To your dismay, you've noticed that the app fires up like a champ and you can still process text files normally, but PDF files don't work. You'll get an error message similar to the following: +[NSArray arrayWithObject:]: attempt to insert nil. Not exactly the most informative error message you've ever seen, but it can all be worked out.

If you look back to the new code you inserted in AppController.m, you see that there's one place where you pass the message arrayWithObject: to an NSArray, but you're not passing nil to it; you're passing an NSString returned from a pathForResrouce: ofType: message, right? Well, the problem is that there's no JavaClasses.jar resource in your application bundle. If you check all of the subfolders under Targets in Xcode's left-hand pane, you won't see it there, and this verifies the problem. To fix this problem, simply drag JavaClasses.jar from above in the Products folder down into the Bundle Resources folder.

That's all done, but after another build and run, there's some exception spewage that contains the line "java.lang.NoClassDefFoundError" when you try to process a PDF file. Compilation works, so this must be related to the Java runtime. Specifically, the JVM cannot find the PDFBox JAR files referenced. Let this be an illustration in the differences between compile time and runtime. Since, at the moment, we only want to run the application in Xcode, you can specify the CLASSPATH to point to the JAR files by double-clicking PodReader under Executables in Xcode's left-hand pane to reveal the PodReader Info window. Choose the Arguments tab and set the CLASSPATH in the environment to point to your two PDFBox-related JAR files that Xcode copied into your project directory. Separate their paths with a colon.

How to set runtime environment variables during development in Xcode

You can now successfully run your application. You'll get a couple of warnings from log4j, but those are PDFBox-related and we don't need to resolve them for our project. Depending on the type of PDF file you extract (like a white paper with a lot of formulas), you might get some characters that are garbage. If you're really set on removing those characters, you have three options: do it by hand, modify PDFBox to do it as it is extracting the text, or use Objective-C to do it once the text is extracted. This "garbage" can't necessarily be filtered out by PDFBox in general, because "garbage" is very context-dependent, especially with Unicode character sets.

Deploying the Application

If you only plan on using your PodReader from within the confines of Xcode, then you're all done and can quit for the day. If you plan on deploying the application so that you can place it on your desktop or in your Applications folder, there's still work to be done.

For a deployable application to be "OS X-like," it needs to be able to stand alone and not require a lot of manual intervention by the user. This requires a few more modifications to what we have working in Xcode. For starters, right-click on the blue project icon in the top of the left-hand pane, choose the Styles tab, and change the active build style to Deployment. This changes the linking for the executable, among other things. Clean and rebuild each of the targets under the Build menu, and verify that your project still runs.

Now go into your project's build directory using Terminal and launch your project outside of the protective veil of Xcode by typing open PodReader.app. Try to extract text from a PDF document and note the response you get: the Copy It button quickly loses focus and the progress indicator continues spinning. Even after you return from your haircut, it's still spinning.

How to change the build style from Development to Deployment

At this juncture, things are difficult to troubleshoot. But wait, there's Console.app, which keeps logs of application output and other interesting tidbits. In Terminal, type open /Applications/Utilities/Console.app and look to the bottom. It's the same exception spewage about java.lang.NoClassDefFoundError that we saw before. In Xcode, we could just specify the CLASSPATH in the project settings and be done with it. If you really wanted to be cheesy, you could distribute the PDFBox JAR files along with your application and require that all users of it set their CLASSPATHs to find them. If you're the only one using it, that would be fine for you to do as a one-time thing, but there's a better way. Keep reading!

As a great alternative to the Cheez Whiz approach, we'll embed these JAR files in your application bundle and have them be completely transparent to you and any other user. All you need to do to make them part of your bundle is to drag each of them to the Bundle Resources folder in the left-hand pane of Xcode, but that's only half of the solution. The JVM still needs a reference to them in the CLASSPATH. A few unsuccessful attempts you can suffer through to define your CLASSPATH include trying to load them the same way you load your JavaClasses.jar file in your AppController.m file, or by setting LSEnvironment values in your application's Info.plist that you can find in Xcode. A nice way to remedy the situation, however, is through what some call a prelude script.

Create a Prelude Script

In short, we're going to have the application loader execute a script to set the CLASSPATH environment variable to point to the application's embedded JAR files at runtime and then transfer control to the binary executable. This is actually pretty simple, but you'll need to understand a rough sketch of how an application loads and runs to fire that synapse that's flickering.

Before you proceed, skim over Bundles to understand how a bundled application works. Now understanding the anatomy of a bundle, we can change the value of CFBundleExecutable in Info.plist (located under Resources in Xcode's left-hand pane) to execute a shell script that points the CLASSPATH to the JAR files located in PodReader.app/Contents/Resources and then turn control over to the PodReader binary residing in PodReader.app/Contents/MacOS that would normally have been executed. All in all, this takes care of our problem.

In Xcode, copy the JAR files related to PDFBox into the Bundle Resources folder if you haven't done so already. Next, open up Info.plist and change the value PodReader for the key CFBundleExecutable to Prelude. Now, the application loader will attempt to find Prelude and execute it. To generate and set correct permissions for the Prelude script with each build, do the following:

So far, you've told Xcode that at the very end of the build phase for the target PodReader, it should copy the file prelude.sh (currently undefined) to the PodReader.app/Contents/MacOS directory of the bundle and set its permissions to be executable. All that remains is to define prelude.sh. From the main menu, choose File -> New File -> New Empty File in Project and name the file "prelude.sh". Make sure both boxes are unchecked for the targets PodReader and JavaClasses. Finally, move prelude.sh down to the Other Sources folder, and define its body as:

#!/bin/bash
#get the current working directory
#$0 is this script's absolute filename, like: /dir/dir/dir/file
#the % operator specifies to remove the final / and everything following it,
# leaving, in this example, /dir/dir/dir.
#`pwd` does not accomplish this same effect.
here="${0%/*}"
#the bundle's executable
cmd=PodReader
#required jar files are bundled in the app, so append them to the CLASSPATH
export CLASSPATH=$CLASSPATH:$here/../Resources/log4j-1.2.8.jar
export CLASSPATH=$CLASSPATH:$here/../Resources/PDFBox-0.6.7a.jar
#the output of "echo" appears in Console for debugging purposes if you need it
#echo "classpath is $CLASSPATH"
#surrender control to the bundle's product now that CLASSPATH is set
#for more info on any bash commands type "man commandName" in Terminal
exec $here/$cmd

Success at Last

At this point, clean all targets from the Build menu, and rebuild JavaClasses and then PodReader. You should now have a fully deployable application that runs via double-clicking or the open PodReader.app approach in Terminal. If you still experience troubles, use Terminal to go inside of the application bundle and ensure that Info.plist is set correctly, and that Prelude exists with correct permissions and resides in the same directory as the PodReader binary. Finally, use Console.app to check for exception spewage and to inspect the results from echo $CLASSPATH in prelude.sh.

Join the Open Source Project

From here, there are literally hundreds of directions you can go if you want to keep spicing up this application. Some of the obvious choices are: drag-and-drop ability, interfacing to Project Gutenberg to browse/download books, handling other document formats, adding a sound like "ding!" to indicate that a long document is finished processing, reading an RSS or Atom feed to sync the latest news, and so on. Because of all of the potential, I've opened this project up under the GPL and continued some more work on it since this writing. If you want to contribute and continue learning more Cocoa, check out The PodReader Project page. Full source and working binaries are also located there.

Matthew Russell
is a computer scientist from middle Tennessee; and serves Digital Reasoning Systems as the Director of Advanced Technology. Hacking and writing are two activities essential to his renaissance man regimen.