Using a Java Delegate With Cantaloupe

02 May 2025

Table of Contents


Cantaloupes. This really shows a lack of imagination…

(This is not a Cantaloupe tutorial. It assumes some basic familiarity with the core Cantaloupe image server.)

Introduction

I have been using the Cantaloupe image server version 5.0.5 for a couple of years now. It has always worked perfectly for my needs, but I recently noticed that there have been a couple of new releases (v5.0.6 and v5.0.7) - including several library updates to resolve security issues.

My use of Cantaloupe includes a custom Java class I wrote (the “delegate”) to handle how Cantaloupe retrieves image files from storage (which in my case is an AWS S3 bucket). As part of the Cantaloupe upgrade I also needed to update my delegate code - and I had forgotten how I originally set it up. So here is a walkthrough.

Terminology

First, some terminology:

The IIIF (“triple-eye-eff”) - A consortium which develops and maintains a set of APIs for how to manage image (picture) files in a consistent and interchangeable way. A good introduction can be found here. As a practical example, consider an organization such as a museum, with a large collection of digital images which it wishes to make available for research purposes - including sharing images with other similar organizations.

IIIF Image API - Defines a web service that returns an image in response to a standard HTTP request. This is the API most directly relevant to this article, in terms of my Java delegate code. Features such as cropping, scaling, rotating and deep-zooming are specified.

The Cantaloupe image server - Cantaloupe is “an open-source dynamic image server for on-demand generation of derivatives of high-resolution source images.” It implements the IIIF image API. It is built using Java and includes an embedded Jetty web server.

Image source delegate - In Cantaloupe this is a feature which lets you define a customized way to retrieve image files from different types of file store (filesystems, databases, cloud storage, etc.).

IIIF Image API Example

This is one example of a simple HTTP request for an image (note that %5E is the percent encoding for ^):

GET /iiif/3/example_image.jpg/full/%5E360,/0/default.jpg

The parts of this URL break down as follows:

scheme - e.g. https (not shown above)

server - the domain name of the Cantaloupe server (not shown above)

prefix = /iiif/3/ (the path on the Cantaloupe server to its image service - in this case /3/ means version 3 of the IIIF image API)

identifier = example_image.jpg (e.g. the file name, bucket item ID, etc.)

region = full (the entire image - no cropping)

size = ^360, (scale to a width of 360 pixels, keeping a proportional height)

rotation = 0 (degrees clockwise)

quality = default

format = jpg

The full specification of the image request parameters can be found here.

Using an Image Delegate

If the image file for example_image.jpg is actually stored at the above example URL, then Cantaloupe will retrieve it from there by default. There is no need for a “delegate” in this case.

However, in my case, all image files are stored in S3 - and therefore I need some custom logic to tell Cantaloupe how to retrieve example_image.jpg from the S3 bucket, given the above request. Cantaloupe will “delegate” calculation or derivation of specific image storage details to my Java code.

Property File Settings

To tell Cantaloupe to use an image delegate, you first need to set up some values in the main Cantaloupe properties file (cantaloupe.properties):

  • source.static = S3Source (S3 is the source of all images)
  • source.delegate = false (The S3 source is never overridden)
  • S3Source.lookup_strategy = ScriptLookupStrategy

That second one may be a bit confusing: Setting it to true means you can change the delegate used dynamically at runtime. But since all my images are always coming from S3, I do not need to do this.

The term “script” in the above settings may also be a bit confusing, as my Java code is not really a script. But Cantaloupe provides a Python script which you can choose to use as your custom delegate. I think that existed before the Java delegate was added - and if you provide that Python script, then that is what will be used (even if you also provide a Java delegate). In my case, I do not provide any such Python delegate script in my Cantaloupe configuration.

The Java Code

My Java delegate class is declared as follows:

Java
1
2
3
public class MyDelegate extends AbstractJavaDelegate implements JavaDelegate {
    ...
}

You can see the relevant JavaDoc for the JavaDelegate interface here. It shows all the methods you need to implement. In my case, I do not use most of these methods, so I simply implement them like this example:

