Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

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

Code Generation with Perl

Next, I wrote a Perl script to load the information in the YAML file and generate the packages. You can see the full source for package-generator.pl as well. This script is pretty ugly. I do all the work of generating the information in Perl with embedded here-documents. A much better way to do this would be to use a templating tool like Andy Wardley's Template Toolkit, which is what I'd ultimately like to do.

Basically, this program iterates over all the entries loaded from the YAML file and generates a package for each class. It creates a Perl package name from the Java package name and a Perl package file at the appropriate location in the distribution.

For example, javax.jcr.nodetype.ItemDefinition gets a Perl package name of Java::JCR::Nodetype::ItemDefinition and a file location of lib/Java/JCR/Nodetype/ItemDefinition.pm.

The code injects a stock header and footer into the package file. All the real magic happens in between these.

Handling Static Fields

The code adds static fields by modifying the symbol table so that the wrappers point to the automatically generated stubs. For example, Java::JCR::PropertyType gets several entries like:

*STRING = *Java::JCR::javax::jcr::PropertyType::STRING;
*BINARY = *Java::JCR::javax::jcr::PropertyType::BINARY;
*LONG = *Java::JCR::javax::jcr::PropertyType::LONG;

For those who may not know, the first line makes the name Java::JCR::PropertyType::STRING exactly identical to using the longer name, Java::JCR::javax::jcr::Property::STRING by modifying the symbol table directly.

OK, looking at that, you probably want to know why all the Inline::Java stubs now have Java::JCR on the front of them. The reason is that in the generated code, I use the study_classes() routine to import the Java code and specify that the base package for the import should be Java::JCR:

study_classes(['javax.jcr.PropertyType'], 'Java::JCR');

Why? It's really not that critical, but I figured that because the name of the package I was putting on CPAN was Java::JCR, I really didn't want to drop packages into an external namespace while I was at it. Because the wrappers hide all the long names, the actual length of the internal names doesn't matter anyway.

Dealing with Constructors and Methods

