Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

Using Java Classes in Perl
by Andrew Hanenkamp | Pages: 1, 2, 3

Handling Special Cases

With all that work, I still couldn't make the second hop because I still hadn't resolved the whole problem of passing an array of characters. However, with the infrastructure I had in place, this was now solvable.

I created an additional YAML configuration file named specials.yml. This file contains hand-coded alternatives to use where appropriate. I then wrote an alternative for the new constructor:

javax.jcr.SimpleCredentials:
  new: |-
    sub new {
        my $class = shift;
        my $user = shift;
        my $password = shift;

        my $charArray = Java::JCR::PerlUtils->charArray($password);

        return bless {
            obj => Java::JCR::javax::jcr::SimpleCredentials->new($user, $charArray),
        }, $class;
    }

Then, I reran the generator script. Fortunately, I had already improved it to use any implemented method or constructor rather than generating one automatically.

To perform the conversion, I also needed to embed a little extra Java code. I wrote a very small Java class called PerlUtils for handling the conversion:

use Inline (
    Java => <<'END_OF_JAVA',

class PerlUtils {
    public static char[] charArray(String str) {
        return str.toCharArray();
    }
}

END_OF_JAVA
);

Given a string, it returns an array of characters to pass back into the SimpleCredentials constructor. No other work is necessary. I could now perform the JCR second hop in Perl. That script attaches to a Jackrabbit repository, logs in as "username" with password "password" and then creates a node.

Using Handles as InputStreams

The third (and final) hop of the Jackrabbit tutorial demonstrates node import using an XML file. However, in order to perform the import shown, you must pass an InputStream off to the importXML() method. While Inline::Java provides the ability to use Java InputStreams as Perl file handles, it doesn't provide the mapping in the opposite direction. Thus I needed another special handler and an additional set of helper methods.

The special code configuration looks like:

javax.jcr.Session:
  import_xml: |-
    sub import_xml {
        my $self = shift;
        my $path = shift;
        my $handle = shift;
        my $behavior = shift;

        my $input_stream = Java::JCR::JavaUtils::input_stream($handle);

        $self->{obj}->importXML($path, $input_stream, $behavior);
    }

This calls the input_stream() method, which is a Perl subroutine.

sub input_stream {
    my $glob = shift;
    my $glob_val = $$glob;
    $glob_val =~ s/^\*//;
    my $glob_caller = Java::JCR::GlobCaller->new($glob_val);
    return Java::JCR::GlobInputStream->new($glob_caller);
}

As you can see, this subroutine uses two separate Java classes to provide the interface from a Perl file handle to Java InputStream. The first class, Java::JCR::GlobCaller, performs most of the real work using the callback features provided by Inline::Java. It gets passed to the Java::JCR::GlobInputStream, which calls read() whenever the JCR reads from the stream:

public int read() throws InlineJavaException, InlineJavaPerlException {
    String ch = (String) CallPerlSub(
            "Java::JCR::JavaUtils::read_one_byte", new Object[] {
                this.glob
           });
    return ch != null ? ch.charAt(0) : -1;
}

The read_one_byte() function is a very basic wrapper for the Perl built-in getc.

sub read_one_byte {
    my $glob = shift;
    my $c = getc $glob;
    return $c;
}

With this in place, you can now perform the third JCR hop in Perl. By executing this script, you will connect to a repository, log in, and then create nodes and properties from an XML file.

Getting Ready to Distribute

The implementation is now, more or less, complete. You can use Java::JCR to connect to a Jackrabbit repository, log in, create nodes and properties, and import data from XML. There's a lot left untested, but the essentials are now present. With this done, I was ready to begin getting ready for the distribution. However, because some Java libraries are requirements to use the library, the library has some special needs to build and install easily. You should be able to install it by just running:

% cpan Java::JCR

I needed a way to build this library. My preferred build tool is Ken Williams' Module::Build. It's in common use, compatible with the CPAN installer, and cooperates well with g-cpan.pl, which is a packaging tool for my favorite Linux distribution, Gentoo. Finally, it's easy to extend.

When customizing Module::Build, I prefer to create a custom build module rather than by placing the extension directly inline with the Build.PL file. In this case, I've called the module Java::JCR::Build. I placed it inside a directory named inc/ with the rest of the tools I built for generating the package.

After creating the basic module that extends Module::Build, I added a custom action to fetch the JAR files called get_jars. I also added the code to execute this action on build by extending the code ACTION:

sub ACTION_get_jars {
    my $self = shift;

    eval "require LWP::UserAgent"
        or die "Failed to load LWP::UserAgent: $@";

    my $mirror_dir
        = File::Spec->catdir($self->blib, 'lib', 'Java', 'JCR');
    mkpath( $mirror_dir, 1);

    my $ua = LWP::UserAgent->new;

    print "Checking for needed jar files...n";
    while (my ($file, $url) = each %jars) {
        my $path = File::Spec->catfile($mirror_dir, $file);
        $self->add_to_cleanup($path);

        next if -f $path;

        my $response = $ua->mirror($url, $path);
        if ($response->is_success) {
            print "Mirroring $url to $file.n";
        }

        elsif ($response->is_error) {
            die "An error occurred fetching $url to $file: ",
                $response->status_line, "n";
        }
    }
}

sub ACTION_code {
    my $self = shift;

    $self->ACTION_get_jars;
    $self->SUPER::ACTION_code;
}

I use Gisle Aas's LWP::UserAgent to fetch the JAR files from the public Maven repositories and drop them into the build library folder, blib. Module::Build will take care of the rest by copying those JAR files to the appropriate location during the install process.

I also needed some code in Java::JCR to set the CLASSPATH correctly ahead of time:

my $classpath;
BEGIN {
    my @classpath;
    my $this_path = $INC{'Java/JCR.pm'};
    $this_path =~ s/.pm$//;
    my $jar_glob = File::Spec->catfile($this_path, "*.jar");
    for my $jar_file (glob $jar_glob) {
        push @classpath, $jar_file;
    }
    $classpath = join ':', @classpath, ($ENV{'CLASSPATH'} || '');
    $ENV{'CLASSPATH'} = $classpath;
}

This bit of code asks Perl for the path to the location of this library, which I assume is the installed location of the JAR files. Then, I find each file ending with .jar in that directory and put them into the CLASSPATH. Unfortunately, my code assumes a Unix environment when it uses the colon as the path separator. A future revision could make sure that this works on other systems as well, but because I use only Unix-based operating systems, my motivation is lacking.

With all that, you can now deploy this by downloading the tarball and running:

% perl Build.PL
% ./Build
% ./Build test
% ./Build install

It works!

Testing

I haven't mentioned this yet, but during the whole process of building this library, I also built a series of test cases. You can find these in the t/ directory of the distribution. The first few tests are actually just variations on the Jackrabbit tutorial, as well as a test to make sure the POD documentation contains no errors (every module author should use this test; you can just copy and paste it into any project).

Final Thoughts

I love Perl. This port from Java to Perl was easier than I would have thought possible. I wanted to share my success in the hopes of spurring on others. Kudos go to Ken Williams and Patrick LeBoutillier and the others that have assisted them to build the tools that made this possible.

Cheers.