diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..067d1b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.env + +lib/build +lib/bin + +.vscode/settings.json + +.gradle +gradle +gradlew +gradlew.bat +.DS_Store +.vscode/launch.json +cantaloupe.minimal.properties +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..359f934 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Delegate for Cantaloupe / Nuxeo + +This repository contains code to create a jar file that can be used as a [Delegate](https://cantaloupe-project.github.io/manual/5.0/delegate-system.html) for [Cantaloupe's](https://cantaloupe-project.github.io/) Delegate System to load source from [Nuxeo](https://www.nuxeo.com/). + +## Before building, testing, running, etc +- [Download](https://github.com/cantaloupe-project/cantaloupe/releases) a recent Cantaloupe build (v.50 or higher) or build from source +- change the value of *cantaloupe_jar* in _lib/build/build-example.gradle_ to match the path of your Cantaloupe Jar file +- Rename _lib/build/build-example.gradle_ to _lib/build/build.gradle_ +- Modify if needed and rename _cantaloupe.properties.sample_ to _cantaloupe.properties_ + +## Build +To build the jar file in lib/build/libs/: + +`gradle jar` + +## Run Cantaloupe with this Delegate +There is a minimal settings file for Cantaloupe in this repository "cantaloupe.minimal.properties" + +`java -cp \ + "/opt/cantaloupe/cantaloupe-6.0-SNAPSHOT.jar":"./lib/build/libs/cantaloupe-nuxeo-delegate-1.0.jar" \ + -Dcantaloupe.config=cantaloupe.minmal.properties \ + edu.illinois.library.cantaloupe.StandaloneEntry +` \ No newline at end of file diff --git a/cantaloupe.properties.sample b/cantaloupe.properties.sample new file mode 100644 index 0000000..0dc7a94 --- /dev/null +++ b/cantaloupe.properties.sample @@ -0,0 +1,19 @@ +# These are the minimal properties for Cantaloupe to run against Nuxeo using this Delegate +# and start Cantaloupe as a standalone server +# See https://github.com/cantaloupe-project/cantaloupe/blob/develop/cantaloupe.properties.sample + +http.enabled = true +http.host = 0.0.0.0 +http.port = 8182 + +max_pixels = 10000000 +source.static = HttpSource +delegate_script.enabled = true + +HttpSource.lookup_strategy = ScriptLookupStrategy + +# Specific properties for the Nuxeo Delegate: +nuxeo.url=http:/example.com/nuxeo +nuxeo.username=Username +nuxeo.secret=Password + diff --git a/lib/build-example.gradle b/lib/build-example.gradle new file mode 100644 index 0000000..fd9884f --- /dev/null +++ b/lib/build-example.gradle @@ -0,0 +1,44 @@ +// Change path to Cantaloupe Jar to feed your needs and rename to 'build.gradle' + +def cantaloupe_jar = "/opt/cantaloupe/cantaloupe-6.0-SNAPSHOT.jar" + +plugins { + id 'java-library' +} + +group "io.memorix.cantaloupe" +version "1.0" +archivesBaseName = "io.memorix.cantaloupe" + +repositories { + mavenCentral() +} + +test { + testLogging.showStandardStreams = true + jvmArgs '-Dcantaloupe.config=cantaloupe.minimal.properties' +} + +dependencies { + api 'com.google.code.gson:gson:2.8.9' + api 'io.github.cdimascio:dotenv-java:2.2.0' + + implementation(files(cantaloupe_jar)) + testImplementation(files(cantaloupe_jar)) + testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' + + +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} + +jar { + manifest { + attributes( + 'Main-Class': 'io.memorix.cantaloupe.Main' + ) + } +} \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 0000000..8249423 --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java-library' +} + +def CANTALOUPE_JAR = "/Users/mlindeman/Code/software/cantaloupe/target/cantaloupe-6.0-SNAPSHOT.jar" +group "io.memorix" +version "1.0" +archivesBaseName = "cantaloupe-nuxeo-delegate" + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +test { + testLogging.showStandardStreams = true + jvmArgs '-Dcantaloupe.config=cantaloupe.minimal.properties' +} + +dependencies { + api 'com.google.code.gson:gson:2.8.9' + + compileOnly(files(CANTALOUPE_JAR)) + testImplementation(files(CANTALOUPE_JAR)) + testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' + testImplementation 'commons-cli:commons-cli:1.5.0' +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} diff --git a/lib/src/main/java/io/memorix/cantaloupe/CantaloupeNuxeoDelegate.java b/lib/src/main/java/io/memorix/cantaloupe/CantaloupeNuxeoDelegate.java new file mode 100644 index 0000000..93579c5 --- /dev/null +++ b/lib/src/main/java/io/memorix/cantaloupe/CantaloupeNuxeoDelegate.java @@ -0,0 +1,111 @@ +package io.memorix.cantaloupe; + +import edu.illinois.library.cantaloupe.config.Configuration; +import edu.illinois.library.cantaloupe.delegate.AbstractJavaDelegate; +import edu.illinois.library.cantaloupe.delegate.JavaDelegate; +import edu.illinois.library.cantaloupe.delegate.Logger; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CantaloupeNuxeoDelegate extends AbstractJavaDelegate implements JavaDelegate { + + protected Configuration config = Configuration.getInstance(); + final NuxeoClient nuxeoClient = new NuxeoClient(); + + @Override + public Boolean preAuthorize() { + + return true; + } + + @Override + public Boolean authorize() { + return true; + } + + @Override + public Map getHTTPSourceResourceInfo() { + Map resource = new HashMap<>(); + resource.put("uri", nuxeoClient.getFileUrlForIdentifier(getContext().getIdentifier())); + resource.put("username", config.getString(NuxeoKey.NX_USERNAME.key())); + resource.put("secret", config.getString(NuxeoKey.NX_SECRET.key())); + return resource; + } + + @Override + public Map getExtraIIIF2InformationResponseKeys() { + return getExtraIIIF3InformationResponseKeys(); + } + + @Override + public Map getExtraIIIF3InformationResponseKeys() { + if (!config.getBoolean(NuxeoKey.NX_LOAD_PROPERTIES.key(), false)) return Collections.emptyMap(); + else return nuxeoClient.getMetadata(getContext().getIdentifier()); + } + + @Override + public String getSource() { + //NB this will only be used when source.delegate = true + return "HttpSource"; + } + + @Override + public String getMetadata() { + Logger.debug(getContext().getMetadata().toString()); + return null; + } + + @Override + public String getAzureStorageSourceBlobKey() { + return null; + } + + @Override + public String getFilesystemSourcePathname() { + return null; + } + + @Override + public String getJDBCSourceDatabaseIdentifier() { + return null; + } + + @Override + public String getJDBCSourceMediaType() { + return null; + } + + @Override + public String getJDBCSourceLookupSQL() { + return null; + } + + @Override + public Map getS3SourceObjectInfo() { + return null; + } + + @Override + public Map getOverlay() { + return null; + } + + @Override + public List> getRedactions() { + return Collections.emptyList(); + } + + @Override + public String serializeMetaIdentifier(Map metaIdentifier) { + return null; + } + + @Override + public Map deserializeMetaIdentifier(String metaIdentifier) { + return null; + } + +} diff --git a/lib/src/main/java/io/memorix/cantaloupe/NuxeoClient.java b/lib/src/main/java/io/memorix/cantaloupe/NuxeoClient.java new file mode 100644 index 0000000..7bcfdef --- /dev/null +++ b/lib/src/main/java/io/memorix/cantaloupe/NuxeoClient.java @@ -0,0 +1,103 @@ +package io.memorix.cantaloupe; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.net.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.io.IOException; +import java.net.InetSocketAddress; + +import edu.illinois.library.cantaloupe.config.Configuration; +import edu.illinois.library.cantaloupe.delegate.Logger; + + +public final class NuxeoClient { + + public static final String NX_FILE_PATH = "/nxfile/default/"; + public static final String NX_API_PATH = "/api/v1/id/"; + + protected final Configuration config = Configuration.getInstance(); + public final String url = config.getString(NuxeoKey.NX_URL.key()).replaceAll("/+$", ""); + + + public static String getUrl() + { + final NuxeoClient self = new NuxeoClient(); + return self.url; + } + + private OkHttpClient getHttpClient() { + final OkHttpClient.Builder builder = new OkHttpClient.Builder(); + final boolean httpProxyEnabled = config.getBoolean(NuxeoKey.NX_PROXY_ENABLED.key(), false); + if (httpProxyEnabled) { + final String httpProxyServer = config.getString(NuxeoKey.NX_PROXY_SERVER.key()); + if (httpProxyServer == null) { + throw new RuntimeException(String.format("proxy server setting '%s' should not be empty", NuxeoKey.NX_PROXY_SERVER.key())); + } + final int httpProxyPort = config.getInt(NuxeoKey.NX_PROXY_PORT.key(), 8080); + Logger.debug(String.format("******* [MRX:getHTTPClient] Using HTTP Proxy at server %s on port %d", httpProxyServer, httpProxyPort)); + Proxy httpProxy = new Proxy(Proxy.Type.HTTP,new InetSocketAddress(httpProxyServer, httpProxyPort)); + builder.proxy(httpProxy); + } + return builder.build(); + } + + public String getFileUrlForIdentifier(String identifier) { + return String.format("%s%s%s", url, NX_FILE_PATH, identifier); + } + + public Map getMetadata(String identifier) { + if (!config.getBoolean(NuxeoKey.NX_LOAD_PROPERTIES.key(), false)) return Collections.emptyMap(); + + String url = String.format("%s%s%s?properties=*", this.url, NX_API_PATH, identifier); + + Request.Builder builder = new Request.Builder().method("GET", null).url(url); + byte[] BasicAuthHeader = String.format("%s:%s", + config.getString(NuxeoKey.NX_USERNAME.key()), + config.getString(NuxeoKey.NX_SECRET.key()) + ).getBytes(StandardCharsets.UTF_8); + builder.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString(BasicAuthHeader)); + + Response response; + + Map metadata = new HashMap<>(); + + try { + response = getHttpClient().newCall(builder.build()).execute(); + } catch (IOException e) { + metadata.put("nuxeoPropertiesError", "Failed to load properties from Nuxeo" + e.getMessage()); + return metadata; + } + + if (!response.isSuccessful()) { + metadata.put("nuxeoPropertiesError", "Failed to load properties from Nuxeo"); + return metadata; + } + + String body; + try { + body = response.body().string(); + } catch (IOException e) { + metadata.put("nuxeoPropertiesError", "Failed to load body from Nuxeo response"); + return metadata; + } + Map nuxeoProperties = new Gson().fromJson( + body, new TypeToken>() {}.getType() + ); + + metadata.put("nuxeoProperties", nuxeoProperties); + + return metadata; + } + +} diff --git a/lib/src/main/java/io/memorix/cantaloupe/NuxeoKey.java b/lib/src/main/java/io/memorix/cantaloupe/NuxeoKey.java new file mode 100644 index 0000000..27ccb02 --- /dev/null +++ b/lib/src/main/java/io/memorix/cantaloupe/NuxeoKey.java @@ -0,0 +1,26 @@ +package io.memorix.cantaloupe; + +public enum NuxeoKey { + + NX_URL("nuxeo.url"), + NX_USERNAME("nuxeo.username"), + NX_SECRET("nuxeo.secret"), + NX_LOAD_PROPERTIES("nuxeo.load_properties"), + NX_PROXY_ENABLED("nuxeo.proxy.enabled"), + NX_PROXY_SERVER("nuxeo.proxy.server"), + NX_PROXY_PORT("nuxeo.proxy.port"); + + private final String key; + + NuxeoKey(String key) { + this.key = key; + } + + public String key() { + return key; + } + + public String toString() { + return key(); + } +} diff --git a/lib/src/main/resources/META-INF/services/edu.illinois.library.cantaloupe.delegate.JavaDelegate b/lib/src/main/resources/META-INF/services/edu.illinois.library.cantaloupe.delegate.JavaDelegate new file mode 100644 index 0000000..f3f1586 --- /dev/null +++ b/lib/src/main/resources/META-INF/services/edu.illinois.library.cantaloupe.delegate.JavaDelegate @@ -0,0 +1 @@ +io.memorix.cantaloupe.CantaloupeNuxeoDelegate \ No newline at end of file diff --git a/lib/src/test/java/io/memorix/cantaloupe/BaseTest.java b/lib/src/test/java/io/memorix/cantaloupe/BaseTest.java new file mode 100644 index 0000000..1a07352 --- /dev/null +++ b/lib/src/test/java/io/memorix/cantaloupe/BaseTest.java @@ -0,0 +1,30 @@ +package io.memorix.cantaloupe; + +import org.junit.jupiter.api.BeforeEach; + +import edu.illinois.library.cantaloupe.config.Configuration; +import edu.illinois.library.cantaloupe.config.ConfigurationFactory; + +public abstract class BaseTest { + + public static String NX_URL = "http://example.org/nuxeo"; + public static String NX_USERNAME = "Username"; + public static String NX_SECRET = "Secret"; + + static { + // Suppress a Dock icon and annoying Space transition in full-screen + // mode in macOS. + System.setProperty("java.awt.headless", "true"); + // Suppress an exception thrown by the JAI framework. + System.setProperty("com.sun.media.jai.disableMediaLib", "true"); + } + + static public void setUp() throws Exception { + ConfigurationFactory.clearInstance(); + System.setProperty(ConfigurationFactory.CONFIG_VM_ARGUMENT, "memory"); + Configuration.getInstance().setProperty(NuxeoKey.NX_URL.key(), "http://example.org/nuxeo"); + Configuration.getInstance().setProperty(NuxeoKey.NX_USERNAME.key(), "Username"); + Configuration.getInstance().setProperty(NuxeoKey.NX_SECRET.key(), "Secret"); + } + +} diff --git a/lib/src/test/java/io/memorix/cantaloupe/CantaloupeNuxeoDelegateTest.java b/lib/src/test/java/io/memorix/cantaloupe/CantaloupeNuxeoDelegateTest.java new file mode 100644 index 0000000..ef5a2a0 --- /dev/null +++ b/lib/src/test/java/io/memorix/cantaloupe/CantaloupeNuxeoDelegateTest.java @@ -0,0 +1,64 @@ +package io.memorix.cantaloupe; + +import org.junit.jupiter.api.Test; + +import edu.illinois.library.cantaloupe.config.Configuration; +import edu.illinois.library.cantaloupe.delegate.JavaRequestContext; +import edu.illinois.library.cantaloupe.image.Identifier; +import edu.illinois.library.cantaloupe.resource.RequestContext; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeAll; + +class CantaloupeNuxeoDelegateTest extends BaseTest +{ + + private static CantaloupeNuxeoDelegate delegate; + + public static CantaloupeNuxeoDelegate getDelegate() { + if (null == delegate) { + delegate = new CantaloupeNuxeoDelegate(); + final RequestContext requestContext = new RequestContext(); + requestContext.setIdentifier(new Identifier("cats")); + final JavaRequestContext context = new JavaRequestContext(requestContext); + delegate.setContext(context); + } + return delegate; + } + + @BeforeAll + static public void setUp() throws Exception { + BaseTest.setUp(); + } + + + @Test + void testResource() + { + + CantaloupeNuxeoDelegate delegate = getDelegate(); + Map resource = delegate.getHTTPSourceResourceInfo(); + + assertTrue(resource.containsKey("uri"), "Missing key `uri` in response resource"); + assertTrue(resource.containsKey("username"), "Missing key `username` in response resource"); + assertTrue(resource.containsKey("secret"), "Missing key `secret` in response resource"); + + + assertTrue( + resource.get("uri").toString().startsWith( + Configuration.getInstance().getString(NuxeoKey.NX_URL.key()) + ), "uri is not correct" + ); + + assertEquals(NX_USERNAME, resource.get("username"), "Wrong value for key `username`"); + assertEquals(NX_SECRET, resource.get("secret"), "Wrong value for key `secret`"); + } + + @Test void testAuthorization() + { + + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0dbfb1d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'cantaloupe-nuxeo-delegate' +include('lib') diff --git a/start-cantaloupe-with-delagate.sh b/start-cantaloupe-with-delagate.sh new file mode 100755 index 0000000..4e9be06 --- /dev/null +++ b/start-cantaloupe-with-delagate.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Convenience Bash script to build Delegate and start Cantaloupe server +# @Author Mark Lindeman + +script=$0 +cd $(dirname $script) + +./gradlew jar + +GradleBuildFile="./lib/build.gradle" +test -f $GradleBuildFile || { + echo "[ERROR] expected Gradle build file at $GradleBuildFile" 1>&2 + exit 2 +} + +echo -n "Locate Cantaloupe JAR: " +CantaloupeJar=$(grep 'def CANTALOUPE_JAR = ' ./lib/build.gradle | sed 's/.*"\(.*\)"/\1/') +test -z "$CantaloupeJar" && { + echo "error" + echo "[ERROR] failed to auto detect Cantaloupe Jar from gradle implementation" 1>&2 + exit 1 +} + +test -f "$CantaloupeJar" || { + echo "not found" + echo "[ERROR] JAR '$CantaloupeJar' is not a file" 1>&2 + exit 2 +} + +echo $CantaloupeJar + +# Autodetect Delegate JAR: +archivesBaseName=$(grep "archivesBaseName" ./lib/build.gradle|cut -d '"' -f 2) +test -z $archivesBaseName && { + echo "error" + echo "[ERROR] failed to grep 'archivesBaseName' from build.gradle" 1>&2 + exit 3 +} +version=$(grep 'version ' ./lib/build.gradle | cut -d '"' -f 2) +DelegateJar="./lib/build/libs/$archivesBaseName-$version.jar" +test -f $DelegateJar || { + echo "not found" + echo "[ERROR] Expected Delegate JAR at $DelegateJar" 1>&2 + exit 3 +} + +java -cp \ + "$CantaloupeJar":"$DelegateJar" \ + -Dcantaloupe.config=cantaloupe.minimal.properties \ + edu.illinois.library.cantaloupe.StandaloneEntry