Java
1
2
3
4
@Override
public String serializeMetaIdentifier(Map<String, Object> metaIdentifier) {
    throw new UnsupportedOperationException("Not supported yet.");
}

Some of the other methods are implemented with basic logic to support default processing:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public Object preAuthorize() {
	return true;
}

@Override
public Object authorize() {
	return true;
}

@Override
public Map<String, Object> getExtraIIIF2InformationResponseKeys() {
	return Collections.emptyMap();
}

@Override
public Map<String, Object> getExtraIIIF3InformationResponseKeys() {
	return Collections.emptyMap();
}

@Override
public List<Map<String, Long>> getRedactions() {
	return Collections.emptyList();
}

@Override
public String getMetadata() {
	return null;
}

You can read more about these in the JavaDoc.

Finally, here is my logic to tell Cantaloupe how to retrieve my S3 resources:

Java
1
2
3
4
5
6
7
8
@Override
public Map<String, String> getS3SourceObjectInfo() {
	final String resource = getContext().getIdentifier();
	final String bucket = "some-actual-s3-bucket-name-here";
	final String key = String.format("%s/%s", resource.substring(0, 1), resource);
	Logger.debug(String.format("S3 bucket = [%s], key = [%s]", bucket, key));
	return Map.of("bucket", bucket, "key", key);
}

In the above code, the context accessed by getContext() is based on the URL of the received request. Therefore getIdentifier() would retrieve the value example_image.jpg (as shown in the example URL I showed earlier).

In my case, all images are stored my S3 bucket in subfolders (I actually use UUIDs for all my image names), where the subfolder is the first character of the resource name - as a simple way to hash my images across multiple subfolders.

The method returns a map containing two keys: bucket and key. So in my case, using my previous request example, this map would contain the following:

  • bucket = some-actual-s3-bucket-name-here
  • key = e/example_image.jpg

So, the logic is very simple in my case.

Cantaloupe has built-in support for resources stored in S3. So, as well as the bucket and key values provided above, the only other things Cantaloupe needs are the S3 credentials. These can be provided to Cantaloupe in various ways.

You can read more about the Java delegate for Cantaloupe here.

Packaging the Java Delegate

In order for Cantaloupe to find the Java delegate, you need to package your Java class into a JAR file.

You must also include a “provider configuration” file in the META-INF.services directory of your JAR file. See the next section for details.

Finally, you must ensure your JAR file is on the runtime classpath when you start the Cantaloupe server. So, for example:

Windows:

1
2
3
java -cp cantaloupe-5.0.7.jar;MyJavaDelegate.jar ^
  -Dcantaloupe.config=cantaloupe.properties ^
  edu.illinois.library.cantaloupe.StandaloneEntry

Linux:

1
2
3
java -cp cantaloupe-5.0.7.jar:MyJavaDelegate.jar \
  -Dcantaloupe.config=cantaloupe.properties \
  edu.illinois.library.cantaloupe.StandaloneEntry

The Provider Configuration File

Cantaloupe uses the JDK’s ServiceLoader system to auto-discover your custom Java class and then instantiate and use it (similar to how JDBC drivers are loaded without needing to use Class.forName()).

The auto-discovery mechanism requires us to provide a file in the META-INF.services directory of our JAR, as described in the Java ServiceLoader Javadoc:

A service provider that is packaged as a JAR file for the class path is identified by placing a provider-configuration file in the resource directory META-INF/services. The name of the provider-configuration file is the fully qualified binary name of the service. The provider-configuration file contains a list of fully qualified binary names of service providers, one per line.

In our case, this file is therefore called (the service name):

edu.illinois.library.cantaloupe.delegate.JavaDelegate

(Note the above literal file name, including the periods. It’s a text file but it does not have a .txt suffix.)

The file contains the following text (the name of our Java implementation class) - for example:

org.me.cantaloupe.MyJavaDelegate

It does not contain anything else - just the above text.

If you create this text file in your project’s src/main/resources/META-INF/services directory (create it manually if it does not exist!), then a packaging utility such as Maven will automatically ensure the provider configuration file is included in your JAR.