Index: gs3-extensions/iiif-servlet/trunk/src/CLEAN-then-COMPILE.sh
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/CLEAN-then-COMPILE.sh	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/CLEAN-then-COMPILE.sh	(revision 32707)
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+echo "Checking that maven (mvn) is on your PATH:"
+which mvn
+if [ $? = 0 ] ; then
+  cd packages/cantaloupe-4.0.2 \
+   && mvn clean package -DskipTests
+else
+  echo "Failed to find 'mvn'" 1>&2
+fi
+
Index: gs3-extensions/iiif-servlet/trunk/src/COMPILE.sh
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/COMPILE.sh	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/COMPILE.sh	(revision 32707)
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+echo "Checking that maven (mvn) is on your PATH:"
+which mvn
+if [ $? = 0 ] ; then
+  cd packages/cantaloupe-4.0.2 \
+      && mvn package -DskipTests
+  echo
+  echo "Next step:"
+  echo "  ./INSTALL.sh"
+  echo  
+else
+  echo "Failed to find 'mvn'" 1>&2
+fi
+
Index: gs3-extensions/iiif-servlet/trunk/src/INSTALL.sh
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/INSTALL.sh	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/INSTALL.sh	(revision 32707)
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+/bin/cp packages/cantaloupe-4.0.2/target/cantaloupe-4.0.2.war ../../packages/tomcat/webapps/gs-cantaloupe.war
+
Index: gs3-extensions/iiif-servlet/trunk/src/PREPARE-GSDL.sh
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/PREPARE-GSDL.sh	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/PREPARE-GSDL.sh	(revision 32707)
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+echo "Running rsync update on gsdl-src/java"
+rsync -pav gsdl-src/java ../../src/.
+
+pushd ../../ && ant compile-core && popd
+
+/bin/cp ../../build/gsdl3.jar src/main/webapp/WEB-INF/lib/gsdl3.jar
+/bin/cp ../../build/gutil.jar src/main/webapp/WEB-INF/lib/gutil.jar
Index: gs3-extensions/iiif-servlet/trunk/src/PREPARE.sh
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/PREPARE.sh	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/PREPARE.sh	(revision 32707)
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+if [ ! -d packages/cantaloupe-4.0.2 ] ; then
+    cd packages/ 
+    tar xvzf cantaloupe-4.0.2.tar.gz
+    cd ..
+fi
+
+echo "Running rsync update on src/main"
+rsync -pav src/main packages/cantaloupe-4.0.2/src/.
+
+echo "Running rsync update on cantaloupe-pom.xml -> cantaloupe-4.0.2/pom.xml"
+rsync -pav cantaloupe-pom.xml packages/cantaloupe-4.0.2/pom.xml
+
+
+echo
+
+echo "Copying Greenstone's global.properties into Cantaloupe's resources folder"
+/bin/cp ../../web/WEB-INF/classes/global.properties packages/cantaloupe-4.0.2/src/main/resources/.
+
+echo "Copying Greenstone's log4j.properties into Cantaloupe's resources folder"
+/bin/cp ../../web/WEB-INF/classes/log4j.properties packages/cantaloupe-4.0.2/src/main/resources/.
+
+echo "Copying Greenstone's OAIConfig.xml into Cantaloupe's resources folder"
+echo "**** Note: this is only a temporary measure, and should be transformed into IIIFConfig.xml"
+/bin/cp ../../web/WEB-INF/classes/OAIConfig.xml packages/cantaloupe-4.0.2/src/main/resources/.
+
+
+echo
+echo "Next step:"
+echo "  ./COMPILE.sh"
+echo
Index: gs3-extensions/iiif-servlet/trunk/src/cantaloupe-pom.xml
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/cantaloupe-pom.xml	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/cantaloupe-pom.xml	(revision 32707)
@@ -0,0 +1,763 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>edu.illinois.library.cantaloupe</groupId>
+  <artifactId>cantaloupe</artifactId>
+  <packaging>war</packaging>
+  <version>4.0.2</version>
+  <name>Cantaloupe</name>
+  <url>https://medusa-project.github.io/cantaloupe/</url>
+  <inceptionYear>2015</inceptionYear>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    <aws-sdk.version>1.11.291</aws-sdk.version>
+    <jackson.version>2.9.5</jackson.version>
+    <jetty.version>9.4.12.v20180830</jetty.version>
+    <jruby.version>9.1.17.0</jruby.version>
+    <restlet.version>2.3.11</restlet.version>
+    <surefire.version>2.21.0</surefire.version>
+  </properties>
+
+  <developers>
+    <developer>
+      <id>alexd</id>
+      <name>Alex Dolski</name>
+      <email>alexd@n-ary.net</email>
+      <organization>University of Illinois at Urbana-Champaign Library</organization>
+      <organizationUrl>http://www.library.illinois.edu/</organizationUrl>
+      <roles>
+        <role>founder</role>
+        <role>lead</role>
+        <role>developer</role>
+        <role>debugger</role>
+        <role>reviewer</role>
+        <role>support</role>
+        <role>maintainer</role>
+      </roles>
+      <timezone>-6</timezone>
+    </developer>
+  </developers>
+
+  <scm>
+    <connection>scm:git:git://github.com/medusa-project/cantaloupe</connection>
+    <developerConnection>scm:git:git@github.com:medusa-project/cantaloupe</developerConnection>
+    <tag>HEAD</tag>
+    <url>https://github.com/medusa-project/cantaloupe</url>
+  </scm>
+  <issueManagement>
+    <system>GitHub Issues</system>
+    <url>https://github.com/medusa-project/cantaloupe/issues</url>
+  </issueManagement>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.greenstone</groupId>
+      <artifactId>gsdl3</artifactId>
+      <version>3.0.9</version>
+      <scope>system</scope>
+      <!--
+	  <systemPath>${project.basedir}/src/main/resources/gsdl3.jar</systemPath>
+      -->
+      <systemPath>${project.basedir}/src/main/webapp/WEB-INF/lib/gsdl3.jar</systemPath>
+    </dependency>
+
+    <dependency>
+      <groupId>org.greenstone</groupId>
+      <artifactId>gutil</artifactId>
+      <version>3.0.9</version>
+      <scope>system</scope>
+      <!--
+	  <systemPath>${project.basedir}/src/main/resources/gutil.jar</systemPath>
+      -->      
+      <systemPath>${project.basedir}/src/main/webapp/WEB-INF/lib/gutil.jar</systemPath>	  
+    </dependency>
+
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <version>1.2.8</version>
+    </dependency>
+
+
+    
+    <!-- Used by RedisCache -->
+    <dependency>
+      <groupId>biz.paluch.redis</groupId>
+      <artifactId>lettuce</artifactId>
+      <version>4.3.3.Final</version>
+    </dependency>
+    <!-- The application logger. -->
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.2.3</version>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-core</artifactId>
+      <version>1.2.3</version>
+    </dependency>
+    <!-- Used by S3Source and S3Cache -->
+    <dependency>
+      <groupId>com.amazonaws</groupId>
+      <artifactId>aws-java-sdk-core</artifactId>
+      <version>${aws-sdk.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.amazonaws</groupId>
+      <artifactId>aws-java-sdk-s3</artifactId>
+      <version>${aws-sdk.version}</version>
+    </dependency>
+    <!-- In-memory object cache: https://github.com/ben-manes/caffeine-->
+    <dependency>
+      <groupId>com.github.ben-manes.caffeine</groupId>
+      <artifactId>caffeine</artifactId>
+      <version>2.5.6</version>
+    </dependency>
+    <!-- Used in tests to test that System.exit() is called -->
+    <dependency>
+      <groupId>com.github.stefanbirkner</groupId>
+      <artifactId>system-rules</artifactId>
+      <version>1.15.1</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Used by HeapCache -->
+    <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+      <version>3.5.1</version>
+    </dependency>
+    <!-- Used by JdbcSourceTest and JdbcCacheTest -->
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+      <version>1.4.196</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Used by com.mortennobel.imagescaling -->
+    <dependency>
+      <groupId>com.jhlabs</groupId>
+      <artifactId>filters</artifactId>
+      <version>2.0.235</version>
+    </dependency>
+    <!-- Used by AzureStorageSource and AzureStorageCache-->
+    <dependency>
+      <groupId>com.microsoft.azure</groupId>
+      <artifactId>azure-storage</artifactId>
+      <version>5.2.0</version>
+    </dependency>
+    <!-- Provides a JDBC connection pool for JdbcSource and JdbcCache -->
+    <dependency>
+      <groupId>com.zaxxer</groupId>
+      <artifactId>HikariCP</artifactId>
+      <version>2.7.2</version>
+    </dependency>
+    <!-- Manages the application configuration. -->
+    <dependency>
+      <groupId>commons-configuration</groupId>
+      <artifactId>commons-configuration</artifactId>
+      <version>1.10</version>
+    </dependency>
+    <!-- IOUtils is used widely -->
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <version>2.4</version>
+    </dependency>
+    <!-- Used by S3SourceTest and S3CacheTest -->
+    <dependency>
+      <groupId>io.findify</groupId>
+      <artifactId>s3mock_2.12</artifactId>
+      <version>0.2.4</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Dependency on part of Java EE that's available by default in Java 8
+    but not 9 -->
+    <dependency>
+      <groupId>javax.xml.bind</groupId>
+      <artifactId>jaxb-api</artifactId>
+      <version>2.3.0</version>
+    </dependency>
+    <!-- Enhanced ImageIO TIFF plugin supporting BigTIFF:
+     https://github.com/geosolutions-it/imageio-ext/ -->
+    <dependency>
+      <groupId>it.geosolutions.imageio-ext</groupId>
+      <artifactId>imageio-ext-tiff</artifactId>
+      <version>1.1.19</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>3.1.0</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>3.6</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.10</version>
+    </dependency>
+    <!-- This is used to fix a CVE-2015-6420 in commons-collections-3.2.1, a
+    transitive dependency -->
+    <dependency>
+      <groupId>commons-collections</groupId>
+      <artifactId>commons-collections</artifactId>
+      <version>3.2.2</version>
+    </dependency>
+    <!-- Provides PDFBox for PdfBoxProcessor. Note that there are also some
+    other dependencies in this POM used only by PDFBox. -->
+    <dependency>
+      <groupId>org.apache.pdfbox</groupId>
+      <artifactId>pdfbox</artifactId>
+      <version>2.0.12</version>
+    </dependency>
+    <!-- Assists PDFBox in decoding JBIG2-encoded images -->
+    <dependency>
+      <groupId>org.apache.pdfbox</groupId>
+      <artifactId>jbig2-imageio</artifactId>
+      <version>3.0.0</version>
+    </dependency>
+    <!-- Unit tests -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Performance tests -->
+    <dependency>
+      <groupId>org.openjdk.jmh</groupId>
+      <artifactId>jmh-core</artifactId>
+      <version>1.19</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.openjdk.jmh</groupId>
+      <artifactId>jmh-generator-annprocess</artifactId>
+      <version>1.19</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Assists in parsing XMP metadata in RDF/XML -->
+    <dependency>
+      <groupId>org.apache.jena</groupId>
+      <artifactId>jena-core</artifactId>
+      <version>3.4.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jena</groupId>
+      <artifactId>jena-arq</artifactId>
+      <version>3.4.0</version>
+    </dependency>
+    <!-- Used for media type detection -->
+    <dependency>
+      <groupId>org.apache.tika</groupId>
+      <artifactId>tika-core</artifactId>
+      <version>1.19.1</version>
+    </dependency>
+    <!-- Assists PDFBox in decoding certain PDFs -->
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+      <version>1.55</version>
+    </dependency>
+    <!-- Supports <if> expressions in logback.xml -->
+    <dependency>
+      <groupId>org.codehaus.janino</groupId>
+      <artifactId>janino</artifactId>
+      <version>2.7.8</version>
+    </dependency>
+    <!-- Provides HTTP/2 support in the Jetty client.
+    N.B.: ALPN support is in a different module. -->
+    <dependency>
+      <groupId>org.eclipse.jetty.http2</groupId>
+      <artifactId>http2-client</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty.http2</groupId>
+      <artifactId>http2-http-client-transport</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Provides HTTP/2 support in the Jetty server.
+    N.B.: ALPN support is in a different module. -->
+    <dependency>
+      <groupId>org.eclipse.jetty.http2</groupId>
+      <artifactId>http2-server</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Enables support for ALPN in Jetty client.
+    N.B.: in Java 8, this requires an alpn-boot JAR on the boot classpath. -->
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-alpn-client</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Enables support for ALPN in Jetty server.
+    N.B.: in Java 8, this requires an alpn-boot JAR on the boot classpath. -->
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-alpn-server</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Enables Jetty client to support the ALPN built into Java 9 -->
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-alpn-java-client</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Enables Jetty server to support the ALPN built into Java 9 -->
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-alpn-java-server</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Used by HttpSource -->
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-http</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-io</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-client</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Provides the standalone servlet container -->
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-util</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-server</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-servlet</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-webapp</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <!-- Enables colorized Logback console output in Windows -->
+    <dependency>
+      <groupId>org.fusesource.jansi</groupId>
+      <artifactId>jansi</artifactId>
+      <version>1.13</version>
+    </dependency>
+    <!-- JRuby is used to execute delegate script methods. -->
+    <dependency>
+      <groupId>org.jruby</groupId>
+      <artifactId>jruby-core</artifactId>
+      <version>${jruby.version}</version>
+    </dependency>
+    <!-- Needed by tests, but not necessary at compile time. -->
+    <dependency>
+      <groupId>org.jruby</groupId>
+      <artifactId>jruby-stdlib</artifactId>
+      <version>${jruby.version}</version>
+      <scope>runtime</scope>
+    </dependency>
+    <!-- Restlet is our REST framework. -->
+    <dependency>
+      <groupId>org.restlet.jee</groupId>
+      <artifactId>org.restlet</artifactId>
+      <version>${restlet.version}</version>
+    </dependency>
+    <!-- Jackson for data serialization -->
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-core</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
+    <!-- Make Jackson aware of JDK8 date/time objects -->
+    <dependency>
+      <groupId>com.fasterxml.jackson.datatype</groupId>
+      <artifactId>jackson-datatype-jsr310</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
+    <!-- Makes Restlet log to SLF4J -->
+    <dependency>
+      <groupId>org.restlet.jee</groupId>
+      <artifactId>org.restlet.ext.slf4j</artifactId>
+      <version>${restlet.version}</version>
+    </dependency>
+    <!-- Enable usage of the application in a Servlet container -->
+    <dependency>
+      <groupId>org.restlet.jee</groupId>
+      <artifactId>org.restlet.ext.servlet</artifactId>
+      <version>${restlet.version}</version>
+    </dependency>
+    <!-- Provides Velocity for the landing and error page templates -->
+    <dependency>
+      <groupId>org.apache.velocity</groupId>
+      <artifactId>velocity-engine-core</artifactId>
+      <version>2.0</version>
+    </dependency>
+    <!-- Selenium is used to test the control panel, using a headless HtmlUnit
+    browser. -->
+    <dependency>
+      <groupId>org.seleniumhq.selenium</groupId>
+      <artifactId>htmlunit-driver</artifactId>
+      <version>2.21</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.seleniumhq.selenium</groupId>
+      <artifactId>selenium-api</artifactId>
+      <version>2.53.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.seleniumhq.selenium</groupId>
+      <artifactId>selenium-support</artifactId>
+      <version>2.53.0</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Logging interface used by the application -->
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <version>1.7.25</version>
+    </dependency>
+    <!-- intercepts jcl log messages from Commons etc. -->
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <version>1.7.25</version>
+    </dependency>
+  </dependencies>
+
+  <repositories>
+    <!-- Provides imageio-ext-tiff -->
+    <repository>
+      <id>imageio-ext-repository</id>
+      <name>imageio-ext Repository</name>
+      <url>http://maven.geo-solutions.it/</url>
+    </repository>
+    <!-- Provides Restlet -->
+    <repository>
+      <id>maven-restlet</id>
+      <name>Restlet repository</name>
+      <url>http://maven.restlet.com</url>
+    </repository>
+  </repositories>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>${surefire.version}</version>
+        <configuration>
+          <runOrder>random</runOrder>
+          <reuseForks>false</reuseForks>
+        </configuration>
+      </plugin>
+
+      <!-- Find bugs with mvn findbugs:findbugs findbugs:gui -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>findbugs-maven-plugin</artifactId>
+        <version>3.0.1</version>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.3</version>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+          <useIncrementalCompilation>false</useIncrementalCompilation>
+        </configuration>
+      </plugin>
+
+      <!-- About the build: it's easy to build a JAR using the Maven Shade
+      plugin, and it's easy to build a WAR using the Maven WAR plugin. However,
+      this application is packaged as a special WAR that can also be run like a
+      JAR, and there's no all-in-one Maven plugin for that. So, from here, we
+      have to go through a sequence of steps to build the custom WAR. -->
+
+      <!-- The first step is to unpack dependencies (direct and transient)
+      needed in standalone mode into the root, for compatibility with
+      `java -jar` invocation. Note that this plugin is not smart about handling
+      dependencies with the same filename, which mainly is a problem with
+      same-named META-INF/services files from e.g. Jetty, so we will have to
+      work around that in a later step. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>3.0.2</version>
+        <executions>
+          <execution>
+            <id>unpack-dependencies</id>
+            <phase>prepare-package</phase>
+            <goals>
+              <goal>unpack-dependencies</goal>
+            </goals>
+            <configuration>
+              <excludes>**/META-INF/*.DSA, **/META-INF/*.MF,
+                  **/META-INF/*.RSA, **/META-INF/*.SF,
+                  **/META-INF/*.txt</excludes>
+              <includeGroupIds>
+                commons-configuration,
+                    commons-lang,
+                    commons-logging,
+                javax.servlet,
+                org.eclipse.jetty,
+		org.greenstone, log4j
+              </includeGroupIds>
+              <outputDirectory>
+                ${project.build.directory}/${project.artifactId}-${project.version}
+              </outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <!-- The previous step copied only dependencies. Now we copy the
+      application classes needed for standalone mode into the root. -->
+      <plugin>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-standalone-classes-to-root</id>
+            <phase>prepare-package</phase>
+            <goals>
+              <goal>run</goal>
+            </goals>
+            <configuration>
+              <tasks>
+                <copy todir="${project.build.directory}/${project.artifactId}-${project.version}/">
+                  <fileset dir="${project.build.directory}/classes/">
+                    <include name="edu/illinois/library/cantaloupe/config/*" />
+                    <include name="edu/illinois/library/cantaloupe/util/FilesystemWatcher.class" />
+                    <include name="edu/illinois/library/cantaloupe/util/FilesystemWatcher$Callback.class" />
+                    <include name="edu/illinois/library/cantaloupe/util/SystemUtils.class" />
+                    <include name="edu/illinois/library/cantaloupe/Application.class" />
+                    <include name="edu/illinois/library/cantaloupe/ApplicationServer.class" />
+                    <include name="edu/illinois/library/cantaloupe/StandaloneEntry.class" />
+                  </fileset>
+                </copy>
+              </tasks>
+            </configuration>
+          </execution>
+
+          <!-- And also add back a missing Jetty service that didn't get
+          copied in the unpack-dependencies execution. -->
+          <execution>
+            <id>finalize-services</id>
+            <phase>prepare-package</phase>
+            <goals>
+              <goal>run</goal>
+            </goals>
+            <configuration>
+              <tasks>
+                <echo file="${project.build.directory}/${project.artifactId}-${project.version}/META-INF/services/org.eclipse.jetty.http.HttpFieldPreEncoder" append="true">
+                  ${line.separator}org.eclipse.jetty.http2.hpack.HpackFieldPreEncoder
+                </echo>
+              </tasks>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <!-- Create the war file from the stuff we just unpacked. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <version>3.2.0</version>
+        <executions>
+          <execution>
+            <id>create-war</id>
+            <phase>package</phase>
+            <goals>
+              <goal>war</goal>
+            </goals>
+            <configuration>
+              <archive>
+                <manifest>
+                  <mainClass>edu.illinois.library.cantaloupe.StandaloneEntry</mainClass>
+                  <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                  <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
+                </manifest>
+              </archive>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <!-- Assemble the created war into a release package. -->
+      <plugin>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <version>3.1.0</version>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <appendAssemblyId>false</appendAssemblyId>
+          <descriptors>
+            <descriptor>${basedir}/build/assembly.xml</descriptor>
+          </descriptors>
+        </configuration>
+        <!-- This plugin depends on a version of plexus-archiver that
+        doesn't work with JDK 9. So, use a version that does.
+        TODO: revisit this -->
+        <dependencies>
+          <dependency>
+             <groupId>org.codehaus.plexus</groupId>
+             <artifactId>plexus-archiver</artifactId>
+             <version>3.5</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+
+      <!-- Enables running with mvn exec:java -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.2.1</version>
+        <configuration>
+          <mainClass>edu.illinois.library.cantaloupe.StandaloneEntry</mainClass>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>nodeps</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <version>${surefire.version}</version>
+            <configuration>
+              <runOrder>random</runOrder>
+              <reuseForks>false</reuseForks>
+              <excludes>
+                <exclude>AzureStorage*Test</exclude>
+                <exclude>FfmpegProcessorTest</exclude>
+                <exclude>GraphicsMagickProcessorTest</exclude>
+                <exclude>ImageMagickProcessorTest</exclude>
+                <exclude>KakaduDemoProcessorTest</exclude>
+                <exclude>KakaduNativeProcessorTest</exclude>
+                <exclude>OpenJpegProcessorTest</exclude>
+                <exclude>RedisCacheTest</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>freedeps</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <version>${surefire.version}</version>
+            <configuration>
+              <runOrder>random</runOrder>
+              <reuseForks>false</reuseForks>
+              <excludes>
+                <exclude>Azure*Test</exclude>
+                <!-- openjpeg is free but there is no package containing opj_decompress in trusty -->
+                <exclude>OpenJpeg*Test</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>benchmark</id>
+      <properties>
+        <skipTests>true</skipTests>
+        <benchmark>edu.illinois</benchmark>
+      </properties>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <testIncludes>
+                <testInclude>**/*</testInclude>
+              </testIncludes>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>exec-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>benchmark</id>
+                <phase>test</phase>
+                <goals>
+                  <goal>exec</goal>
+                </goals>
+                <configuration>
+                  <classpathScope>test</classpathScope>
+                  <executable>java</executable>
+                  <arguments>
+                    <argument>-classpath</argument>
+                    <classpath/>
+                    <argument>org.openjdk.jmh.Main</argument>
+                    <argument>-rf</argument>
+                    <argument>json</argument>
+                    <argument>-rff</argument>
+                    <argument>target/jmh-result.${benchmark}.json</argument>
+                    <argument>${benchmark}</argument>
+                  </arguments>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
Index: gs3-extensions/iiif-servlet/trunk/src/cantaloupe.properties.svn
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/cantaloupe.properties.svn	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/cantaloupe.properties.svn	(revision 32707)
@@ -0,0 +1,708 @@
+###########################################################################
+# Sample Cantaloupe configuration file
+#
+# Copy this file to `cantaloupe.properties` and edit as desired.
+#
+# Keys may change from version to version. See the "Upgrading" section of
+# the website.
+#
+# Most changes will take effect without restarting. Those that won't are
+# marked with "!!".
+###########################################################################
+
+# !! Leave blank to use the JVM default temporary directory.
+temp_pathname =
+
+# !! Configures the HTTP server. (Standalone mode only.)
+http.enabled = true
+http.host = 0.0.0.0
+http.port = 8182
+http.http2.enabled = false
+
+# !! Configures the HTTPS server. (Standalone mode only.)
+https.enabled = false
+https.host = 0.0.0.0
+https.port = 8183
+# Secure HTTP/2 requires Java 9 or later.
+https.http2.enabled = false
+
+# !! Available values are `JKS` and `PKCS12`. (Standalone mode only.)
+https.key_store_type = JKS
+https.key_store_password = myPassword
+https.key_store_path = /path/to/keystore.jks
+https.key_password = myPassword
+
+# !! Maximum size of the HTTP(S) request queue. Leave blank to use the
+# default.
+http.accept_queue_limit =
+
+# Base URI to use for internal links, such as Link headers and JSON-LD
+# @id values, in a reverse-proxy context. This should only be used when
+# X-Forwarded-* headers cannot be used instead. (See the user manual.)
+base_uri =
+
+# Normally, slashes in a URI path component must be percent-encoded as
+# "%2F". If your proxy is not able to pass these through without decoding,
+# you can define an alternate character or character sequence to substitute
+# for a slash. Supply the non-percent-encoded version here, and use the
+# percent-encoded version in URLs.
+slash_substitute =
+
+# Maximum number of pixels to return in a response, to prevent overloading
+# the server. Requests for more pixels than this will receive an error
+# response. Set to 0 for no maximum.
+max_pixels = 400000000
+
+# Errors will also be logged to the error log (if enabled).
+print_stack_trace_on_error_pages = true
+
+###########################################################################
+# DELEGATE SCRIPT
+###########################################################################
+
+# Enables the delegate script: a Ruby script containing various delegate
+# methods. (See the user manual.)
+delegate_script.enabled = false
+
+# !! This can be an absolute path, or a filename; if only a filename is
+# specified, it will be searched for in the same folder as this file, and
+# then the current working directory.
+delegate_script.pathname = delegates.rb
+
+# Enables the invocation cache, which caches method invocations and return
+# values in memory. See the user manual for more information.
+delegate_script.cache.enabled = false
+
+###########################################################################
+# ENDPOINTS
+###########################################################################
+
+# !! Configures HTTP Basic authentication in all public endpoints.
+endpoint.public.auth.basic.enabled = false
+endpoint.public.auth.basic.username = myself
+endpoint.public.auth.basic.secret = mypassword
+
+# Enables the IIIF Image API 1.x endpoint, at /iiif/1.
+endpoint.iiif.1.enabled = false
+
+# Enables the IIIF Image API 2.x endpoint, at /iiif/2.
+endpoint.iiif.2.enabled = true
+
+# Controls the response Content-Disposition header for images. Allowed
+# values are `inline`, `attachment`, and `none`. This can be overridden
+# using the ?response-content-disposition query argument.
+endpoint.iiif.content_disposition = inline
+
+# Minimum size that will be used in info.json `sizes` keys.
+endpoint.iiif.min_size = 64
+
+# Minimum size that will be used in info.json `tiles` keys. The user manual
+# explains how these are calculated.
+endpoint.iiif.min_tile_size = 512
+
+# If true, requests for sizes other than those contained in an information
+# response will be denied.
+endpoint.iiif.2.restrict_to_sizes = false
+
+# Enables the Control Panel, at /admin.
+endpoint.admin.enabled = false
+endpoint.admin.username = admin
+endpoint.admin.secret =
+
+# Enables the administrative HTTP API. (See the user manual.)
+endpoint.api.enabled = false
+
+# HTTP Basic credentials to access the HTTP API.
+endpoint.api.username =
+endpoint.api.secret =
+
+###########################################################################
+# SOURCES
+###########################################################################
+
+# Uses one source for all requests. Available values are `FilesystemSource`,
+# `HttpSource`, `JdbcSource`, `S3Source`, and `AzureStorageSource`.
+source.static = FilesystemSource
+
+# If true, `source.static` will be overridden, and the `source()` delegate
+# method will be used to select a source per-request.
+source.delegate = false
+
+#----------------------------------------
+# FilesystemSource
+#----------------------------------------
+
+# How to look up files. Allowed values are `BasicLookupStrategy` and
+# `ScriptLookupStrategy`. ScriptLookupStrategy uses the delegate script for
+# dynamic lookups; see the user manual.
+FilesystemSource.lookup_strategy = BasicLookupStrategy
+
+# Server-side path that will be prefixed to the identifier in the URL.
+# Trailing slash is important!
+FilesystemSource.BasicLookupStrategy.path_prefix = /home/osboxes/research/code/greenstone3-svn/ext/cantaloupe/packages/src/cantaloupe-4.0.2/sample-images
+
+
+# Server-side path or extension that will be suffixed to the identifier in
+# the URL.
+FilesystemSource.BasicLookupStrategy.path_suffix =
+
+#----------------------------------------
+# HttpSource
+#----------------------------------------
+
+HttpSource.trust_all_certs = false
+HttpSource.request_timeout =
+
+# Tells HttpSource how to look up resources. Allowed values are
+# `BasicLookupStrategy` and `ScriptLookupStrategy`. ScriptLookupStrategy
+# uses a delegate method for dynamic lookups; see the user manual.
+HttpSource.lookup_strategy = BasicLookupStrategy
+
+# URL that will be prefixed to the identifier in the request URL.
+# Trailing slash is important!
+HttpSource.BasicLookupStrategy.url_prefix = http://localhost/images/
+
+# Path, extension, query string, etc. that will be suffixed to the
+# identifier in the request URL.
+HttpSource.BasicLookupStrategy.url_suffix =
+
+# Enables access to resources that require HTTP Basic authentication.
+HttpSource.BasicLookupStrategy.auth.basic.username =
+HttpSource.BasicLookupStrategy.auth.basic.secret =
+
+#----------------------------------------
+# JdbcSource
+#----------------------------------------
+
+# Note: JdbcSource requires some delegate methods to be implemented in
+# addition to the configuration here, and a JDBC driver to be installed on
+# the classpath; see the user manual.
+
+# !!
+JdbcSource.url = jdbc:postgresql://localhost:5432/my_database
+# !!
+JdbcSource.user = postgres
+# !!
+JdbcSource.password = postgres
+
+# !! Connection timeout in seconds.
+JdbcSource.connection_timeout = 10
+
+#----------------------------------------
+# S3Source
+#----------------------------------------
+
+# !! Endpoint URI.
+# For AWS endpoints, see:
+# https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
+S3Source.endpoint =
+
+# !! Credentials for your AWS account.
+# See: http://aws.amazon.com/security-credentials
+# Note that this info can be obtained from elsewhere rather than setting
+# it here; see the user manual.
+S3Source.access_key_id =
+S3Source.secret_key =
+
+# !! Maximum number of concurrent HTTP connections to AWS. Leave blank to
+# use the default.
+S3Source.max_connections =
+
+# Tells S3Source how to look up objects. Allowed values are
+# `BasicLookupStrategy` and `ScriptLookupStrategy`. ScriptLookupStrategy
+# uses a delegate method for dynamic lookups; see the user manual.
+S3Source.lookup_strategy = BasicLookupStrategy
+
+# !! Name of the bucket containing images to be served.
+S3Source.BasicLookupStrategy.bucket.name =
+
+# Path within the bucket that will be prefixed to the identifier in the URL.
+# Trailing slash is important!
+S3Source.BasicLookupStrategy.path_prefix =
+
+# Path or extension that will be suffixed to the identifier in the URL.
+S3Source.BasicLookupStrategy.path_suffix =
+
+#----------------------------------------
+# AzureStorageSource
+#----------------------------------------
+
+# !! Name of your Azure account.
+# Leave blank if using URI with a SAS token in your object key.
+AzureStorageSource.account_name =
+
+# !! Key of your Azure account.
+# Leave blank if using URI with a SAS token in your object key.
+AzureStorageSource.account_key =
+
+# !! Name of the container containing images to be served.
+# Leave blank if using URI with the container in your object key.
+AzureStorageSource.container_name =
+
+# Tells AzureStorageSource how to look up objects. Allowed values are
+# `BasicLookupStrategy` and `ScriptLookupStrategy`. ScriptLookupStrategy
+# uses a delegate method for dynamic lookups; see the user manual.
+AzureStorageSource.lookup_strategy = BasicLookupStrategy
+
+###########################################################################
+# PROCESSORS
+###########################################################################
+
+#----------------------------------------
+# Processor Selection
+#----------------------------------------
+
+# Image processors to use for various source formats. Available values are
+# `Java2dProcessor`, `GraphicsMagickProcessor`, `ImageMagickProcessor`,
+# `KakaduDemoProcessor`, `KakaduNativeProcessor`, `OpenJpegProcessor`,
+# `JaiProcessor`, `PdfBoxProcessor`, and `FfmpegProcessor`.
+
+# These extension-specific definitions are optional.
+processor.avi = FfmpegProcessor
+processor.bmp =
+processor.dcm = ImageMagickProcessor
+processor.flv = FfmpegProcessor
+processor.gif =
+processor.jp2 = KakaduNativeProcessor
+processor.jpg =
+processor.mov = FfmpegProcessor
+processor.mp4 = FfmpegProcessor
+processor.mpg = FfmpegProcessor
+processor.pdf = PdfBoxProcessor
+processor.png =
+processor.tif =
+processor.webm = FfmpegProcessor
+processor.webp = ImageMagickProcessor
+
+# Fall back to this processor for any formats not assigned above.
+processor.fallback = Java2dProcessor
+
+#----------------------------------------
+# Global Processor Configuration
+#----------------------------------------
+
+# Controls how content is fed to processors from stream-based sources.
+# * `StreamStrategy` will try to stream a source image from a source when
+#   possible, and use `processor.fallback_retrieval_strategy` otherwise.
+# * `DownloadStrategy` will download it to a temporary file, and delete
+#   it after the request is complete.
+# * `CacheStrategy` will download it into the source cache using
+#   FilesystemCache, which must also be configured. (This will perform a
+#   lot better than DownloadStrategy if you can spare the disk space.)
+processor.stream_retrieval_strategy = StreamStrategy
+
+# Controls how an incompatible StreamSource + FileProcessor combination is
+# dealt with.
+# * `DownloadStrategy` and `CacheStrategy` work the same as above.
+# * `AbortStrategy` causes the request to fail.
+processor.fallback_retrieval_strategy = DownloadStrategy
+
+# Resolution of vector rasterization (of e.g. PDFs) at a scale of 1.
+processor.dpi = 150
+
+# Expands contrast to utilize available dynamic range. This usually requires
+# the whole source image to be read into memory, so it can be inefficient.
+processor.normalize = false
+
+# Color of the background when an image is rotated or alpha-flattened, for
+# output formats that don't support transparency.
+# This may not be respected for indexed color derivative images.
+processor.background_color = white
+
+# Available values are `bell`, `bspline`, `bicubic`, `box`, `hermite`,
+# `lanczos3`, `mitchell`, `triangle`. (JaiProcessor & KakaduNativeProcessor
+# ignore these.)
+processor.downscale_filter = bicubic
+processor.upscale_filter = bicubic
+
+# Intensity of an unsharp mask from 0 to 1.
+processor.sharpen = 0
+
+# Attempts to copy source image metadata (EXIF, IPTC, XMP) into derivative
+# images. (This is not foolproof; see the user manual.)
+processor.metadata.preserve = false
+
+# Whether to auto-rotate images using the EXIF `Orientation` field.
+# The check for this field can impair performance slightly.
+processor.metadata.respect_orientation = false
+
+# Progressive JPEGs are usually more compact.
+processor.jpg.progressive = true
+
+# JPEG output quality (1-100).
+processor.jpg.quality = 80
+
+# TIFF output compression type. Available values are `Deflate`, `JPEG`,
+# `LZW`, and `RLE`. Leave blank for no compression.
+processor.tif.compression = LZW
+
+#----------------------------------------
+# ImageIO Plugin Preferences
+#----------------------------------------
+
+# These override the default plugins used by the application and should not
+# normally be changed.
+processor.imageio.bmp.reader =
+processor.imageio.gif.reader =
+processor.imageio.gif.writer =
+processor.imageio.jpg.reader =
+processor.imageio.jpg.writer =
+processor.imageio.png.reader =
+processor.imageio.png.writer =
+processor.imageio.tif.reader =
+processor.imageio.tif.writer =
+
+#----------------------------------------
+# FfmpegProcessor
+#----------------------------------------
+
+# Optional absolute path of the directory containing the FFmpeg binaries.
+# Overrides the PATH.
+FfmpegProcessor.path_to_binaries =
+
+#----------------------------------------
+# GraphicsMagickProcessor
+#----------------------------------------
+
+# !! Optional absolute path of the directory containing the GraphicsMagick
+# binary. Overrides the PATH.
+GraphicsMagickProcessor.path_to_binaries =
+
+#----------------------------------------
+# ImageMagickProcessor
+#----------------------------------------
+
+# !! Optional absolute path of the directory containing the ImageMagick
+# binary. Overrides the PATH.
+ImageMagickProcessor.path_to_binaries =
+
+#----------------------------------------
+# KakaduDemoProcessor
+#----------------------------------------
+
+# Optional absolute path of the directory containing kdu_expand.
+# Overrides the PATH.
+KakaduDemoProcessor.path_to_binaries =
+
+#----------------------------------------
+# OpenJpegProcessor
+#----------------------------------------
+
+# Optional absolute path of the directory containing opj_decompress.
+# Overrides the PATH.
+OpenJpegProcessor.path_to_binaries =
+
+###########################################################################
+# CLIENT-SIDE CACHING
+###########################################################################
+
+# Whether to enable the response Cache-Control header.
+cache.client.enabled = true
+
+cache.client.max_age = 2592000
+cache.client.shared_max_age =
+cache.client.public = true
+cache.client.private = false
+cache.client.no_cache = false
+cache.client.no_store = false
+cache.client.must_revalidate = false
+cache.client.proxy_revalidate = false
+cache.client.no_transform = true
+
+###########################################################################
+# SERVER-SIDE CACHING
+###########################################################################
+
+# N.B.: The source cache may be used if the
+# `processor.stream_retrieval_strategy` and/or
+# `processor.fallback_retrieval_strategy` keys are set to `CacheStrategy`.
+
+# FilesystemCache is the only available source cache.
+cache.server.source = FilesystemCache
+
+# Amount of time source cache content remains valid. Set to blank or 0
+# for forever.
+cache.server.source.ttl_seconds = 2592000
+
+# Enables the derivative (processed image) cache.
+cache.server.derivative.enabled = false
+
+# Available values are `FilesystemCache`, `JdbcCache`, `RedisCache`,
+# `HeapCache`, `S3Cache`, and `AzureStorageCache`.
+cache.server.derivative =
+
+# Amount of time derivative cache content remains valid. Set to blank or 0
+# for forever.
+cache.server.derivative.ttl_seconds = 2592000
+
+# Whether to use the Java heap as a "level 1" cache for image infos, either
+# independently or in front of a "level 2" derivative cache (if enabled).
+cache.server.info.enabled = true
+
+# If true, when a source reports that the requested source image has gone
+# missing, all cached information relating to it (if any) will be deleted.
+# (This is effectively always false when cache.server.resolve_first is also
+# false.)
+cache.server.purge_missing = false
+
+# If true, the source image will be confirmed to exist before a cached copy
+# is returned. If false, the cached copy will be returned without checking.
+# Resolving first is safer but slower.
+cache.server.resolve_first = false
+
+# !! Enables the cache worker, which periodically purges invalid cache
+# items in the background.
+cache.server.worker.enabled = false
+
+# !! The cache worker will wait this many seconds before starting its
+# next shift.
+cache.server.worker.interval = 86400
+
+#----------------------------------------
+# FilesystemCache
+#----------------------------------------
+
+# If this directory does not exist, it will be created automatically.
+FilesystemCache.pathname = /var/cache/cantaloupe
+
+# Levels of folder hierarchy in which to store cached images. Deeper depth
+# results in fewer files per directory. Set to 0 to disable subdirectories.
+# Purge the cache after changing this.
+FilesystemCache.dir.depth = 3
+
+# Number of characters in tree directory names. Should be set to
+# 16^n < (max number of directory entries your filesystem can deal with).
+# Purge the cache after changing this.
+FilesystemCache.dir.name_length = 2
+
+#----------------------------------------
+# HeapCache
+#----------------------------------------
+
+# Target cache size, in bytes or a number ending in M, MB, G, GB, etc.
+# This is not a hard limit, and may be transiently exceeded.
+# Ensure your heap can accommodate this size.
+HeapCache.target_size = 2G
+
+# If true, the cache contents will be written to a file on exit and during
+# cache worker shifts, and read back in at startup.
+HeapCache.persist = false
+
+# When the contents are persisted, this specifies the location of the cache
+# file. If the parent directory does not exist, it will be created
+# automatically.
+HeapCache.persist.filesystem.pathname = /var/cache/cantaloupe/heap.cache
+
+#----------------------------------------
+# JdbcCache
+#----------------------------------------
+
+# !!
+JdbcCache.url = jdbc:postgresql://localhost:5432/cantaloupe
+# !!
+JdbcCache.user = postgres
+# !!
+JdbcCache.password =
+
+# !! Connection timeout in seconds.
+JdbcCache.connection_timeout = 10
+
+# These must be created manually; see the user manual.
+JdbcCache.derivative_image_table = derivative_cache
+JdbcCache.info_table = info_cache
+
+#----------------------------------------
+# S3Cache
+#----------------------------------------
+
+# !! Endpoint URI.
+# For AWS endpoints, see:
+# https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
+S3Cache.endpoint =
+
+# !! Credentials for your AWS account.
+# See: http://aws.amazon.com/security-credentials
+# Note that this info can be obtained from elsewhere rather than setting it
+# here; see the user manual.
+S3Cache.access_key_id =
+S3Cache.secret_key =
+
+# !! Name of a bucket to use to hold cached data.
+S3Cache.bucket.name =
+
+# !! String that will be prefixed to object keys.
+S3Cache.object_key_prefix =
+
+# !! Maximum number of concurrent HTTP connections to AWS. Leave blank to
+# use the default.
+S3Cache.max_connections =
+
+#----------------------------------------
+# AzureStorageCache
+#----------------------------------------
+
+# !! Credentials for your Azure account.
+AzureStorageCache.account_name =
+AzureStorageCache.account_key =
+
+# !! Name of the container containing cached images.
+AzureStorageCache.container_name =
+
+# !! String that will be prefixed to object keys.
+AzureStorageCache.object_key_prefix =
+
+#----------------------------------------
+# RedisCache
+#----------------------------------------
+
+# !! Redis connection info.
+RedisCache.host = localhost
+RedisCache.port = 6379
+RedisCache.ssl = false
+RedisCache.password =
+RedisCache.database = 0
+
+###########################################################################
+# OVERLAYS
+###########################################################################
+
+# Whether to enable overlays.
+overlays.enabled = false
+
+# Controls how overlays are configured. `BasicStrategy` will use the
+# `overlays.BasicStrategy.*` keys in this section. `ScriptStrategy` will
+# use a delegate method. (See the user manual.)
+overlays.strategy = BasicStrategy
+
+# `image` or `string`.
+overlays.BasicStrategy.type = image
+
+# Absolute path or URL of the overlay image. Must be a PNG file.
+overlays.BasicStrategy.image = /path/to/overlay.png
+
+# Overlay text.
+overlays.BasicStrategy.string = Copyright \u00A9ïž My Great Organization\nAll rights reserved.
+
+# For possible values, launch with the -Dcantaloupe.list_fonts option.
+overlays.BasicStrategy.string.font = Helvetica
+
+# Font size in points.
+overlays.BasicStrategy.string.font.size = 24
+
+# If the string doesn't fit in the image at the above size, the largest size
+# at which it does fit will be used, down to this.
+overlays.BasicStrategy.string.font.min_size = 18
+
+# Font weight. 1 = regular, 2 = bold. Unfortunately, many fonts don't
+# support fractional weights.
+overlays.BasicStrategy.string.font.weight = 1.0
+
+# Point spacing between glyphs, typically between -0.1 and 0.1.
+overlays.BasicStrategy.string.glyph_spacing = 0.02
+
+# CSS color syntax is supported.
+overlays.BasicStrategy.string.color = white
+
+# CSS color syntax is supported.
+overlays.BasicStrategy.string.stroke.color = black
+
+# Stroke width in pixels.
+overlays.BasicStrategy.string.stroke.width = 1
+
+# Color of a rectangular background to draw under the string.
+# CSS color syntax and alpha are supported.
+overlays.BasicStrategy.string.background.color = rgba(0, 0, 0, 100)
+
+# Allowed values: `top left`, `top center`, `top right`, `left center`,
+# `center`, `right center`, `bottom left`, `bottom center`, `bottom right`.
+overlays.BasicStrategy.position = bottom right
+
+# Pixel margin between the overlay and the image edge.
+overlays.BasicStrategy.inset = 10
+
+# Output images less than this many pixels wide will not receive an overlay.
+# Set to 0 to add the overlay regardless.
+overlays.BasicStrategy.output_width_threshold = 400
+
+# Output images less than this many pixels tall will not receive an overlay.
+# Set to 0 to add the overlay regardless.
+overlays.BasicStrategy.output_height_threshold = 300
+
+###########################################################################
+# REDACTIONS
+###########################################################################
+
+# See the user manual for information about how redactions work.
+redaction.enabled = false
+
+###########################################################################
+# LOGGING
+###########################################################################
+
+#----------------------------------------
+# Application Log
+#----------------------------------------
+
+# `trace`, `debug`, `info`, `warn`, `error`, `all`, or `off`
+log.application.level = debug
+
+log.application.ConsoleAppender.enabled = true
+
+# N.B.: Don't enable FileAppender and RollingFileAppender simultaneously!
+log.application.FileAppender.enabled = false
+log.application.FileAppender.pathname = /path/to/logs/application.log
+
+log.application.RollingFileAppender.enabled = false
+log.application.RollingFileAppender.pathname = /path/to/logs/application.log
+log.application.RollingFileAppender.policy = TimeBasedRollingPolicy
+log.application.RollingFileAppender.TimeBasedRollingPolicy.filename_pattern = /path/to/logs/application-%d{yyyy-MM-dd}.log
+log.application.RollingFileAppender.TimeBasedRollingPolicy.max_history = 30
+
+# See the "SyslogAppender" section for a list of facilities:
+# http://logback.qos.ch/manual/appenders.html
+log.application.SyslogAppender.enabled = false
+log.application.SyslogAppender.host =
+log.application.SyslogAppender.port = 514
+log.application.SyslogAppender.facility = LOCAL0
+
+#----------------------------------------
+# Error Log
+#----------------------------------------
+
+# Application log messages with a severity of WARN or greater can be copied
+# into a dedicated error log, which may make them easier to spot.
+
+# N.B.: Don't enable FileAppender and RollingFileAppender simultaneously!
+log.error.FileAppender.enabled = false
+log.error.FileAppender.pathname = /path/to/logs/error.log
+
+log.error.RollingFileAppender.enabled = false
+log.error.RollingFileAppender.pathname = /path/to/logs/error.log
+log.error.RollingFileAppender.policy = TimeBasedRollingPolicy
+log.error.RollingFileAppender.TimeBasedRollingPolicy.filename_pattern = /path/to/logs/error-%d{yyyy-MM-dd}.log
+log.error.RollingFileAppender.TimeBasedRollingPolicy.max_history = 30
+
+#----------------------------------------
+# Access Log
+#----------------------------------------
+
+log.access.ConsoleAppender.enabled = false
+
+# N.B.: Don't enable FileAppender and RollingFileAppender simultaneously!
+log.access.FileAppender.enabled = false
+log.access.FileAppender.pathname = /path/to/logs/access.log
+
+# RollingFileAppender is an alternative to using something like
+# FileAppender + logrotate.
+log.access.RollingFileAppender.enabled = false
+log.access.RollingFileAppender.pathname = /path/to/logs/access.log
+log.access.RollingFileAppender.policy = TimeBasedRollingPolicy
+log.access.RollingFileAppender.TimeBasedRollingPolicy.filename_pattern = /path/to/logs/access-%d{yyyy-MM-dd}.log
+log.access.RollingFileAppender.TimeBasedRollingPolicy.max_history = 30
+
+# See the "SyslogAppender" section for a list of facilities:
+# http://logback.qos.ch/manual/appenders.html
+log.access.SyslogAppender.enabled = false
+log.access.SyslogAppender.host =
+log.access.SyslogAppender.port = 514
+log.access.SyslogAppender.facility = LOCAL0
Index: gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/IIIFServerBridge.java
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/IIIFServerBridge.java	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/IIIFServerBridge.java	(revision 32707)
@@ -0,0 +1,271 @@
+/*
+ *    IIIFServerBridge.java
+ *    Copyright (C) 2018 New Zealand Digital Library, http://www.nzdl.org
+ *
+ *    This program is free software; you can redistribute it and/or modify
+ *    it under the terms of the GNU General Public License as published by
+ *    the Free Software Foundation; either version 2 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program; if not, write to the Free Software
+ *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package org.greenstone.gsdl3;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.UnavailableException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+import org.greenstone.gsdl3.comms.Communicator;
+import org.greenstone.gsdl3.comms.SOAPCommunicator;
+import org.greenstone.gsdl3.core.IIIFMessageRouter;
+import org.greenstone.gsdl3.core.IIIFReceptionist;
+import org.greenstone.gsdl3.util.GSConstants;
+import org.greenstone.gsdl3.util.GSParams;
+import org.greenstone.gsdl3.util.GSXML;
+import org.greenstone.gsdl3.util.OAIXML;
+import org.greenstone.gsdl3.util.XMLConverter;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/** a class the serve as a bridge between the Cantaloupe IIIF image server and 
+ * Greenstone collections.  Loosely based on OAIServer
+ *
+ * the init method is called only once - the first time the bridge is established
+ * then doGet() each time a document image request is made
+ * @see Receptionist
+ */
+/**
+ * OAI server configuration instructions *
+ * 
+ */
+public class IIIFServerBridge
+{
+	/** the receptionist to send messages to */
+	protected IIIFReceptionist recept = null;
+
+        /**
+	 * The name of the site with which we will finally be dealing, whether it is
+	 * a local site or a remote site through a communicator.
+	 */
+	protected String site = "";
+
+	static Logger logger = Logger.getLogger(org.greenstone.gsdl3.IIIFServerBridge.class.getName());
+
+        private void configure() throws UnavailableException
+        {	    
+	    // Read in OAIConfig.xml (residing web/WEB-INF/classes/) and 
+	    //use it to configure the receptionist. 
+	    Element oai_config = OAIXML.getOAIConfigXML();
+	    if (oai_config == null)
+		{
+		    logger.error("Fail to parse oai config file OAIConfig.xml.");
+		    throw new UnavailableException("IIIFServerBridge: Couldn't parse OAIConfig.xml");
+		}
+	    // pass it to the receptionist
+	    if (!this.recept.configure(oai_config)) {
+		logger.error("Couldn't configure receptionist");
+		throw new UnavailableException("IIIFServerBridge: Couldn't configure receptionist"); 
+	    }	    
+	}
+
+	/**
+	 * initialise the class
+	 */
+    public void init(String site_name) throws UnavailableException, Exception
+	{
+	    //org.greenstone.util.GlobalProperties.loadGlobalProperties("");
+	    //org.greenstone.util.GlobalProperties.loadGlobalProperties(config.getServletContext().getRealPath(""));
+	    org.greenstone.util.GlobalProperties.loadGlobalProperties("/home/osboxes/research/code/greenstone3-svn/web");
+	    java.io.InputStream in = Class.forName("org.greenstone.util.GlobalProperties").getClassLoader().getResourceAsStream("global.properties");
+
+	    //String gsdl3_writablehome = System.getProperty("gsdl3.writablehome");
+	    String tomcat_context = System.getProperty("tomcat.context");
+	    System.err.println("*** in = " + in);
+	    //System.err.println("*** gsdl3.writablehome = " + gsdl3_writablehome);
+	    System.err.println("*** tomcat.context = " + tomcat_context);
+	    
+	    // the receptionist -the servlet will talk to this
+	    this.recept = new IIIFReceptionist();
+
+	    //this site_name could consist of comma separated more than one site name.
+	    IIIFMessageRouter message_router = new IIIFMessageRouter();
+
+	    message_router.setSiteName(site_name);
+	    // lots of work is done in this step; see IIIFMessageRouter.java
+	    if (!message_router.configure()) {
+		throw new UnavailableException("IIIFServerBridge: Couldn't configure IIIFMessageRouter");
+	    }
+	    this.recept.setSiteName(site_name);
+	    this.recept.setMessageRouter(message_router);
+	    
+	    configure();
+	} // end of init()
+
+        public void remote_init(String remote_site_name, String remote_site_type, String remote_site_address) throws UnavailableException
+        { 
+	    if (remote_site_name == null || remote_site_type == null || remote_site_address == null)
+		{
+		    logger.error("Initialisation paramters not all set!");
+		    logger.error("You must have remote_site_name, remote_site_type and remote_site_address set");
+		    throw new UnavailableException("IIIFServerBridge: incorrect remote connection parameters specified");
+		}
+
+	    // the receptionist -the servlet will talk to this
+	    this.recept = new IIIFReceptionist();
+	    
+	    // talking to a remote site, create a communicator
+	    Communicator communicator = null;
+	    // we need to create the XML to configure the communicator
+	    Document site_doc = XMLConverter.newDOM();
+	    Element site_elem = site_doc.createElement(GSXML.SITE_ELEM);
+	    site_elem.setAttribute(GSXML.TYPE_ATT, remote_site_type);
+	    site_elem.setAttribute(GSXML.NAME_ATT, remote_site_name);
+	    site_elem.setAttribute(GSXML.ADDRESS_ATT, remote_site_address);
+	    
+	    if (remote_site_type.equals(GSXML.COMM_TYPE_SOAP_JAVA))
+		{
+		    communicator = new SOAPCommunicator();
+		}
+	    else
+		{
+		    logger.error("IIIFServerBridge.init Error: invalid Communicator type: " + remote_site_type);
+		    throw new UnavailableException("OAIServer: invalid communicator type");
+		}
+	    
+	    if (!communicator.configure(site_elem))
+		{
+		    logger.error("IIIFServerBridge.init Error: Couldn't configure communicator");
+		    throw new UnavailableException("OAIServer: Couldn't configure communicator");
+		}
+	    this.recept.setSiteName(remote_site_name);
+	    this.recept.setMessageRouter(communicator);
+	    
+	    configure();
+	} // end of remote_init()
+
+
+        public String doGetDocumentMessage(String identifier)
+	{
+		// oai always requires the content type be text/xml
+		//request.setCharacterEncoding("UTF-8");
+	        //response.setContentType("text/xml;charset=UTF-8");
+		//PrintWriter out = response.getWriter();
+
+	        String[] pairs = null;
+	    
+		String verb = "GetRecord";
+		Document response_doc = XMLConverter.newDOM();
+		Element xml_response = OAIXML.createBasicResponse(response_doc, verb, pairs);
+		Element verb_elem = null;
+		
+		// compose the request message to the receptionist
+		Document request_doc = XMLConverter.newDOM();
+		Element xml_message = request_doc.createElement(GSXML.MESSAGE_ELEM);
+		Element xml_request = request_doc.createElement(GSXML.REQUEST_ELEM);
+		// The type attribute is set to be 'oaiService' from OAIServer to OAIReceptionist.
+		////xml_request.setAttribute(GSXML.TYPE_ATT, OAIXML.OAI_SERVICE);
+		//xml_request.setAttribute(GSXML.LANG_ATT, lang);
+		xml_request.setAttribute(GSXML.TO_ATT, verb);
+		addParams(xml_request, pairs);
+
+		//xml_request.setAttribute(GSXML.OUTPUT_ATT, output);????
+		xml_message.appendChild(xml_request);
+
+		Node xml_result = this.recept.process(xml_message);
+		if (xml_result == null)
+		{
+			logger.info("xml_result is null");
+			verb_elem = OAIXML.createErrorElement(response_doc, "Internal error", "");
+			xml_response.appendChild(verb_elem);
+		}
+		else
+		{
+
+			//
+			// All response elements are in the form (with a corresponding verb
+			// name): <message> <response> <verb> ... <resumptionToken> .. this
+			// is optional! </resumptionToken> </verb> </response> </message>
+			//
+			Node res = GSXML.getChildByTagName(xml_result, GSXML.RESPONSE_ELEM);
+			if (res == null)
+			{
+				logger.info("response element in xml_result is null");
+				verb_elem = OAIXML.createErrorElement(response_doc, "Internal error", "");
+			}
+			else
+			{
+				verb_elem = GSXML.getFirstElementChild(res);
+			}
+
+			if ( verb_elem.getTagName().equals(OAIXML.ERROR))
+			{
+			  xml_response.appendChild(response_doc.importNode(verb_elem, true));
+			}
+			else if (OAIXML.oai_version.equals(OAIXML.OAI_VERSION2)) {
+			  xml_response.appendChild(response_doc.importNode(verb_elem, true));
+			}
+			else
+			{
+				GSXML.copyAllChildren(xml_response, verb_elem);
+			}
+		}
+		/*
+		out.println("<?xml version='1.0' encoding='UTF-8' ?>");
+		if (this.use_oai_stylesheet)
+		{
+			out.println("<?xml-stylesheet type='text/xsl' href='" + this.oai_stylesheet + "' ?>\n");
+		}
+		out.println(XMLConverter.getPrettyString(xml_response));
+		*/
+		
+		return XMLConverter.getPrettyString(xml_response);
+	}
+
+	/** append parameter elements to the request sent to the receptionist */
+	public void addParams(Element request, String[] pairs)
+	{
+	  Document doc = request.getOwnerDocument();
+		// no params apart from the verb
+		if (pairs == null || pairs.length < 2)
+			return;
+
+		/**
+		 * the request xml is composed in the form: <request> <param name=.../>
+		 * <param name=.../> </request> (No paramList element in between).
+		 */
+		for (int i = 1; i < pairs.length; i++)
+		{
+			//the first pair in pairs is the verb=xxx
+			int index = pairs[i].indexOf("=");
+			if (index != -1)
+			{ //just a double check
+			  Element param = GSXML.createParameter(doc, pairs[i].substring(0, index), OAIXML.oaiDecode(pairs[i].substring(index + 1)));
+			  request.appendChild(param);
+			}
+		}
+	}
+
+
+	public void destroy()
+	{
+		recept.cleanUp();
+	}
+
+}
Index: gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/core/IIIFMessageRouter.java
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/core/IIIFMessageRouter.java	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/core/IIIFMessageRouter.java	(revision 32707)
@@ -0,0 +1,188 @@
+/*
+ *    IIIFMessageRouter.java
+ *    Copyright (C) 2018 New Zealand Digital Library, http://www.nzdl.org
+ *
+ *    This program is free software; you can redistribute it and/or modify
+ *    it under the terms of the GNU General Public License as published by
+ *    the Free Software Foundation; either version 2 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program; if not, write to the Free Software
+ *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package org.greenstone.gsdl3.core;
+
+import java.io.File;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Iterator;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.Logger;
+import org.greenstone.gsdl3.collection.OAICollection;
+import org.greenstone.gsdl3.collection.ServiceCluster;
+import org.greenstone.gsdl3.comms.Communicator;
+import org.greenstone.gsdl3.comms.SOAPCommunicator;
+import org.greenstone.gsdl3.service.ServiceRack;
+import org.greenstone.gsdl3.util.GSFile;
+import org.greenstone.gsdl3.util.GSPath;
+import org.greenstone.gsdl3.util.GSXML;
+import org.greenstone.gsdl3.util.OAIXML;
+import org.greenstone.gsdl3.util.UserContext;
+import org.greenstone.gsdl3.util.XMLConverter;
+import org.greenstone.util.GlobalProperties;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * The hub of a Greenstone IIIF image server/bridge.
+ * 
+ * A simplified version of MessageRouter for OAIServer. Only loads up collections that have OAI services.
+ */
+public class IIIFMessageRouter extends MessageRouter
+{
+
+  static Logger logger = Logger.getLogger(org.greenstone.gsdl3.core.IIIFMessageRouter.class.getName());
+
+  public Element oai_config = null;
+	//***************************************************************
+	// public methods
+	//***************************************************************
+
+	/** constructor */
+	public IIIFMessageRouter()
+	{
+	}
+
+	/**
+	 * read thru own site config file - create services and connect to sites
+	 */
+	protected boolean configureLocalSite()
+	{
+	  
+	  // this may be a reconfigure, so clean up the old moduleMap
+		cleanUpModuleMapEntire();
+
+		// for oai, we don't do anything with the site config file. But we'll read it in and keep it in case need it later, eg for replace elements when retrieving metadata - which I don't think has been implemented
+		File configFile = new File(GSFile.siteConfigFile(this.site_home));
+
+		if (!configFile.exists())
+		{
+			logger.error(" site config file: " + configFile.getPath() + " not found!");
+			return false;
+		}
+
+		Document config_doc = XMLConverter.getDOM(configFile);
+		if (config_doc == null)
+		{
+			logger.error(" couldn't parse site config file: " + configFile.getPath());
+			return false;
+		}
+
+		this.config_info = config_doc.getDocumentElement();
+
+		// this is the receptionists OAIConfig.xml. Need to rethink how the MR gets this this if we ever talk to remote site, and whether it should be using it anyway
+		this.oai_config = OAIXML.getOAIConfigXML();
+		if (this.oai_config == null)
+		{
+		  logger.error("Couldn't load in OAIConfig.xml");
+		  return false;
+		}
+		Document doc = XMLConverter.newDOM();
+		// load up the collections
+		this.collection_list = doc.createElement(GSXML.COLLECTION_ELEM + GSXML.LIST_MODIFIER);
+		configureCollections();
+
+		return true;
+
+	}
+
+
+	/**
+	 * creates and configures a new collection if this is done for a
+	 * reconfigure, the collection should be deactivated first.
+	 * 
+	 * @param col_name
+	 *            the name of the collection
+	 * @return true if collection created ok
+	 */
+	protected boolean activateCollectionByName(String col_name)
+	{
+
+		logger.info("Activating collection: " + col_name + ".");
+		Document doc = this.collection_list.getOwnerDocument();
+		// use our special OAICollection - this will only load in oai services
+		OAICollection c = new OAICollection();
+
+		c.setCollectionName(col_name);
+		c.setSiteHome(this.site_home);
+		c.setSiteAddress(this.site_http_address);
+		c.setMessageRouter(this);
+		if (!c.configure()) {
+		  logger.error("Couldn't configure collection: " + col_name + ".");
+		  return false;
+		}
+
+		logger.info("have just configured collection " + col_name);
+		if (!c.hasOAI()) {
+		  logger.info ("collection "+col_name+" has no OAI services. Not keeping it loaded");
+		  return false;
+		}
+		if (!c.configureOAI(this.oai_config)) {
+		  logger.info("couldn't configure the collection : "+col_name +" with the oai config info");
+		  return false;
+		}
+		// add to list of collections
+		this.module_map.put(col_name, c);
+		Element e = doc.createElement(GSXML.COLLECTION_ELEM);
+		e.setAttribute(GSXML.NAME_ATT, col_name);
+		e.setAttribute(OAIXML.LASTMODIFIED, "" + c.getLastmodified());
+		e.setAttribute(OAIXML.EARLIEST_DATESTAMP, "" + c.getEarliestDatestamp());
+		e.setAttribute(OAIXML.EARLIEST_OAI_DATESTAMP, "" + c.getEarliestOAIDatestamp());
+		this.collection_list.appendChild(e);
+		return true;
+		
+	}
+
+
+
+	//*****************************************************************
+	// auxiliary process methods
+	//*****************************************************************
+
+	/**
+	 * handles requests made to the MessageRouter itself
+	 * 
+	 * @param req
+	 *            - the request Element- <request>
+	 * @return the result Element - should be <response>
+	 */
+  protected Element processMessage(Element req) {
+    Document doc = XMLConverter.newDOM();
+    String type = req.getAttribute(GSXML.TYPE_ATT);
+    Element response = doc.createElement(GSXML.RESPONSE_ELEM);
+    response.setAttribute(GSXML.FROM_ATT, "");
+    
+    if (type.equals(OAIXML.OAI_SET_LIST))
+      {
+	logger.info("oaiSetList request received");
+	//this is the oai receptionist asking for a list of oai-support collections
+	response.setAttribute(GSXML.TYPE_ATT, OAIXML.OAI_SET_LIST);
+	response.appendChild(doc.importNode(this.collection_list, true));
+	return response;
+      }
+    return super.processMessage(req);
+  }
+
+
+}
Index: gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/core/IIIFReceptionist.java
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/core/IIIFReceptionist.java	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/gsdl-src/java/org/greenstone/gsdl3/core/IIIFReceptionist.java	(revision 32707)
@@ -0,0 +1,438 @@
+/*
+ *    IIIFReceptionist.java
+ *    Copyright (C) 2018 New Zealand Digital Library, http://www.nzdl.org
+ *
+ *    This program is free software; you can redistribute it and/or modify
+ *    it under the terms of the GNU General Public License as published by
+ *    the Free Software Foundation; either version 2 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program; if not, write to the Free Software
+ *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+package org.greenstone.gsdl3.core;
+
+import org.greenstone.gsdl3.util.*;
+import org.greenstone.gsdl3.action.*;
+// XML classes
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+// other java classes
+import java.io.File;
+import java.util.*;
+
+import org.apache.log4j.*;
+
+/** a Receptionist, used for IIIF image server support.
+ * This receptionist talks to the message router directly, 
+ * instead of via any action, hence no action map is needed.
+ * @see the basic Receptionist
+ */
+public class IIIFReceptionist implements ModuleInterface {
+  
+  static Logger logger = Logger.getLogger(org.greenstone.gsdl3.core.IIIFReceptionist.class.getName());
+  
+  /** Instead of a config_params object, only a site_name is needed by iiif receptionist. */
+  protected String site_name = null;
+  /** The unique  repository identifier */
+  protected String repository_id = null;
+      
+  /** the configure file of this receptionist passed from the iiif restlet. */
+  protected Element oai_config = null;
+
+  /** the message router that the Receptionist and Actions will talk to */
+  protected ModuleInterface mr = null;
+  
+  // Some of the data/responses will not change while the servlet is running, so
+  // we can cache them
+  
+  /** A list of all the collections available to this IIIF image server */
+  protected Element collection_list = null;
+  /** a vector of the names, for convenience */
+  protected Vector<String> collection_name_list = null;
+  /** If this is true, then there are no OAI enabled collections, so can always return 
+      noRecordsMatch (after validating the request params) */
+  protected boolean noRecordsMatch = false;
+      
+  /** A set of all known 'sets' */
+  protected HashSet<String> set_set = null;
+
+  protected boolean has_super_colls = false;
+  /** a hash of super set-> collection list */
+  protected HashMap<String, Vector<String>> super_coll_map = null;
+  /** store the super coll elements for convenience */
+  HashMap<String, Element> super_coll_data = null;
+  /** store the metadata formats ??????*/
+  /** The identify response */
+  protected Element identify_response = null;
+  /** The list set response */
+  protected Element listsets_response = null;
+  /** the list metadata formats response */
+  protected Element listmetadataformats_response = null;
+
+  public IIIFReceptionist() {
+
+  }
+  
+  public void cleanUp() {
+    if (this.mr != null) {
+      
+      this.mr.cleanUp();
+    }
+  }
+  
+  public void setSiteName(String site_name) {
+    this.site_name = site_name;
+  }
+  /** sets the message router  - it should already be created and
+   * configured in the init() of a servlet/restlet (OAIServer, for example) before being passed to the receptionist*/
+  public void setMessageRouter(ModuleInterface mr) {
+    this.mr = mr;
+  }
+  
+  /** configures the receptionist */
+  public boolean configure(Element config) {
+    
+    if (this.mr==null) {
+      logger.error(" message routers must be set before calling IIIF configure");
+      return false;
+    }
+    if (config == null) {
+      logger.error(" IIIF configure file is null");
+      return false;
+    }
+    oai_config = config;
+    
+    repository_id = getRepositoryIdentifier(); 
+
+    if (!configureSetInfo()) {
+      // there are no sets
+      logger.error("No sets (collections) available for IIIF");
+      return false;
+    }
+
+    return true;
+  }
+
+  private boolean configureSetInfo() {
+    this.set_set = new HashSet<String>();
+
+    // First, we get a list of all the OAI enabled collections
+    // We get this by sending a listSets request to the MR
+    Document doc = XMLConverter.newDOM();
+    Element message = doc.createElement(GSXML.MESSAGE_ELEM);
+    
+    Element request = GSXML.createBasicRequest(doc, OAIXML.OAI_SET_LIST, "", null);
+    message.appendChild(request);
+    Node msg_node = mr.process(message);
+    
+    if (msg_node == null) {
+      logger.error("returned msg_node from mr is null");
+      return false;
+    }
+    Element resp = (Element)GSXML.getChildByTagName(msg_node, GSXML.RESPONSE_ELEM);
+    Element coll_list = (Element)GSXML.getChildByTagName(resp, GSXML.COLLECTION_ELEM + GSXML.LIST_MODIFIER);
+    if (coll_list == null) {
+      logger.error("coll_list is null");
+      return false;
+    }
+
+    this.collection_list = (Element)doc.importNode(coll_list, true);
+
+    // go through and store a list of collection names for convenience
+    // also create a 'to' attribute for the next request to the MR, which
+    // is a ListSets request to each collection
+    Node child = this.collection_list.getFirstChild();
+    if (child == null) {
+      logger.error("collection list has no children");
+      noRecordsMatch = true;
+      return false;
+    }
+    
+    this.collection_name_list = new Vector<String>();
+    StringBuffer to = new StringBuffer();
+    boolean first = true;
+    while (child != null) {
+      if (child.getNodeName().equals(GSXML.COLLECTION_ELEM)) {
+	String coll_id =((Element) child).getAttribute(GSXML.NAME_ATT);
+	this.collection_name_list.add(coll_id);
+	if (!first) {
+	  to.append(',');
+	}
+	first = false;
+	to.append(coll_id+"/"+OAIXML.LIST_SETS);
+      }
+      child = child.getNextSibling();
+    }
+    if (first) {
+      // we haven't found any collections
+      logger.error("found no collection elements in collectionList");
+      noRecordsMatch = true;
+      return false;
+    }
+    Document listsets_doc = XMLConverter.newDOM();
+    Element listsets_element = listsets_doc.createElement(OAIXML.LIST_SETS);
+    this.listsets_response = getMessage(listsets_doc, listsets_element);
+    
+    // Now, for each collection, get a list of all its sets
+    // might include subsets (classifiers) or super colls
+    // We'll reuse the first message, changing its type and to atts
+    request.setAttribute(GSXML.TYPE_ATT, "");
+    request.setAttribute(GSXML.TO_ATT, to.toString());
+    // send to MR
+    msg_node = mr.process(message);
+    //logger.info("*** " + XMLConverter.getPrettyString(msg_node));
+    NodeList response_list =  ((Element)msg_node).getElementsByTagName(GSXML.RESPONSE_ELEM);
+    for (int c=0; c<response_list.getLength(); c++) {
+      // for each collection's response
+      Element response = (Element)response_list.item(c);
+      String coll_name = GSPath.getFirstLink(response.getAttribute(GSXML.FROM_ATT));
+      logger.info("*** coll from response "+coll_name);
+      NodeList set_list = response.getElementsByTagName(OAIXML.SET);
+      for (int j=0; j<set_list.getLength(); j++) {
+	// now check if it a super collection
+	Element set = (Element)set_list.item(j);
+	String set_spec = GSXML.getNodeText((Element)GSXML.getChildByTagName(set, OAIXML.SET_SPEC));
+	logger.info("*** set spec = "+set_spec);
+	// this may change if we add site name back in
+	// setSpecs will be collname or collname:subset or supercollname
+	if (set_spec.indexOf(":")==-1 && ! set_spec.equals(coll_name)) {
+	  // it must be a super coll spec
+	  logger.info("*** found super coll, "+set_spec);
+	  // check that it is a valid one from config
+	  if (this.has_super_colls == true && this.super_coll_data.containsKey(set_spec)) {
+	    Vector <String> subcolls = this.super_coll_map.get(set_spec);
+	    if (subcolls == null) {
+	      logger.info("*** its new!!");
+	      // not in there yet
+	      subcolls = new Vector<String>();
+	      this.set_set.add(set_spec);
+	      this.super_coll_map.put(set_spec, subcolls);
+	      // the first time a supercoll is mentioned, add into the set list
+	      logger.info("*** finding the set info "+XMLConverter.getPrettyString(this.super_coll_data.get(set_spec)));
+	      listsets_element.appendChild(GSXML.duplicateWithNewName(listsets_doc, this.super_coll_data.get(set_spec), OAIXML.SET, true));
+	    }
+	    // add this collection to the list for the super coll
+	    subcolls.add(coll_name);
+	  }
+	} else { // its either the coll itself or a subcoll
+	  // add in the set
+	  listsets_element.appendChild(listsets_doc.importNode(set, true));
+	  this.set_set.add(set_spec);
+	}
+      } // for each set in the collection
+    } // for each OAI enabled collection
+    return true;
+  }
+
+  protected void resetMessageRouter() {
+    // we just need to send a configure request to MR
+    Document doc = XMLConverter.newDOM();
+    Element mr_request_message = doc.createElement(GSXML.MESSAGE_ELEM);
+    Element mr_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_SYSTEM, "", null);
+    mr_request_message.appendChild(mr_request);
+    
+    Element system = doc.createElement(GSXML.SYSTEM_ELEM);
+    mr_request.appendChild(system);
+    system.setAttribute(GSXML.TYPE_ATT, GSXML.SYSTEM_TYPE_CONFIGURE);
+
+    Element response = (Element) this.mr.process(mr_request_message);
+    logger.info("*** configure response = "+XMLConverter.getPrettyString(response));
+  }
+
+  /** process using strings - just calls process using Elements */
+  public String process(String xml_in) {
+    
+    Node message_node = XMLConverter.getDOM(xml_in);
+    Node page = process(message_node);
+    return XMLConverter.getString(page);
+  }
+
+  //Compose a message/response element used to send back to the OAIServer servlet. 
+  //This method is only used within OAIReceptionist
+  private Element getMessage(Document doc, Element e) {
+    Element msg = doc.createElement(GSXML.MESSAGE_ELEM);
+    Element response = doc.createElement(GSXML.RESPONSE_ELEM);
+    msg.appendChild(response);
+    response.appendChild(e);
+    return msg;
+  }
+
+  /** process - produce xml data in response to a request
+   * if something goes wrong, it returns null -
+   */
+  public Node process(Node message_node) {
+    logger.info("*** IIIFReceptionist received request");
+
+    Element message = GSXML.nodeToElement(message_node);
+    logger.info("*** " + XMLConverter.getString(message));
+
+    // check that its a correct message tag
+    if (!message.getTagName().equals(GSXML.MESSAGE_ELEM)) {
+      logger.error(" Invalid message. GSDL message should start with <"+GSXML.MESSAGE_ELEM+">, instead it starts with:"+message.getTagName()+".");
+      return OAIXML.createErrorMessage(OAIXML.BAD_ARGUMENT, "Internal messaging error");
+    }
+   
+    // get the request out of the message - assume that there is only one
+    Element request = (Element)GSXML.getChildByTagName(message, GSXML.REQUEST_ELEM);
+    if (request == null) {
+      logger.error(" message had no request!");
+      return OAIXML.createErrorMessage(OAIXML.BAD_ARGUMENT, "Internal messaging error");
+    }
+
+    // special case, reset=true for reloading the MR and recept data
+    String reset = request.getAttribute("reset");
+    if (!reset.equals("")) {
+      resetMessageRouter();
+      configureSetInfo();
+      return OAIXML.createResetResponse(true);
+    }
+
+    
+    //At this stage, the value of 'to' attribute of the request must be the 'verb'
+    //The only thing that the oai receptionist can be sure is that these verbs are valid, nothing else.
+    String verb = request.getAttribute(GSXML.TO_ATT);
+    if (verb.equals(OAIXML.IDENTIFY)) {
+      return doIdentify();
+    }
+    if (verb.equals(OAIXML.GET_RECORD)) {
+      return doGetRecord(request);
+    }
+
+    // should never get here as verbs were checked in OAIServer
+    return OAIXML.createErrorMessage(OAIXML.BAD_VERB, "Unexpected things happened");
+    
+  }
+
+  private String getRepositoryIdentifier() {
+    Element ri = (Element)GSXML.getChildByTagName(oai_config, OAIXML.REPOSITORY_IDENTIFIER);
+    if (ri != null) { 
+      return GSXML.getNodeText(ri);
+    }
+    return "";
+  }
+
+
+  private void copyNamedElementfromConfig(Element to_elem, String element_name) {
+    Element original_element = (Element)GSXML.getChildByTagName(oai_config, element_name);
+    if(original_element != null) {
+      GSXML.copyNode(to_elem, original_element);
+    }
+  }
+
+  private Element doIdentify() {
+    //The validation for this verb has been done in OAIServer.validate(). So no bother here.
+    logger.info("");
+    if (this.identify_response != null) {
+      // we have already created it
+      return getMessage(this.identify_response.getOwnerDocument(), this.identify_response);
+    }
+    Document doc = XMLConverter.newDOM();
+    Element identify = doc.createElement(OAIXML.IDENTIFY);
+    //do the repository name
+    copyNamedElementfromConfig(identify, OAIXML.REPOSITORY_NAME);
+    //do the baseurl
+    copyNamedElementfromConfig(identify, OAIXML.BASE_URL);
+    //do the protocol version
+    copyNamedElementfromConfig(identify, OAIXML.PROTOCOL_VERSION);
+        
+    //There can be more than one admin email according to the OAI specification
+    NodeList admin_emails = GSXML.getChildrenByTagName(oai_config, OAIXML.ADMIN_EMAIL);
+    int num_admin = 0;
+    Element from_admin_email = null;  
+    if (admin_emails != null) {
+      num_admin = admin_emails.getLength();
+    }
+    for (int i=0; i<num_admin; i++) {
+      GSXML.copyNode(identify, admin_emails.item(i));
+    }
+
+    /* IIIF does not have the equivalent of oai earliestDatestamp */
+     
+    // output the oai identifier
+    Element description = doc.createElement(OAIXML.DESCRIPTION);
+    identify.appendChild(description);
+    // TODO, make this a valid id
+    Element oaiIdentifier = OAIXML.createOAIIdentifierXML(doc, repository_id, "lucene-jdbm-demo", "ec159e");
+    description.appendChild(oaiIdentifier);
+
+    // if there are any oaiInfo metadata, add them in too.
+    Element info = (Element)GSXML.getChildByTagName(oai_config, OAIXML.OAI_INFO);
+    if (info != null) {
+      NodeList meta = GSXML.getChildrenByTagName(info, OAIXML.METADATA);
+      if (meta != null && meta.getLength() > 0) {
+	Element gsdl = OAIXML.createGSDLElement(doc);
+	description.appendChild(gsdl);
+	for (int m = 0; m<meta.getLength(); m++) {
+	  GSXML.copyNode(gsdl, meta.item(m));
+	}
+	
+      }
+    }
+    this.identify_response = identify;
+    return getMessage(doc, identify);
+  }
+
+  private Element doGetRecord(Element req){
+    logger.info("");
+    /** arguments:
+        identifier: required
+        metadataPrefix: required
+	*  Exceptions: badArgument; cannotDisseminateFormat; idDoesNotExist
+	*/ 
+    Document doc = XMLConverter.newDOM();
+    Element get_record = doc.createElement(OAIXML.GET_RECORD);
+
+    HashSet<String> valid_strs = new HashSet<String>();
+    valid_strs.add(OAIXML.IDENTIFIER);
+    valid_strs.add(OAIXML.METADATA_PREFIX);
+
+    NodeList params = GSXML.getChildrenByTagName(req, GSXML.PARAM_ELEM);
+    HashMap<String, String> param_map = GSXML.getParamMap(params);    
+
+    // Any need to check all params are valid, like OAI??
+    
+    String identifier = param_map.get(OAIXML.IDENTIFIER);
+    
+    // get the names
+    String[] strs = identifier.split(":", 2);
+    if(strs == null || strs.length < 2) {
+      logger.error("identifier is not in the form coll:id" + identifier);
+      return OAIXML.createErrorMessage(OAIXML.ID_DOES_NOT_EXIST, "");
+    }    
+    //String name_of_site = strs[0];
+    String coll_name = strs[0];
+    String oid = strs[1];
+    
+    //re-organize the request element
+    // reset the 'to' attribute
+    String verb = req.getAttribute(GSXML.TO_ATT);
+    req.setAttribute(GSXML.TO_ATT, coll_name + "/" + verb);
+    // reset the identifier element
+    Element param = GSXML.getNamedElement(req, GSXML.PARAM_ELEM, GSXML.NAME_ATT, OAIXML.IDENTIFIER);
+    if (param != null) {
+      param.setAttribute(GSXML.NAME_ATT, OAIXML.OID);
+      param.setAttribute(GSXML.VALUE_ATT, oid);
+    }    
+
+    //Now send the request to the message router to process
+    Element msg = doc.createElement(GSXML.MESSAGE_ELEM);
+    msg.appendChild(doc.importNode(req, true));
+    Node result_node = mr.process(msg);
+    return GSXML.nodeToElement(result_node);
+  }
+
+
+}
+
+  
Index: gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/GSRestletApplication.java
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/GSRestletApplication.java	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/GSRestletApplication.java	(revision 32707)
@@ -0,0 +1,396 @@
+package edu.illinois.library.cantaloupe;
+
+import edu.illinois.library.cantaloupe.config.Configuration;
+import edu.illinois.library.cantaloupe.config.Key;
+import edu.illinois.library.cantaloupe.processor.UnsupportedOutputFormatException;
+import edu.illinois.library.cantaloupe.processor.UnsupportedSourceFormatException;
+import edu.illinois.library.cantaloupe.resource.AbstractResource;
+import edu.illinois.library.cantaloupe.resource.IllegalClientArgumentException;
+import edu.illinois.library.cantaloupe.resource.GSLandingResource;
+import edu.illinois.library.cantaloupe.resource.TrailingSlashRemovingResource;
+import edu.illinois.library.cantaloupe.resource.admin.AdminResource;
+import edu.illinois.library.cantaloupe.resource.admin.StatusResource;
+import edu.illinois.library.cantaloupe.resource.api.TaskResource;
+import edu.illinois.library.cantaloupe.resource.api.TasksResource;
+import edu.illinois.library.cantaloupe.resource.iiif.RedirectingResource;
+import org.restlet.Application;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Directory;
+import org.restlet.resource.ResourceException;
+import org.restlet.routing.Router;
+import org.restlet.routing.Template;
+import org.restlet.security.Authenticator;
+import org.restlet.security.ChallengeAuthenticator;
+import org.restlet.security.LocalVerifier;
+import org.restlet.service.CorsService;
+import org.restlet.service.StatusService;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.NoSuchFileException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+
+/**
+ * Restlet Application implementation. Creates endpoint routes and connects
+ * them to {@link org.restlet.resource.Resource resources}.
+ *
+ * @see <a href="https://restlet.com/open-source/documentation/jdocs/2.3/jse">
+ *     Restlet 2.3 JSE Javadoc</a>
+ * @see <a href="https://restlet.com/open-source/documentation/2.3/changelog">
+ *     Restlet Change Log</a>
+ */
+public class GSRestletApplication extends RestletApplication {
+
+    private static class CustomStatusService extends StatusService {
+
+        private static final List<MediaType> SUPPORTED_MEDIA_TYPES =
+                Arrays.asList(MediaType.TEXT_PLAIN, MediaType.TEXT_HTML,
+                        MediaType.APPLICATION_XHTML);
+
+        @Override
+        public Representation toRepresentation(Status status,
+                                               Request request,
+                                               Response response) {
+            String message = null, stackTrace = null;
+            Throwable throwable = status.getThrowable();
+            if (throwable != null) {
+                if (throwable.getCause() != null) {
+                    throwable = throwable.getCause();
+                }
+                message = throwable.getMessage();
+                Configuration config = Configuration.getInstance();
+                if (config.getBoolean(Key.PRINT_STACK_TRACE_ON_ERROR_PAGES, false)) {
+                    try (StringWriter sw = new StringWriter()) {
+                        throwable.printStackTrace(new PrintWriter(sw));
+                        stackTrace = sw.toString();
+                    } catch (IOException e) {
+                        // We are almost certain to never get here...
+                    }
+                }
+            } else if (status.getDescription() != null) {
+                message = status.getDescription();
+            } else if (Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
+                message = "No resource exists at this URL.";
+            }
+
+            final Map<String,Object> templateVars =
+                    AbstractResource.getCommonTemplateVars(request);
+            templateVars.put("pageTitle", status.getCode() + " " +
+                    status.getReasonPhrase());
+            templateVars.put("message", message);
+            templateVars.put("stackTrace", stackTrace);
+
+            // Negotiate a response representation content type.
+            // Web browsers will usually request `text/html` and
+            // `application/xhtml+xml` in order of priority. In the absence
+            // of either of those, we will prefer to return `text/plain`.
+            MediaType requestedType = request.getClientInfo().
+                    getPreferredMediaType(SUPPORTED_MEDIA_TYPES);
+            if (requestedType == null) {
+                requestedType = MediaType.TEXT_PLAIN;
+            }
+
+            // Use a template that best fits the representation's content type.
+            String template;
+            MediaType mediaType;
+            if (Arrays.asList("text/html", "application/xhtml+xml").
+                    contains(requestedType.toString())) {
+                template = "/error.html.vm";
+                mediaType = MediaType.TEXT_HTML;
+            } else {
+                template = "/error.txt.vm";
+                mediaType = MediaType.TEXT_PLAIN;
+            }
+
+            Representation rep = new AbstractResource() {}.
+                    template(template, templateVars);
+            rep.setMediaType(mediaType);
+            return rep;
+        }
+
+        /**
+         * <p>Returns a {@link Status} appropriate for the given {@link
+         * Throwable}.</p>
+         *
+         * <p>Note that illegal arguments from the client, which would be
+         * intended to produce a 400 status, should be represented
+         * by {@link IllegalClientArgumentException}. In contrast, {@link
+         * IllegalArgumentException} will produce a 500 response.</p>
+         */
+        @Override
+        public Status toStatus(Throwable t,
+                               Request request,
+                               Response response) {
+            Status status;
+            t = (t.getCause() != null) ? t.getCause() : t;
+
+            if (t instanceof ResourceException) {
+                status = ((ResourceException) t).getStatus();
+            } else if (t instanceof IllegalClientArgumentException ||
+                    t instanceof UnsupportedEncodingException) {
+                status = new Status(Status.CLIENT_ERROR_BAD_REQUEST, t);
+            } else if (t instanceof UnsupportedOutputFormatException) {
+                status = new Status(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE, t);
+            } else if (t instanceof FileNotFoundException ||
+                    t instanceof NoSuchFileException) {
+                status = new Status(Status.CLIENT_ERROR_NOT_FOUND, t);
+            } else if (t instanceof AccessDeniedException) {
+                status = new Status(Status.CLIENT_ERROR_FORBIDDEN, t);
+            } else if (t instanceof UnsupportedSourceFormatException) {
+                status = new Status(Status.SERVER_ERROR_NOT_IMPLEMENTED, t);
+            } else {
+                status = new Status(Status.SERVER_ERROR_INTERNAL, t);
+            }
+            return status;
+        }
+
+    }
+
+    /**
+     * Verifies given user credentials against a single set of stored user
+     * credentials in the application configuration.
+     */
+    private static class ConfigurationVerifier extends LocalVerifier {
+
+        private final Key userKey;
+        private final Key secretKey;
+
+        /**
+         * @param userKey   Key under which the username is stored.
+         * @param secretKey Key under which the secret is stored.
+         */
+        ConfigurationVerifier(Key userKey, Key secretKey) {
+            this.userKey = userKey;
+            this.secretKey = secretKey;
+        }
+
+        @Override
+        public char[] getLocalSecret(String givenUser) {
+            final Configuration config = Configuration.getInstance();
+            if (config.getString(userKey).equals(givenUser)) {
+                return config.getString(secretKey).toCharArray();
+            }
+            return null;
+        }
+
+        /**
+         * Overrides super to disallow an empty stored secret.
+         */
+        @Override
+        public int verify(String identifier, char[] secret) {
+            final Configuration config = Configuration.getInstance();
+            final String configSecret = config.getString(secretKey);
+            if (configSecret == null || configSecret.isEmpty()) {
+                return RESULT_INVALID;
+            }
+            return super.verify(identifier, secret);
+        }
+
+    }
+
+    public static final String ADMIN_PATH = "/admin";
+    public static final String ADMIN_CONFIG_PATH = "/admin/configuration";
+    public static final String ADMIN_STATUS_PATH = "/status";
+    public static final String CONFIGURATION_PATH = "/configuration";
+    public static final String IIIF_PATH = "/iiif";
+    public static final String IIIF_1_PATH = "/iiif/1";
+    public static final String IIIF_2_PATH = "/iiif/2";
+    public static final String STATIC_ROOT_PATH = "/static";
+    public static final String TASKS_PATH = "/tasks";
+
+    public static final String ADMIN_REALM =
+            edu.illinois.library.cantaloupe.Application.getName() + " Control Panel";
+    public static final String API_REALM =
+            edu.illinois.library.cantaloupe.Application.getName() + " API Realm";
+    public static final String PUBLIC_REALM = "Image Realm";
+
+    public GSRestletApplication() {
+        super();
+
+        // Tell Restlet to use a custom status service for transforming
+        // uncaught exceptions into error responses.
+        setStatusService(new CustomStatusService());
+
+        // Enable CORS.
+        // See: http://restlet.com/blog/2015/12/15/understanding-and-using-cors/
+        CorsService corsService = new CorsService();
+        corsService.setAllowedOrigins(new HashSet<>(Collections.singletonList("*")));
+        corsService.setAllowedCredentials(true);
+        getServices().add(corsService);
+
+        // Disable support for ranging. This will tell Restlet not to honor the
+        // Range request header, as well as not to send an Accept-Ranges header.
+        getRangeService().setEnabled(false);
+    }
+
+    private Authenticator newAdminAuthenticator() {
+        ChallengeAuthenticator auth = new ChallengeAuthenticator(
+                getContext(), ChallengeScheme.HTTP_BASIC, ADMIN_REALM);
+        auth.setVerifier(new ConfigurationVerifier(
+                Key.ADMIN_USERNAME, Key.ADMIN_SECRET));
+        return auth;
+    }
+
+    private Authenticator newAPIAuthenticator() {
+        ChallengeAuthenticator auth = new ChallengeAuthenticator(
+                getContext(), ChallengeScheme.HTTP_BASIC, API_REALM);
+        auth.setVerifier(new ConfigurationVerifier(
+                Key.API_USERNAME, Key.API_SECRET));
+        return auth;
+    }
+
+    private Authenticator newPublicEndpointAuthenticator() {
+        final Configuration config = Configuration.getInstance();
+
+        if (config.getBoolean(Key.BASIC_AUTH_ENABLED, false)) {
+            getLogger().log(Level.INFO,
+                    "Enabling HTTP Basic authentication for public endpoints");
+
+            final ChallengeAuthenticator auth = new ChallengeAuthenticator(
+                    getContext(), ChallengeScheme.HTTP_BASIC, PUBLIC_REALM) {
+                @Override
+                protected int beforeHandle(Request request, Response response) {
+                    final String path = request.getResourceRef().getPath();
+                    if (path.startsWith(IIIF_PATH)) {
+                        return super.beforeHandle(request, response);
+                    }
+                    response.setStatus(Status.SUCCESS_OK);
+                    return CONTINUE;
+                }
+            };
+
+            auth.setVerifier(new ConfigurationVerifier(
+                    Key.BASIC_AUTH_USERNAME, Key.BASIC_AUTH_SECRET));
+            return auth;
+        } else {
+            getLogger().info("Public endpoint authentication is disabled (" +
+                    Key.BASIC_AUTH_ENABLED + " = false)");
+        }
+        return null;
+    }
+
+    /**
+     * Creates a root Restlet that will receive all incoming calls.
+     *
+     * @see <a href="http://iiif.io/api/image/2.0/#uri-syntax">URI Syntax</a>
+     */
+    @Override
+    public Restlet createInboundRoot() {
+        final Router router = new Router(getContext());
+        router.setDefaultMatchingMode(Template.MODE_EQUALS);
+
+        ////////////////////// IIIF Image API routes ///////////////////////
+
+        // Redirect IIIF_PATH/ to IIIF_PATH
+        router.attach(IIIF_PATH + "/", TrailingSlashRemovingResource.class);
+
+        // Redirect IIIF_PATH to IIIF_2_PATH
+        router.attach(IIIF_PATH, RedirectingResource.class);
+
+        //////////////////// IIIF Image API 1.x routes /////////////////////
+
+        // landing page
+        router.attach(IIIF_1_PATH,
+                edu.illinois.library.cantaloupe.resource.iiif.v1.LandingResource.class);
+
+        // Redirect IIIF_1_PATH/ to IIIF_1_PATH
+        router.attach(IIIF_1_PATH + "/", TrailingSlashRemovingResource.class);
+
+        // image request
+        router.attach(IIIF_1_PATH + "/{identifier}/{region}/{size}/{rotation}/{quality_format}",
+                edu.illinois.library.cantaloupe.resource.iiif.v1.ImageResource.class);
+
+        // information request
+        router.attach(IIIF_1_PATH + "/{identifier}",
+                edu.illinois.library.cantaloupe.resource.iiif.v1.InformationResource.RedirectingResource.class);
+        router.attach(IIIF_1_PATH + "/{identifier}/info.json",
+                edu.illinois.library.cantaloupe.resource.iiif.v1.InformationResource.class);
+
+        //////////////////// IIIF Image API 2.x routes /////////////////////
+
+        // landing page
+        router.attach(IIIF_2_PATH,
+                edu.illinois.library.cantaloupe.resource.iiif.v2.LandingResource.class);
+
+        // Redirect IIIF_2_PATH/ to IIIF_2_PATH
+        router.attach(IIIF_2_PATH + "/", TrailingSlashRemovingResource.class);
+
+        // image request
+        router.attach(IIIF_2_PATH + "/{identifier}/{region}/{size}/{rotation}/{quality}.{format}",
+                edu.illinois.library.cantaloupe.resource.iiif.v2.ImageResource.class);
+
+        // information request
+        router.attach(IIIF_2_PATH + "/{identifier}",
+                edu.illinois.library.cantaloupe.resource.iiif.v2.GSInformationResource.RedirectingResource.class);
+        router.attach(IIIF_2_PATH + "/{identifier}/info.json",
+                edu.illinois.library.cantaloupe.resource.iiif.v2.GSInformationResource.class);
+
+        ////////////////////////// Admin routes ///////////////////////////
+
+        Authenticator adminAuth = newAdminAuthenticator();
+        adminAuth.setNext(AdminResource.class);
+        router.attach(ADMIN_PATH, adminAuth);
+
+        adminAuth = newAdminAuthenticator();
+        adminAuth.setNext(edu.illinois.library.cantaloupe.resource.admin.ConfigurationResource.class);
+        router.attach(ADMIN_CONFIG_PATH, adminAuth);
+
+        adminAuth = newAdminAuthenticator();
+        adminAuth.setNext(StatusResource.class);
+        router.attach(ADMIN_STATUS_PATH, adminAuth);
+
+        /////////////////////////// API routes ////////////////////////////
+
+        Authenticator apiAuth = newAPIAuthenticator();
+        apiAuth.setNext(edu.illinois.library.cantaloupe.resource.api.ConfigurationResource.class);
+        router.attach(CONFIGURATION_PATH, apiAuth);
+
+        apiAuth = newAPIAuthenticator();
+        apiAuth.setNext(TasksResource.class);
+        router.attach(TASKS_PATH, apiAuth);
+
+        apiAuth = newAPIAuthenticator();
+        apiAuth.setNext(TaskResource.class);
+        router.attach(TASKS_PATH + "/{uuid}", apiAuth);
+
+        ////////////////////////// Other routes ///////////////////////////
+
+        // Application landing page
+        router.attach("/", GSLandingResource.class);
+
+        // Hook up the static file server (for images, CSS, & scripts)
+        // This uses Restlet's "CLAP" (Class Loader Access Protocol) mechanism
+        // which must be enabled in web.xml.
+        final Directory dir = new Directory(
+                getContext(), "clap://resources/public_html/");
+        dir.setDeeplyAccessible(true);
+        dir.setListingAllowed(false);
+        dir.setNegotiatingContent(false);
+        router.attach(STATIC_ROOT_PATH, dir);
+
+        // Hook up public endpoint authentication
+        Authenticator endpointAuth = newPublicEndpointAuthenticator();
+        if (endpointAuth != null) {
+            endpointAuth.setNext(router);
+            return endpointAuth;
+        }
+
+        return router;
+    }
+
+}
Index: gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/resource/GSLandingResource.java
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/resource/GSLandingResource.java	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/resource/GSLandingResource.java	(revision 32707)
@@ -0,0 +1,31 @@
+package edu.illinois.library.cantaloupe.resource;
+
+import org.restlet.data.CacheDirective;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Get;
+import org.restlet.resource.ResourceException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles the landing page.
+ */
+public class GSLandingResource extends AbstractResource {
+
+    @Override
+    protected void doInit() throws ResourceException {
+        super.doInit();
+        // Add a Cache-Control header to tell clients this page never expires.
+        final List<CacheDirective> directives = new ArrayList<>();
+        directives.add(CacheDirective.maxAge(Integer.MAX_VALUE));
+        directives.add(CacheDirective.publicInfo());
+        getResponseCacheDirectives().addAll(directives);
+    }
+
+    @Get
+    public Representation doGet() throws Exception {
+        return template("/gslanding.vm");
+    }
+
+}
Index: gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v2/GSInformationResource.java
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v2/GSInformationResource.java	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v2/GSInformationResource.java	(revision 32707)
@@ -0,0 +1,200 @@
+package edu.illinois.library.cantaloupe.resource.iiif.v2;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.List;
+
+import edu.illinois.library.cantaloupe.RestletApplication;
+import edu.illinois.library.cantaloupe.cache.CacheFacade;
+import edu.illinois.library.cantaloupe.config.Configuration;
+import edu.illinois.library.cantaloupe.config.Key;
+import edu.illinois.library.cantaloupe.image.Format;
+import edu.illinois.library.cantaloupe.image.Identifier;
+import edu.illinois.library.cantaloupe.image.Info;
+import edu.illinois.library.cantaloupe.processor.Processor;
+import edu.illinois.library.cantaloupe.processor.ProcessorFactory;
+import edu.illinois.library.cantaloupe.source.Source;
+import edu.illinois.library.cantaloupe.source.SourceFactory;
+import edu.illinois.library.cantaloupe.resource.JSONRepresentation;
+import edu.illinois.library.cantaloupe.processor.ProcessorConnector;
+import org.restlet.data.MediaType;
+import org.restlet.data.Preference;
+import org.restlet.data.Reference;
+import org.restlet.representation.EmptyRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Get;
+
+import org.greenstone.gsdl3.IIIFServerBridge;
+
+/**
+ * Handles IIIF Image API 2.x information requests.
+ *
+ * @see <a href="http://iiif.io/api/image/2.1/#information-request">Information
+ *      Requests</a>
+ */
+public class GSInformationResource extends InformationResource {
+
+    /**
+     * Redirects {@literal /:identifier} to {@literal /:identifier/info.json},
+     * respecting the Servlet context root and
+     * {@link #PUBLIC_IDENTIFIER_HEADER} header.
+     */
+    public static class RedirectingResource extends IIIF2Resource {
+        @Get
+        public Representation doGet() {
+            final Reference newRef = new Reference(
+                    getPublicRootReference() +
+                            RestletApplication.IIIF_2_PATH + "/" +
+                            getPublicIdentifier() +
+                            "/info.json");
+            redirectSeeOther(newRef);
+            return new EmptyRepresentation();
+        }
+    }
+
+    /**
+     * Responds to information requests.
+     *
+     * @return {@link ImageInfo} instance serialized as JSON.
+     */
+    @Get
+    public Representation doGet() throws Exception {
+        final Configuration config = Configuration.getInstance();
+        final Identifier identifier = getIdentifier();
+        final CacheFacade cacheFacade = new CacheFacade();
+
+        // If we don't need to resolve first, and are using a cache, and the
+        // cache contains an info matching the request, skip all the setup and
+        // just return the cached info.
+        if (!isResolvingFirst()) {
+            try {
+                Info info = cacheFacade.getInfo(identifier);
+                if (info != null) {
+                    // The source format will be null or UNKNOWN if the info was
+                    // serialized in version < 3.4.
+                    final Format format = info.getSourceFormat();
+                    if (format != null && !Format.UNKNOWN.equals(format)) {
+                        final Processor processor = new ProcessorFactory().
+                                newProcessor(format);
+                        commitCustomResponseHeaders();
+                        return newRepresentation(info, processor);
+                    }
+                }
+            } catch (IOException e) {
+                // Don't rethrow -- it's still possible to service the request.
+                getLogger().severe(e.getMessage());
+            }
+        }
+
+	IIIFServerBridge gs_iiif_bridge = new IIIFServerBridge();
+	gs_iiif_bridge.init("localsite");
+	String gs_message = gs_iiif_bridge.doGetDocumentMessage(identifier.toString());
+	
+        final Source source = new SourceFactory().newSource(
+                identifier, getDelegateProxy());
+
+	System.err.println("***** identifier = " + identifier);
+	System.err.println("***** source path = " + ((edu.illinois.library.cantaloupe.source.FileSource)source).getPath());
+	
+        // If we are resolving first, or if the source image is not present in
+        // the source cache (if enabled), check access to it in preparation for
+        // retrieval.
+        final Path sourceImage = cacheFacade.getSourceCacheFile(identifier);
+        if (sourceImage == null || isResolvingFirst()) {
+            try {
+                source.checkAccess();
+            } catch (NoSuchFileException e) { // this needs to be rethrown!
+                if (config.getBoolean(Key.CACHE_SERVER_PURGE_MISSING, false)) {
+                    // If the image was not found, purge it from the cache.
+                    cacheFacade.purgeAsync(identifier);
+                }
+                throw e;
+            }
+        }
+
+        // Get the format of the source image.
+        // If we are not resolving first, and there is a hit in the source
+        // cache, read the format from the source-cached-file, as we will
+        // expect source cache access to be more efficient.
+        // Otherwise, read it from the source.
+        Format format = Format.UNKNOWN;
+        if (!isResolvingFirst() && sourceImage != null) {
+            List<edu.illinois.library.cantaloupe.image.MediaType> mediaTypes =
+                    edu.illinois.library.cantaloupe.image.MediaType.detectMediaTypes(sourceImage);
+            if (!mediaTypes.isEmpty()) {
+                format = mediaTypes.get(0).toFormat();
+            }
+        } else {
+            format = source.getFormat();
+        }
+
+        // Obtain an instance of the processor assigned to that format.
+        try (Processor processor = new ProcessorFactory().newProcessor(format)) {
+            // Connect it to the source.
+            tempFileFuture = new ProcessorConnector().connect(
+                    source, processor, identifier, format);
+
+            final Info info = getOrReadInfo(identifier, processor);
+
+            commitCustomResponseHeaders();
+
+            return newRepresentation(info, processor);
+        }
+    }
+
+    /**
+     * @return Full image URI corresponding to the given identifier, respecting
+     *         the {@literal X-Forwarded-*} and
+     *         {@link #PUBLIC_IDENTIFIER_HEADER} reverse proxy headers.
+     */
+    private String getImageURI() {
+        return getPublicRootReference() + RestletApplication.IIIF_2_PATH + "/" +
+                getPublicIdentifier();
+    }
+
+    private MediaType getNegotiatedMediaType() {
+        MediaType mediaType;
+        // If the client has requested JSON-LD, set the content type to
+        // that; otherwise set it to JSON.
+        List<Preference<MediaType>> preferences = getRequest().getClientInfo().
+                getAcceptedMediaTypes();
+        if (preferences.get(0) != null && preferences.get(0).toString().
+                startsWith("application/ld+json")) {
+            mediaType = new MediaType("application/ld+json");
+        } else {
+            mediaType = new MediaType("application/json");
+        }
+        return mediaType;
+    }
+
+    private Representation newRepresentation(Info info,
+                                             Processor processor) {
+        final ImageInfoFactory factory = new ImageInfoFactory(
+                processor.getSupportedFeatures(),
+                processor.getSupportedIIIF2Qualities(),
+                processor.getAvailableOutputFormats());
+        factory.setDelegateProxy(getDelegateProxy());
+
+        final ImageInfo<String, Object> imageInfo = factory.newImageInfo(
+                getImageURI(), info, getPageIndex());
+        final MediaType mediaType = getNegotiatedMediaType();
+
+        return new JSONRepresentation(imageInfo, mediaType, () -> {
+            if (tempFileFuture != null) {
+                Path tempFile = tempFileFuture.get();
+                if (tempFile != null) {
+                    Files.deleteIfExists(tempFile);
+                }
+            }
+            return null;
+        });
+    }
+
+    private boolean isResolvingFirst() {
+        return Configuration.getInstance().
+                getBoolean(Key.CACHE_SERVER_RESOLVE_FIRST, true);
+    }
+
+}
Index: gs3-extensions/iiif-servlet/trunk/src/src/main/resources/global.properties
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/src/main/resources/global.properties	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/src/main/resources/global.properties	(revision 32707)
@@ -0,0 +1,61 @@
+## Don't edit version in web/WEB-INF/classes, edit .in version in resources/web
+
+## greenstone home
+#If you want to specify the gsdl3.home manually then uncomment this line
+#in resources/java/global.properties.in, not global.properties since
+#the latter is repeatedly re-generated from global.properties.in
+#gsdl3.home=/home/osboxes/research/code/greenstone3-svn-cantaloupe/web
+#gsdl3.writablehome=/home/osboxes/research/code/greenstone3-svn-cantaloupe/web
+gsdl3.version=trunk
+
+disable.collection.building=false
+
+# for collection building
+perl.path=/usr/bin/
+
+# tomcat info
+server.protocol=http
+tomcat.server=localhost
+tomcat.port=8383
+tomcat.context=greenstone3
+solr.context=solr
+derby.server.port=1527
+derby.server=localhost
+
+server.protocols=http
+tomcat.port.https=8443
+localhost.protocol.http=http
+localhost.server.http=127.0.0.1
+localhost.port.http=8383
+restrict.http.to.local=false
+
+
+## Proxy setup - set these if you are behind a firewall and you want services that access the internet
+# The proxy server name 
+proxy.host=
+# The proxy server port
+proxy.port=
+# A user name to use when talking to the proxy server 
+proxy.user=
+# A password to use when talking to the proxy server 
+proxy.password=
+
+#outgoing mail setup.
+#  by default it will use port 25 on localhost. Uncomment and modify 
+# these settings to use another mail server
+# sample values are for gmail setup
+# Note for gmail, you will need to enable account access for less secure apps
+# for this to work. https://www.google.com/settings/security/lesssecureapps
+#mail.smtp.host=smtp.gmail.com
+#mail.smtp.username=xxx@gmail.com
+#mail.smtp.password=xxxpassword
+# port number,  eg for gmail: 465 (ssl) 587 (tls)
+#mail.smtp.port=587
+#mail.from=xxx@gmail.com
+# set the following to ssl or tls
+#mail.security=tls
+
+# flax auxiliary external server url for collocation retrieval
+flax.aux.server=flax.nzdl.org/greenstone3/flax
+wikipedia.server=http://wdm.cs.waikato.ac.nz:8080/services/
+wikipedia.url=http://en.wikipedia.org/wiki/
Index: gs3-extensions/iiif-servlet/trunk/src/src/main/resources/gslanding.vm
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/src/main/resources/gslanding.vm	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/src/main/resources/gslanding.vm	(revision 32707)
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+    #if ($baseUri)
+        <base href="$baseUri/">
+    #end
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <link rel="stylesheet" href="static/styles/landing.css">
+
+    <title>Greenstone3 + Cantaloupe Image Server</title>
+</head>
+
+<body>
+    <div id="container">
+        <img src="static/images/cantaloupe-320.png" width="320" alt="Logo">
+        <h1>Greenstone3 + Cantaloupe Image Server <small>$version</small></h1>
+    </div>
+</body>
+
+</html>
Index: gs3-extensions/iiif-servlet/trunk/src/src/main/webapp/WEB-INF/web.xml
===================================================================
--- gs3-extensions/iiif-servlet/trunk/src/src/main/webapp/WEB-INF/web.xml	(revision 32707)
+++ gs3-extensions/iiif-servlet/trunk/src/src/main/webapp/WEB-INF/web.xml	(revision 32707)
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app version="3.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://java.sun.com/xml/ns/javaee"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
+                  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
+    <display-name>Greenstone3 + Cantaloupe</display-name>
+
+    <context-param>
+        <param-name>org.restlet.application</param-name>
+        <param-value>edu.illinois.library.cantaloupe.GSRestletApplication</param-value>
+    </context-param>
+
+    <listener>
+        <display-name>ApplicationListener</display-name>
+        <listener-class>edu.illinois.library.cantaloupe.ApplicationContextListener</listener-class>
+    </listener>
+    <listener>
+        <display-name>IIOProviderContextListener</display-name>
+        <listener-class>edu.illinois.library.cantaloupe.processor.codec.IIOProviderContextListener</listener-class>
+    </listener>
+
+    <servlet>
+        <servlet-name>ServerServlet</servlet-name>
+        <servlet-class>org.restlet.ext.servlet.ServerServlet</servlet-class>
+        <init-param>
+            <param-name>org.restlet.clients</param-name>
+            <param-value>CLAP HTTP HTTPS</param-value>
+        </init-param>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>ServerServlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+</web-app>