After fields, the code checks whether the Java class provides a constructor (that is, if it's a class rather than an interface). As it turns out, I never actually use the code for dealing with constructors for two reasons:

  • Exceptions. For reasons I'll explain later, I don't generate the exception classes. Therefore, these constructors go unused.
  • SimpleCredentials. The only remaining class that has a constructor is java.jcr.SimpleCredentials, which is the special case I've already mentioned. Therefore, I only need to cope with constructors as a special case. I'll cover the special cases later as well.

After the constructor, the program runs through each method and generates both the static and instance method wrappers. Here's a typical method wrapper from Java::JCR::Repository:

sub login {
    my $self = shift;
    my @args = Java::JCR::Base::_process_args(@_);

    my $result = eval { $self->{obj}->login(@args) };
    if ($@) { my $e = Java::JCR::Exception->new($@); croak $e }

    return Java::JCR::Base::_process_return($result, "javax.jcr.Session", "Java::JCR::Session");
}

Camel Case

This particular example doesn't show it, but I also changed the camel-case Java names of every method to all lowercase with underscores, which is a much more common way of naming methods in Perl. I may add aliases using the Java names in the future, but I don't care for Java-style naming conventions in Perl code. The most interesting part of this process was handing names that include all-caps abbreviations. That required two lines of Perl:

my $perl_method_name = $method_name;
$perl_method_name =~ s/(p{IsLu}+)/_L$1E/g;

The /(\p{IsLu}+)/ matches any uppercase letter or string of uppercase letters. The replacement applies the \L modifier to the regular expression to convert the matched snippet to all lowercase. I prepend an underscore to complete the conversion. Thus, the method named getDescriptor becomes get_descriptor and the method named getNodeByUUID becomes get_node_by_uuid. This won't work very well, by the way, if there are any names that have abbreviations before the end (for example, if there had been a getUUIDNode, which would become get_uuidnode) Fortunately, this case never shows up in the JCR API.

Method Wrappers

Java::JCR::Base::_process_arg() processes the arguments passed to each method. This function looks for any of the generated wrapper objects (anything that isa Java::JCR::Base) in the list of arguments and unwraps the generated stub by pulling the obj key out of the blessed hash.

sub _process_args {
    my @args;
    for my $arg (@_) {
        if (UNIVERSAL::isa($arg, 'Java::JCR::Base')) {
            push @args, $arg->{obj};
        }
        else {
            push @args, $arg;
        }
    }

    return @args; 
}

The wrapper then executes the wrapped method on the generated stub by passing it the unwrapped arguments (as if the wrappers weren't there).

I make sure to wrap every call in an eval because Inline::Java passes Java exceptions as Perl exception objects. If an exception is thrown, I wrap it in a custom class named Java::JCR::Exception, which I wrote by hand.

Finally, the code returns the result. If the return type has a wrapper, as is the case in login()), I use Java::JCR::Base::_process_return() to cast the class and wrap it.

sub _process_return {
    my $result = shift;
    my $java_package = shift;
    my $perl_package = shift;

    # Null is null
    if (!defined $result) {
        return $result;
    }

    # Process array results
    elsif ($java_package =~ /^Array:(.*)$/) {
        my $real_package = $1;
        return [
            map { bless { obj => cast($real_package, $_) }, $perl_package }
                @{ $result }
        ];
    }

    # Process scalar results
    else {
        return bless {
            obj => cast($java_package, $result),
        }, $perl_package;
    }
}

This brings up two considerations: Why the custom exception class? Why do I need to cast the object? In both cases, I do this to handle minor issues in Inline::Java.

In the case of exceptions, the generated exception objects don't handle Perl stringification very well. Because a lot of exception handlers assume that exceptions are strings or properly stringified, this can be (and has been for me) a problem. My exception class makes sure stringification works the right way.

As for the cast, Inline::Java works on the assumption that you want to use the class in its most specific form, but if it hasn't studied that form, you get a generic object on which you cannot call any methods. Rather than engage the potentially costly AUTOSTUDY option to make sure Inline::Java studies everything and then smarten up the wrappers more, I've chosen to cast the objects into the expected return type. This does limit some of the flexibility.

Loading Packages

Other than the custom pieces, I needed some additional helpers to get the job done. I didn't want to write out a lot of use statements to use this library. As a JAPH, I like to keep things simple. Therefore, if I need to use the JCR and Jackrabbit, I just want to say:

use Java::JCR;
use Java::JCR::Jackrabbit;

I included a package loader in the main package, Java::JCR, that will take care of these details and then created a package for each of the subpackages in the JCR. The loader looks like:

sub import_my_packages {
    my ($package_name, $package_file) = caller;
    my %excludes = map { $_ => 1 } @_;

    my $package_dir = $package_file;
    $package_dir =~ s/.pm$//;
    my $package_glob = File::Spec->catfile($package_dir, '*.pm');

    for my $package (glob $package_glob) {
        $package =~ s/^$package_dir///;
        $package =~ s/.pm$//;
        $package =~ s///::/g;

        next if $excludes{$package};

        eval "use ${package_name}::$package;";
        if ($@) { carp "Error loading $package: $@" }
    }
}

I make sure to call that method once the package has finished loading and pass in exclusions to keep it from loading all the subpackages. This needs further enhancement to allow for future extensions under the Java::JCR namespace, so as not to load them automatically, but this is a good starting point. I built one class for each subpackage, then, that inherits from Java::JCR and then calls this method to load each of those classes.

Connecting to Jackrabbit

Obviously, the next step was to create the code to connect to Jackrabbit. This was done in Java::JCR::Jackrabbit. The initial implementation is very simple:

use base qw( Java::JCR::Base Java::JCR::Repository );

use Inline (
    Java => 'STUDY',
    STUDY => [],
);
use Inline::Java qw( study_classes );

study_classes(['org.apache.jackrabbit.core.TransientRepository'], 'Java::JCR');

sub new {
    my $class = shift;

    return bless {
        obj => Java::JCR::org::apache::jackrabbit::core::TransientRepository
                ->new(@_),
    }, $class;
}

I extended Java::JCR::Repository to add a constructor that calls the Jackrabbit constructor. Done.

Pages: 1, 2, 3

Next Pagearrow