CVE-2013-10019: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWE-89)
value: A vulnerability was found in OCLC-Research OAICat 1.5.61. It has been rated as critical. This issue affects some unknown processing. The manipulation leads to sql injection. The attack may be initiated remotely. Upgrading to version 1.5.62 is able to address this issue. The identifier of the patch is 6cc65501869fa663bcd24a70b63f41f5cfe6b3e1. It is recommended to upgrade the affected component. The identifier VDB-221489 was assigned to this vulnerability.
CWE description: The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.
CVSS v3 base score: 9.8
Published: 2023-02-20T07:15Z
Repository: https://github.com/OCLC-Research/oaicat
Language: Java
Files changed: .classpath, .project, build.properties, build.xml, docs/README.txt, docs/dist.html, docs/filesys.html, docs/gwen.html, docs/index.html, docs/jdbc.html, docs/newton.html, docs/properties.html, docs/sru.html, docs/version, docs/xmlfile.html, src/ORG/oclc/oai/server/OAIHandler.java, src/ORG/oclc/oai/server/catalog/AbstractCatalog.java, src/ORG/oclc/oai/server/catalog/ExtendedJDBCOAICatalog.java, src/ORG/oclc/oai/server/catalog/ExtendedJDBCRecordFactory.java, src/ORG/oclc/oai/server/catalog/FileRecordFactory.java, src/ORG/oclc/oai/server/catalog/FileSystemOAICatalog.java, src/ORG/oclc/oai/server/catalog/FolderOAICatalog.java, src/ORG/oclc/oai/server/catalog/FolderRecordFactory.java, src/ORG/oclc/oai/server/catalog/IMSXMLRecordFactory.java, src/ORG/oclc/oai/server/catalog/JDBCLimitedOAICatalog.java, src/ORG/oclc/oai/server/catalog/JDBCOAICatalog.java, src/ORG/oclc/oai/server/catalog/JDBCRecordFactory.java, src/ORG/oclc/oai/server/catalog/NewFileRecordFactory.java, src/ORG/oclc/oai/server/catalog/NewFileSystemOAICatalog.java, src/ORG/oclc/oai/server/catalog/NewJDBCOAICatalog.java, src/ORG/oclc/oai/server/catalog/NodeRecordFactory.java, src/ORG/oclc/oai/server/catalog/RecordFactory.java, src/ORG/oclc/oai/server/catalog/SRUOAICatalog.java, src/ORG/oclc/oai/server/catalog/XMLFileOAICatalog.java, src/ORG/oclc/oai/server/catalog/XMLFileRecordFactory.java, src/ORG/oclc/oai/server/catalog/XMLRecordFactory.java, src/ORG/oclc/oai/server/catalog/XerSRUOAICatalog.java, src/ORG/oclc/oai/server/catalog/XerSRURecordFactory.java, src/ORG/oclc/oai/server/catalog/helpers/RecordStringHandler.java, src/ORG/oclc/oai/server/crosswalk/Copycdwalite.java, src/ORG/oclc/oai/server/crosswalk/Copydesc.java, src/ORG/oclc/oai/server/crosswalk/Copymets.java, src/ORG/oclc/oai/server/crosswalk/Copyoai_dc.java, src/ORG/oclc/oai/server/crosswalk/Copyxhtml.java, src/ORG/oclc/oai/server/crosswalk/Copyxoaiconfig.java, src/ORG/oclc/oai/server/crosswalk/Copyxsd.java, src/ORG/oclc/oai/server/crosswalk/Copyxsl.java, src/ORG/oclc/oai/server/crosswalk/Crosswalk.java, src/ORG/oclc/oai/server/crosswalk/CrosswalkItem.java, src/ORG/oclc/oai/server/crosswalk/Crosswalks.java, src/ORG/oclc/oai/server/crosswalk/ExtendedJDBC2oai_dc.java, src/ORG/oclc/oai/server/crosswalk/FileMap2oai_dc.java, src/ORG/oclc/oai/server/crosswalk/FileMap2oai_etdms.java, src/ORG/oclc/oai/server/crosswalk/JDBC2oai_dc.java, src/ORG/oclc/oai/server/crosswalk/NodePassThruCrosswalk.java, src/ORG/oclc/oai/server/crosswalk/XML2oai_dc.java, src/ORG/oclc/oai/server/crosswalk/XML2oai_etdms.java, src/ORG/oclc/oai/server/crosswalk/XSLTCrosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLTmarc21Crosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLTmtxCrosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLToai_dcCrosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLTpro2004Crosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLTproCrosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLTxsdCrosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLTxslCrosswalk.java, src/ORG/oclc/oai/server/crosswalk/XSLTz39_19Crosswalk.java, src/ORG/oclc/oai/server/verb/BadArgumentException.java, src/ORG/oclc/oai/server/verb/BadResumptionTokenException.java, src/ORG/oclc/oai/server/verb/BadVerb.java, src/ORG/oclc/oai/server/verb/CannotDisseminateFormatException.java, src/ORG/oclc/oai/server/verb/GetRecord.java, src/ORG/oclc/oai/server/verb/IdDoesNotExistException.java, src/ORG/oclc/oai/server/verb/Identify.java, src/ORG/oclc/oai/server/verb/ListIdentifiers.java, src/ORG/oclc/oai/server/verb/ListMetadataFormats.java, src/ORG/oclc/oai/server/verb/ListRecords.java, src/ORG/oclc/oai/server/verb/ListSets.java, src/ORG/oclc/oai/server/verb/NoItemsMatchException.java, src/ORG/oclc/oai/server/verb/NoMetadataFormatsException.java, src/ORG/oclc/oai/server/verb/NoRecordsMatchException.java, src/ORG/oclc/oai/server/verb/NoSetHierarchyException.java, src/ORG/oclc/oai/server/verb/OAIInternalServerError.java, src/ORG/oclc/oai/server/verb/ServerVerb.java, src/ORG/oclc/oai/server/verb/extension/GetMetadata.java, src/ORG/oclc/oai/server/verb/extension/Redirect.java, src/ORG/oclc/oai/util/OAIUtil.java, src/com/acme/oai/DummyOAICatalog.java, src/edu/getty/oai/server/catalog/GettyFileRecordFactory.java, src/edu/getty/oai/server/catalog/GettyFileSystemOAICatalog.java, src/gov/lanl/myProject/oaicat/crosswalk/XSLToai_wdCrosswalk.java
Commit date: 2013-10-18 17:27:18 +0000
Commit message: Added a AbstractCatalog.paramRegex property to address an SQL injection vulnerability that was reported by Ward van Wanrooij. [https://code.google.com/p/oaicat/issues/detail?id=7]
Diff: diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..dea198a
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="lib" path="lib/servlet-api.jar"/>
+	<classpathentry kind="lib" path="lib/commons-logging.jar"/>
+	<classpathentry kind="lib" path="lib/xalan.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.project b/.project
new file mode 100644
index 0000000..84b5e02
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>OAICat</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/build.properties b/build.properties
new file mode 100644
index 0000000..3229ed9
--- /dev/null
+++ b/build.properties
@@ -0,0 +1,9 @@
+#app.version=1.5.20
+#catalina.home=/servers/tomcat/apache-tomcat-7.0.2
+#catalina.home=/home/jyoung/tomcat/apache-tomcat-7.0.2
+#catalina.home=E:/Program Files/Apache Software Foundation/Tomcat 5.5
+#junit.jar=/home/jyoung/lib/junit.jar
+#log4j.jar=/home/jyoung/lib/log4j.jar
+#xalan.jar=lib/xalan.jar
+#commons-logging.jar=lib/commons-logging.jar
+#oaicatnewton.jar=/home/jyoung/proj/cvs/oai/oaicatnewton/dist/oaicatnewton.jar
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..60cbc18
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,429 @@
+<!-- A "project" describes a set of targets that may be requested
+     when Ant is executed.  The "default" attribute defines the
+     target which is executed if no specific target is requested,
+     and the "basedir" attribute defines the current working directory
+     from which Ant executes the requested task.  This is normally
+     set to the current working directory.
+-->
+
+
+<project name="oaicatjar" default="dist" basedir=".">
+
+
+
+<!-- ===================== Property Definitions =========================== -->
+
+<!--
+
+  Each of the following properties are used in the build script.
+  Values for these properties are set by the first place they are
+  defined, from the following list:
+  * Definitions on the "ant" command line (ant -Dcatalina.home=xyz compile)
+  * Definitions from a "build.properties" file in the top level
+    source directory
+  * Definitions from a "build.properties" file in the developer's
+    home directory
+  * Default definitions in this build.xml file
+
+  You will note below that property values can be composed based on the
+  contents of previously defined properties.  This is a powerful technique
+  that helps you minimize the number of changes required when your development
+  environment is modified.  Note that property composition is allowed within
+  "build.properties" files as well as in the "build.xml" script.
+
+-->
+
+  <property file="build.properties"/>
+  <property file="${user.home}/build.properties"/>
+
+
+<!-- ==================== File and Directory Names ======================== -->
+
+<!--
+
+  These properties generally define file and directory names (or paths) that
+  affect where the build process stores its outputs.
+
+  app.name             Base name of this application, used to
+                       construct filenames and directories.
+                       Defaults to "myapp".
+
+  appp.version          Version identifier for this application.
+
+  build.home           The directory into which the "prepare" and
+                       "compile" targets will generate their output.
+                       Defaults to "build".
+
+  catalina.home        The directory in which you have installed
+                       a binary distribution of Tomcat 4.  This will
+                       be used by the "deploy" target.
+
+  dist.home            The name of the base directory in which
+                       distribution files are created.
+                       Defaults to "dist".
+
+  distv.home           The name of the distributiondirectory in which
+                       distribution files are created for a version.
+                       Defaults to "app.name-appp.version".
+
+-->
+
+  <property name="app.name"      value="oaicat"/>
+  <property name="appp.version"   value="1.5.62"/>
+  <property name="build.home"    value="build"/>
+  <property name="catalina.home" value="../../../.."/> <!-- UPDATE THIS! -->
+  <property name="dist.home"     value="dist"/>
+  <property name="distv.home"    value="${app.name}-${appp.version}"/>
+  <property name="test.dir"      location="${build.home}/test" />
+  <property name="test.data.dir" location="${test.dir}/data" />
+  <property name="test.reports.dir" location="${test.dir}/reports" />
+
+<!--  ==================== Compilation Control Options ==================== -->
+
+<!--
+
+  These properties control option settings on the Javac compiler when it
+  is invoked using the <javac> task.
+
+  compile.debug        Should compilation include the debug option?
+
+  compile.deprecation  Should compilation include the deprecation option?
+
+  compile.optimize     Should compilation include the optimize option?
+
+-->
+
+  <property name="compile.debug"       value="true"/>
+  <property name="compile.deprecation" value="true"/>
+  <property name="compile.optimize"    value="true"/>
+
+
+
+<!-- ==================== External Dependencies =========================== -->
+
+
+<!--
+
+  Use property values to define the locations of external JAR files on which
+  your application will depend.  In general, these values will be used for
+  two purposes:
+  * Inclusion on the classpath that is passed to the Javac compiler
+
+  Because we will automatically include all of the Java classes that Tomcat 4
+  exposes to web applications, we will not need to explicitly list any of those
+  dependencies.  You only need to worry about external dependencies for JAR
+  files that you are going to include inside your "/WEB-INF/lib" directory.
+
+-->
+
+<!-- external dependency -->
+  <!--
+  <property name="foo.jar"
+           value="/home/me/foo.jar"/>
+  -->
+
+
+<!-- ==================== Compilation Classpath =========================== -->
+
+<!--
+
+  Rather than relying on the CLASSPATH environment variable, Ant includes
+  features that makes it easy to dynamically construct the classpath you
+  need for each compilation.  The example below constructs the compile
+  classpath to include the servlet.jar file, as well as the other components
+  that Tomcat makes available to web applications automatically, plus anything
+  that you explicitly added.
+
+-->
+
+  <path id="compile.classpath">
+
+    <!-- Include all JAR files that will be included in /WEB-INF/lib -->
+    <!-- *** CUSTOMIZE HERE AS REQUIRED BY YOUR APPLICATION *** -->
+
+    <pathelement location="${junit.jar}"/>
+<!--
+    <pathelement location="${srw.jar}" />
+-->
+    <pathelement location="${commons-logging.jar}"/>
+    <pathelement location="${xalan.jar}"/>
+
+    <!-- Include all elements that Tomcat exposes to applications -->
+    <pathelement location="${catalina.home}/common/classes"/>
+    <fileset dir="${catalina.home}/lib">
+      <include name="*.jar"/>
+    </fileset>
+    <pathelement location="${catalina.home}/classes"/>
+    <fileset dir="${catalina.home}/lib">
+      <include name="*.jar"/>
+    </fileset>
+
+  </path>
+
+
+
+<!-- ==================== All Target ====================================== -->
+
+<!--
+
+  The "all" target is a shortcut for running the "clean" target followed
+  by the "compile" target, to force a complete recompile.
+
+-->
+
+  <target name="all" depends="clean,dist"
+   description="Clean build and dist, then compile and test"/>
+
+
+
+<!-- ==================== Clean Target ==================================== -->
+
+<!--
+
+  The "clean" target deletes any previous "build" and "dist" directory,
+  so that you can be ensured the application can be built from scratch.
+
+-->
+
+  <target name="clean"
+   description="Delete old build and dist directories">
+    <delete dir="${build.home}"/>
+    <delete dir="${dist.home}"/>
+    <delete dir="${distv.home}"/>
+  </target>
+
+
+
+<!-- ==================== Compile Target ================================== -->
+  <target name="JUNIT">
+    <available property="junit.present" classname="junit.framework.TestCase"
+               classpathref="compile.classpath" />
+  </target>
+
+<!--
+
+  The "compile" target transforms source files (from your "src" directory)
+  into object files in the appropriate location in the build directory.
+  This example assumes that you will be including your classes as a jar file.
+
+-->
+
+  <target name="compile" depends="prepare"
+   description="Compile Java sources">
+
+    <!-- Compile Java classes as necessary -->
+    <javac srcdir="src"
+          destdir="${build.home}"
+           debug="${compile.debug}"
+           includeAntRuntime="false"
+     deprecation="${compile.deprecation}"
+        optimize="${compile.optimize}">
+        <classpath refid="compile.classpath"/>
+    </javac>
+
+    <!-- Copy associated resource files -->
+    <copy  todir="${build.home}">
+    <fileset dir="src" includes="**/*.properties"/>
+    </copy>
+
+  </target>
+
+
+
+<!-- ==================== Jar Target ===================================== -->
+
+
+<!--
+
+  The "dist" target creates a binary distribution of your application
+  in a directory structure ready to be archived in a tar.gz or zip file.
+  Note that this target depends on two others:
+  * "javadoc" so that the application Javadocs will have been created
+
+-->
+
+  <target name="jar" depends="compile"
+   description="Create binary distribution">
+
+    <mkdir dir="${build.home}/lib" />
+    <!-- Create application JAR file -->
+    <jar jarfile="${build.home}/lib/${app.name}.jar"
+         basedir="${build.home}"
+         includes="ORG/** com/** edu/**">
+      <manifest>
+        <attribute name="Specification-Title" value="OAI-PMH"/>
+        <attribute name="Specification-Version" value="2.0"/>
+        <attribute name="Specification-Vendor" value="Open Archives Initiative"/>
+        <attribute name="Implementation-Title" value="OAICat"/>
+        <attribute name="Implementation-Version" value="${appp.version} ${TODAY}"/>
+        <attribute name="Implementation-Vendor" value="OCLC, Online Computer Library Center"/>
+        <attribute name="Implementation-URL" value="http://www.oclc.org/research/software/oai/cat.shtm"/>
+      </manifest>
+    </jar>
+
+  </target>
+
+<!-- ==================== Dist Target ===================================== -->
+
+
+<!--
+
+  The "dist" target creates a binary distribution of your application
+  in a directory structure ready to be archived in a tar.gz or zip file.
+  Note that this target depends on two others:
+  * "javadoc" so that the application Javadocs will have been created
+
+-->
+
+  <target name="dist" depends="javadoc"
+   description="Create binary distribution">
+
+    <!-- Copy documentation subdirectory -->
+    <copy    todir="${dist.home}/docs">
+      <fileset dir="${distv.home}/docs"/>
+    </copy>
+
+    <!-- Copy stuff into dist-version -->
+    <copy file="${build.home}/lib/${app.name}.jar" todir="${distv.home}" />
+    <copy file="${build.home}/lib/${app.name}.jar" todir="${dist.home}" />
+<!--
+    <copy file="${oaicatnewton.jar}" todir="${distv.home}" />
+    <copy file="${oaicatnewton.jar}" todir="${dist.home}" />
+-->
+    <copy    todir="${distv.home}">
+      <fileset dir="docs"/>
+    </copy>
+    <copy    todir="${dist.home}">
+      <fileset dir="docs"/>
+    </copy>
+    <copy    todir="${distv.home}/src">
+      <fileset dir="src" excludes="test/**,**/*.BAK"/>
+    </copy>
+
+    <!-- Create dist TAR file -->
+    <delete file="${distv.home}.tar.gz" />
+    <tar tarfile="${distv.home}.tar.gz"
+         includes="${distv.home}/**"
+         compression="gzip"
+         basedir="."/>
+    <tar tarfile="src${distv.home}.tar.gz"
+         includes="src/**"
+         compression="gzip"
+         basedir="."/>
+
+    <delete file="${distv.home}.zip" />
+    <zip destfile="${distv.home}.zip"
+         includes="${distv.home}/**"
+         basedir="."/>
+    <zip destfile="src${distv.home}.zip"
+         includes="src/**"
+         basedir="."/>
+
+    <copy    todir="${dist.home}">
+      <fileset dir="." includes="*.tar.gz *.zip"/>
+    </copy>
+
+    <!-- Copy additional files to ${dist.home} as necessary -->
+
+  </target>
+
+
+
+<!-- ==================== Test Target ===================================== -->
+
+
+<!--
+
+  The "test" target does unit testing using JUnit.
+-->
+
+<!--
+  <target name="test" depends="jar"
+   description="Test the application">
+
+    <mkdir dir="${test.data.dir}" />
+    <mkdir dir="${test.reports.dir}" />
+    <junit printsummary="withOutAndErr" fork="yes"
+           errorProperty="test.failed"
+           failureProperty="test.failed">
+        <sysproperty key="app.version" value="${appp.version}" />
+        <formatter type="brief" usefile="false"/>
+        <formatter type="xml" />
+        <batchtest todir="${test.data.dir}">
+          <fileset dir="${build.home}" includes="test/**/*Test.class" />
+        </batchtest>
+        <classpath>
+          <pathelement location="${junit.jar}" />
+          <pathelement location="${xalan.jar}" />
+          <pathelement location="${distv.home}/${app.name}.jar" />
+          <pathelement location="${build.home}" />
+          <pathelement location="${catalina.home}/common/classes"/>
+          <fileset dir="${catalina.home}/common/lib">
+            <include name="*.jar"/>
+          </fileset>
+          <pathelement location="${catalina.home}/classes"/>
+          <fileset dir="${catalina.home}/common/lib">
+            <include name="*.jar"/>
+          </fileset>
+        </classpath>
+    </junit>
+
+    <junitreport todir="${test.data.dir}">
+      <fileset dir="${test.data.dir}">
+        <include name="TEST-*.xml" />
+      </fileset>
+      <report format="frames" todir="${test.reports.dir}" />
+    </junitreport>
+
+
+    <fail message="Tests failed. Check log and/or reports." if="test.failed" />
+  </target>
+-->
+
+
+<!-- ==================== Javadoc Target ================================== -->
+
+<!--
+
+  The "javadoc" target creates Javadoc API documentation for the Java
+  classes included in your application.  Normally, this is only required
+  when preparing a distribution release, but is available as a separate
+  target in case the developer wants to create Javadocs independently.
+
+-->
+
+  <target name="javadoc" depends="jar"
+   description="Create Javadoc API documentation">
+
+    <mkdir          dir="${distv.home}/docs/api"/>
+    <javadoc sourcepath="src"
+                destdir="${distv.home}/docs/api"
+           packagenames="ORG.oclc.*,edu.*">
+      <classpath refid="compile.classpath"/>
+    </javadoc>
+
+  </target>
+
+
+
+<!-- ==================== Prepare Target ================================== -->
+
+<!--
+
+  The "prepare" target is used to create the "build" destination directory,
+  and copy the static contents of your web application to it.  If you need
+  to copy static files from external dependencies, you can customize the
+  contents of this task.
+
+  Normally, this task is executed indirectly when needed.
+
+-->
+
+  <target name="prepare">
+
+    <!-- Create build directory and copy static content -->
+    <mkdir  dir="${build.home}"/>
+    <tstamp/>
+  </target>
+
+</project>
diff --git a/docs/README.txt b/docs/README.txt
new file mode 100644
index 0000000..110529b
--- /dev/null
+++ b/docs/README.txt
@@ -0,0 +1,86 @@
+To upgrade OAICat with the latest code changes, copy the latest
+oaicat.jar file to webapps/oaicat/WEB-INF/lib/.
+
+Before customizing OAICat, first install oaicat.war in a J2EE Servlet
+Engine and verify that the default configuration works. If so, proceed
+with any necessary code and configuration changes as described below.
+
+Before building this probject with Ant, create a 'build.properties'
+file in the project directory with the following entries:
+
+catalina.home=/path/to/jakarta-tomcat
+
+To create a new distribution set, issue the command:
+
+ant dist
+
+To customize OAICat, answer these questions:
+
+Q1: What Java package should I use to hold my custom classes?
+    a) For example, if you work for Acme Inc., create a directory
+       hierarchy somewhere named:
+
+       com/acme/oai
+
+Q2: What database engine will I use?
+    a) For example, if using the Foo database, copy
+       oaicatjar/src/ORG/oclc/oai/server/catalog/DummyOAICatalog.java
+       to com/acme/oai/server/catalog/FooOAICatalog.java and modify the
+       code so the class name matches the new filename.
+    b) Change the code in this class to use the Foo database Java API.
+       In general, all this class needs to know about the records
+       is that they are black-box Java Objects. To make life easier
+       downstream, however, it may be worthwhile to convert the records
+       to a more convenient processing form immediately after reading.
+       For example, if the records are stored as XML Strings, load
+       them into DOM objects as soon as they are read. Beyond that,
+       though, leave it to the Crosswalk and RecordFactory
+       implementations to understand the true semantics of the records.
+       Doing this may mean you can't reuse this class for cases where
+       the database returns non-XML byte arrays, but then again, what
+       are the chances of that?
+    c) Make a corresponding package/class name change to the
+       AbstractCatalog.oaiCatalogClassName entry in the
+       webapps/oaicat/WEB-INF/oaicat.properties file to have OAICat use
+       your custom class.
+
+Q3: What are the semantics of these record objects?
+    a) If FooOAICatalog returns records as byte arrays, examples can be
+       anything such as MARC Communications Format. If FooOAICatalog
+       returns Strings, examples might include MARC BER, or any kind of
+       XML String. If FooOAICatalog returns DOM Documents, examples can
+       be any XML-based metadata format. Let's assume FooOAICatalog
+       returns records as DOM Documents containing MARCXML content.
+    b) Copy oaicatjar/src/ORG/oclc/oai/server/catalog/XMLRecordFactory.java
+       to com/acme/oai/server/catalog/MARCXMLDOMRecordFactory.java
+       and modify the code so the class name matches the new filename.
+    c) Change the methods to cast each Object nativeItem parameter to a
+       org.w3c.dom.Document and use it to extract the relevant data for
+       each method.
+    d) Make a corresponding package/class name change to the
+       AbstractCatalog.recordFactoryClassName entry in the
+       webapps/oaicat/WEB-INF/oaicat.properties file to have OAICat use
+       your custom class.
+
+Q4: What OAI metadatdaFormats will be supported?
+    a) Examples include oai_dc, marcxml, or oai_etdms.
+    b) For oai_dc, copy oaicatjar/src/ORG/oclc/oai/crosswalk/XML2oai_dc.java
+       to com/acme/oai/server/catalog/MARCXMLDOM2oai_dc.java and modify
+       the code so the class name matches the new filename.
+    c) Change the constructor to use the appropriate schemaLocation for
+       this metadataFormat.
+    d) Change the methods to cast each Object nativeItem parameter to a
+       org.w3c.dom.Document and use it to service the method accordingly.
+       In this case, you could use the Library of Congress MARCXML to DC
+       XSL stylesheet (see http://www.loc.gov/standards/marcxml/) to
+       perform the crosswalk to Dublin Core.
+    e) Repeat steps b, c, and d for each metadatdaFormat to be supported.
+    f) Make a corresponding package/class name change to the
+       Crosswalks.* entries in the webapps/oaicat/WEB-INF/oaicat.properties
+       file to have OAICat use your custom classes.
+
+Finally, change other properties in oaicat.properties according to your
+preferences.
+
+That's essentially what it takes to customize OAICat. Contact Jeff Young
+at jyoung@oclc.org with questions and comments.
diff --git a/docs/banner01.gif b/docs/banner01.gif
new file mode 100644
index 0000000..2ae047f
Binary files /dev/null and b/docs/banner01.gif differ
diff --git a/docs/banner02.gif b/docs/banner02.gif
new file mode 100644
index 0000000..3ba2058
Binary files /dev/null and b/docs/banner02.gif differ
diff --git a/docs/dist.html b/docs/dist.html
new file mode 100644
index 0000000..13f9940
--- /dev/null
+++ b/docs/dist.html
@@ -0,0 +1,176 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+  <head>
+    <title>OAICat Distribution Files</title>
+  </head>
+
+  <body>
+    <h1>OAICat Distribution Files</h1>
+    <ul>
+      <li><a href="oaicat-1.5.62.tar.gz">oaicat-1.5.62.tar.gz</a> Latest version</li>
+      <li><a href="oaicat-1.5.62.zip">oaicat-1.5.62.zip</a> Latest version</li>
+
+      <li><a href="oaicat-1.5.61.tar.gz">oaicat-1.5.61.tar.gz</a></li>
+      <li><a href="oaicat-1.5.61.zip">oaicat-1.5.61.zip</a></li>
+
+      <li><a href="oaicat-1.5.60.tar.gz">oaicat-1.5.60.tar.gz</a></li>
+      <li><a href="oaicat-1.5.60.zip">oaicat-1.5.60.zip</a></li>
+
+      <li><a href="oaicat-1.5.59.tar.gz">oaicat-1.5.59.tar.gz</a></li>
+      <li><a href="oaicat-1.5.59.zip">oaicat-1.5.59.zip</a></li>
+
+      <li><a href="oaicat-1.5.58.tar.gz">oaicat-1.5.58.tar.gz</a></li>
+      <li><a href="oaicat-1.5.58.zip">oaicat-1.5.58.zip</a></li>
+
+      <li><a href="oaicat-1.5.57.tar.gz">oaicat-1.5.57.tar.gz</a></li>
+      <li><a href="oaicat-1.5.57.zip">oaicat-1.5.57.zip</a></li>
+
+      <li><a href="oaicat-1.5.56.tar.gz">oaicat-1.5.56.tar.gz</a></li>
+      <li><a href="oaicat-1.5.56.zip">oaicat-1.5.56.zip</a></li>
+
+      <li><a href="oaicat-1.5.55.tar.gz">oaicat-1.5.55.tar.gz</a></li>
+      <li><a href="oaicat-1.5.55.zip">oaicat-1.5.55.zip</a></li>
+
+      <li><a href="oaicat-1.5.54.tar.gz">oaicat-1.5.54.tar.gz</a></li>
+      <li><a href="oaicat-1.5.54.zip">oaicat-1.5.54.zip</a></li>
+
+      <li><a href="oaicat-1.5.53.tar.gz">oaicat-1.5.53.tar.gz</a></li>
+      <li><a href="oaicat-1.5.53.zip">oaicat-1.5.53.zip</a></li>
+
+      <li><a href="oaicat-1.5.52.tar.gz">oaicat-1.5.52.tar.gz</a></li>
+      <li><a href="oaicat-1.5.52.zip">oaicat-1.5.52.zip</a></li>
+
+      <li><a href="oaicat-1.5.51.tar.gz">oaicat-1.5.51.tar.gz</a></li>
+      <li><a href="oaicat-1.5.51.zip">oaicat-1.5.51.zip</a></li>
+
+      <li><a href="oaicat-1.5.50.tar.gz">oaicat-1.5.50.tar.gz</a></li>
+      <li><a href="oaicat-1.5.50.zip">oaicat-1.5.50.zip</a></li>
+
+      <li><a href="oaicat-1.5.49.tar.gz">oaicat-1.5.49.tar.gz</a></li>
+      <li><a href="oaicat-1.5.49.zip">oaicat-1.5.49.zip</a></li>
+
+      <li><a href="oaicat-1.5.48.tar.gz">oaicat-1.5.48.tar.gz</a></li>
+      <li><a href="oaicat-1.5.48.zip">oaicat-1.5.48.zip</a></li>
+
+      <li><a href="oaicat-1.5.47.tar.gz">oaicat-1.5.47.tar.gz</a></li>
+      <li><a href="oaicat-1.5.47.zip">oaicat-1.5.47.zip</a></li>
+
+      <li><a href="oaicat-1.5.46.tar.gz">oaicat-1.5.46.tar.gz</a></li>
+      <li><a href="oaicat-1.5.46.zip">oaicat-1.5.46.zip</a></li>
+
+      <li><a href="oaicat-1.5.45.tar.gz">oaicat-1.5.45.tar.gz</a></li>
+      <li><a href="oaicat-1.5.45.zip">oaicat-1.5.45.zip</a></li>
+
+      <li><a href="oaicat-1.5.44.tar.gz">oaicat-1.5.44.tar.gz</a></li>
+      <li><a href="oaicat-1.5.44.zip">oaicat-1.5.44.zip</a></li>
+
+      <li><a href="oaicat-1.5.43.tar.gz">oaicat-1.5.43.tar.gz</a></li>
+      <li><a href="oaicat-1.5.43.zip">oaicat-1.5.43.zip</a></li>
+
+      <li><a href="oaicat-1.5.42.tar.gz">oaicat-1.5.42.tar.gz</a></li>
+      <li><a href="oaicat-1.5.42.zip">oaicat-1.5.42.zip</a></li>
+
+      <li><a href="oaicat-1.5.41.tar.gz">oaicat-1.5.41.tar.gz</a></li>
+      <li><a href="oaicat-1.5.41.zip">oaicat-1.5.41.zip</a></li>
+
+      <li><a href="oaicat-1.5.40.tar.gz">oaicat-1.5.40.tar.gz</a></li>
+      <li><a href="oaicat-1.5.40.zip">oaicat-1.5.40.zip</a></li>
+
+      <li><a href="oaicat-1.5.39.tar.gz">oaicat-1.5.39.tar.gz</a></li>
+      <li><a href="oaicat-1.5.39.zip">oaicat-1.5.39.zip</a></li>
+
+      <li><a href="oaicat-1.5.38.tar.gz">oaicat-1.5.38.tar.gz</a></li>
+      <li><a href="oaicat-1.5.38.zip">oaicat-1.5.38.zip</a></li>
+
+      <li><a href="oaicat-1.5.37.tar.gz">oaicat-1.5.37.tar.gz</a></li>
+      <li><a href="oaicat-1.5.37.zip">oaicat-1.5.37.zip</a></li>
+
+      <li><a href="oaicat-1.5.36.tar.gz">oaicat-1.5.36.tar.gz</a></li>
+      <li><a href="oaicat-1.5.36.zip">oaicat-1.5.36.zip</a></li>
+
+      <li><a href="oaicat-1.5.35.tar.gz">oaicat-1.5.35.tar.gz</a></li>
+      <li><a href="oaicat-1.5.35.zip">oaicat-1.5.35.zip</a></li>
+
+      <li><a href="oaicat-1.5.34.tar.gz">oaicat-1.5.34.tar.gz</a></li>
+      <li><a href="oaicat-1.5.34.zip">oaicat-1.5.34.zip</a></li>
+
+      <li><a href="oaicat-1.5.33.tar.gz">oaicat-1.5.33.tar.gz</a></li>
+      <li><a href="oaicat-1.5.33.zip">oaicat-1.5.33.zip</a></li>
+
+      <li><a href="oaicat-1.5.32.tar.gz">oaicat-1.5.32.tar.gz</a></li>
+      <li><a href="oaicat-1.5.32.zip">oaicat-1.5.32.zip</a></li>
+
+      <li><a href="oaicat-1.5.31.tar.gz">oaicat-1.5.31.tar.gz</a></li>
+      <li><a href="oaicat-1.5.31.zip">oaicat-1.5.31.zip</a></li>
+
+      <li><a href="oaicat-1.5.30.tar.gz">oaicat-1.5.30.tar.gz</a></li>
+      <li><a href="oaicat-1.5.30.zip">oaicat-1.5.30.zip</a></li>
+
+      <li><a href="oaicat-1.5.29.tar.gz">oaicat-1.5.29.tar.gz</a></li>
+      <li><a href="oaicat-1.5.29.zip">oaicat-1.5.29.zip</a></li>
+
+      <li><a href="oaicat-1.5.28.tar.gz">oaicat-1.5.28.tar.gz</a></li>
+      <li><a href="oaicat-1.5.28.zip">oaicat-1.5.28.zip</a></li>
+
+      <li><a href="oaicat-1.5.27.tar.gz">oaicat-1.5.27.tar.gz</a></li>
+      <li><a href="oaicat-1.5.27.zip">oaicat-1.5.27.zip</a></li>
+
+      <li><a href="oaicat-1.5.26.tar.gz">oaicat-1.5.26.tar.gz</a></li>
+      <li><a href="oaicat-1.5.26.zip">oaicat-1.5.26.zip</a></li>
+
+      <li><a href="oaicat-1.5.25.tar.gz">oaicat-1.5.25.tar.gz</a></li>
+      <li><a href="oaicat-1.5.25.zip">oaicat-1.5.25.zip</a></li>
+
+      <li><a href="oaicat-1.5.24.tar.gz">oaicat-1.5.24.tar.gz</a></li>
+      <li><a href="oaicat-1.5.24.zip">oaicat-1.5.24.zip</a></li>
+
+      <li><a href="oaicat-1.5.23.tar.gz">oaicat-1.5.23.tar.gz</a></li>
+      <li><a href="oaicat-1.5.23.zip">oaicat-1.5.23.zip</a></li>
+
+      <li><a href="oaicat-1.5.22.tar.gz">oaicat-1.5.22.tar.gz</a></li>
+      <li><a href="oaicat-1.5.22.zip">oaicat-1.5.22.zip</a></li>
+
+      <li><a href="oaicat-1.5.21.tar.gz">oaicat-1.5.21.tar.gz</a></li>
+      <li><a href="oaicat-1.5.21.zip">oaicat-1.5.21.zip</a></li>
+
+      <li><a href="oaicat-1.5.20.tar.gz">oaicat-1.5.20.tar.gz</a></li>
+      <li><a href="oaicat-1.5.20.zip">oaicat-1.5.20.zip</a></li>
+
+      <li><a href="oaicat-1.5.19.tar.gz">oaicat-1.5.19.tar.gz</a></li>
+      <li><a href="oaicat-1.5.19.zip">oaicat-1.5.19.zip</a></li>
+
+      <li><a href="oaicat-1.5.18.tar.gz">oaicat-1.5.18.tar.gz</a></li>
+      <li><a href="oaicat-1.5.18.zip">oaicat-1.5.18.zip</a></li>
+
+      <li><a href="oaicat-1.5.17.tar.gz">oaicat-1.5.17.tar.gz</a></li>
+      <li><a href="oaicat-1.5.17.zip">oaicat-1.5.17.zip</a></li>
+
+      <li><a href="oaicat-1.5.16.tar.gz">oaicat-1.5.16.tar.gz</a></li>
+      <li><a href="oaicat-1.5.16.zip">oaicat-1.5.16.zip</a></li>
+      <li><a href="oaicat-1.5.15.tar.gz">oaicat-1.5.15.tar.gz</a></li>
+      <li><a href="oaicat-1.5.15.zip">oaicat-1.5.15.zip</a></li>
+      <li><a href="oaicat-1.5.14.tar.gz">oaicat-1.5.14.tar.gz</a></li>
+      <li><a href="oaicat-1.5.14.zip">oaicat-1.5.14.zip</a></li>
+      <li><a href="oaicat-1.5.13.tar.gz">oaicat-1.5.13.tar.gz</a></li>
+      <li><a href="oaicat-1.5.13.zip">oaicat-1.5.13.zip</a></li>
+      <li><a href="oaicat-1.5.12.tar.gz">oaicat-1.5.12.tar.gz</a></li>
+      <li><a href="oaicat-1.5.12.zip">oaicat-1.5.12.zip</a></li>
+      <li><a href="oaicat-1.5.10.tar.gz">oaicat-1.5.10.tar.gz</a></li>
+      <li><a href="oaicat-1.5.10.zip">oaicat-1.5.10.zip</a></li>
+      <!--
+      <li><a href="oaicat-1.5.9.tar.gz">oaicat-1.5.9.tar.gz</a></li>
+      <li><a href="oaicat-1.5.9.zip">oaicat-1.5.9.zip</a></li>
+      <li><a href="oaicat-1.5.8.tar.gz">oaicat-1.5.8.tar.gz</a></li>
+      <li><a href="oaicat-1.5.8.zip">oaicat-1.5.8.zip</a></li>
+      -->
+    </ul>
+
+
+    <hr>
+    <address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address>
+<!-- Created: Tue Apr 22 10:48:02 EDT 2003 -->
+<!-- hhmts start -->
+Last modified: Thu Sep 08 16:00:50 Eastern Daylight Time 2005
+<!-- hhmts end -->
+  </body>
+</html>
diff --git a/docs/filesys.html b/docs/filesys.html
new file mode 100644
index 0000000..57cb489
--- /dev/null
+++ b/docs/filesys.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<title>OAICat - File System Implementation</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+<link href="oaicat.css" rel="stylesheet" type="text/css">
+</head>
+<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+<table width="100%" border="0" cellspacing="20" cellpadding="0">
+<tr valign="top">
+<td bgcolor="#669933">
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr valign="top">
+<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+</tr>
+</table>
+</td>
+</tr>
+<tr valign="top">
+<td>
+<h2>File System Implementation</h2>
+<p>A file system implementation is included with the OAICat package. The purpose is to provide OAI access to metadata records stored in files in the server's directory structure.</p>
+<p>To test the file system implementation, follow these steps:</p>
+<ol>
+<li>Copy oaicat.war to Tomcat's webapps directory, restart Tomcat, and test the default configuration.</li>
+<li>Restart Tomcat and test.</li>
+</ol>
+<p>If this works, change the values in webapps/oaicat/WEB-INF/oaicat.properties to suit your needs.</p>
+</td>
+</tr>
+<tr valign="top">
+<td bgcolor="#669933" height="1"></td>
+</tr>
+<tr valign="top">
+<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+</tr>
+<tr valign="top">
+<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+<!-- hhmts start -->
+Last modified: Mon Apr 21 15:38:29 EDT 2003
+<!-- hhmts end -->
+</span></td>
+</tr>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/docs/gwen.html b/docs/gwen.html
new file mode 100644
index 0000000..54a48f1
--- /dev/null
+++ b/docs/gwen.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<title>OAICat - Gwen Implementation</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+<link href="oaicat.css" rel="stylesheet" type="text/css">
+</head>
+<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+<table width="100%" border="0" cellspacing="20" cellpadding="0">
+<tr valign="top">
+<td bgcolor="#669933">
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr valign="top">
+<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+</tr>
+</table>
+</td>
+</tr>
+<tr valign="top">
+<td>
+<h2>Gwen Implementation</h2>
+<p>A Gwen implementation is included with the OAICat package. <a href="http://www.oclc.org/research/software/gwen/">Gwen</a> is an open-source search engine intended to work with OCLC's open-source <a href="http://www.oclc.org/research/software/pears/">Pears</a> database engine. Contact me for details.</p>
+</td>
+</tr>
+<tr valign="top">
+<td bgcolor="#669933" height="1"></td>
+</tr>
+<tr valign="top">
+<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+</tr>
+<tr valign="top">
+<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+<!-- hhmts start -->
+Last modified: Mon Apr 21 15:38:25 EDT 2003
+<!-- hhmts end -->
+</span></td>
+</tr>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..96364f6
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<title>OAICat - An OAI-PMH v2 Repository Framework</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+<link href="oaicat.css" rel="stylesheet" type="text/css">
+</head>
+<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+<table width="100%" border="0" cellspacing="20" cellpadding="0">
+<tr valign="top">
+<td bgcolor="#669933">
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr valign="top">
+<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+</tr>
+</table>
+</td>
+</tr>
+<tr valign="top">
+<td>
+	  <h2>Description</h2>
+    <p>The <a href="http://www.oclc.org/research/software/oai/cat.shtm">OAICat</a> Open Source project is a Java Servlet web application providing an <a href="http://www.openarchives.org/">OAI-PMH v2.0</a>
+    repository framework. This framework can be customized to work with arbitrary data repositories
+    by implementing some Java interfaces.</p>
+          <h2>Installation</h2>
+          <p>This distribution includes the oaicat.jar file containing the Java Servlet classes only. To update existing OAICat installations, replace the old oaicat/WEB-INF/lib/oaicat.jar file with the one included with this distribution. To do an initial installation of OAICat, download the OAICat <a href="http://www.oclc.org/research/software/oai/cat.shtm">Webapp</a> instead, place it in the J2EE webapp directory, and bounce the server.</p>
+          <h2>For Your Own Good!</h2>
+          <p>This is an open-source project, and the source code for oaicat.jar is included in this distribution along with a prebuild oaicat.jar file. It's in everyone's interest, though, if you resist the urge to change this code to satisfy local needs. It is much better to extend a given class and override its methods rather than change the code directly. This approach will allow you to download a fresh copy of oaicat.jar in the future to pick up bug fixes and enhancements without worrying about reconciling your changes with mine. This isn't always possible, though, so if you have any doubt or trouble with this request, please contact <a href="mailto:jyoung@oclc.org">Jeff Young</a> for assistance.</p>
+    <h2>License Information</h2>
+    <a href="http://purl.oclc.org/oclc/research/ORPL/">OCLC Research Public License</a>
+</td>
+</tr>
+<tr valign="top">
+<td>
+<h2>Documentation</h2>
+          <ul>
+            <li><a href="docs/api/index.html">Javadocs</a></li>
+            <li><a href="properties.html">Description of entries in the <code>oaicat.properties</code> file</a></li>
+            <li><a href="README.txt">Customization Notes</a></li>
+            <li>Sample Implementations
+<ol>
+<li><a href="filesys.html">File System Implementation (default)</a></li>
+<li><a href="xmlfile.html">Single XML File Implementation</a></li>
+<li><a href="sru.html">SRW/U Implementation</a></li>
+<li><a href="jdbc.html">JDBC Implementation</a></li>
+<li><a href="newton.html">Newton Implementation</a></li>
+<li><a href="gwen.html">Gwen Implementation</a></li>
+</ol></li>
+          </ul>
+</td>
+</tr>
+<tr valign="top">
+<td bgcolor="#669933" height="1"></td>
+</tr>
+<tr valign="top">
+<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+</tr>
+<tr valign="top">
+<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+<!-- hhmts start -->
+Last modified: Tue Jan 04 09:40:18 Eastern Standard Time 2005
+<!-- hhmts end -->
+</span></td>
+</tr>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/docs/jdbc.html b/docs/jdbc.html
new file mode 100644
index 0000000..7d2cf21
--- /dev/null
+++ b/docs/jdbc.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<title>OAICat - JDBC Implementation</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+<link href="oaicat.css" rel="stylesheet" type="text/css">
+</head>
+<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+<table width="100%" border="0" cellspacing="20" cellpadding="0">
+<tr valign="top">
+<td bgcolor="#669933">
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr valign="top">
+<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+</tr>
+</table>
+</td>
+</tr>
+<tr valign="top">
+<td>
+<h2>JDBC Implementation</h2>
+<p>A JDBC implementation is included with the OAICat package. In theory, it should be possible to customize OAICat to work with any JDBC accessible database by merely changing a few entries in the application's properties file. If the reality isn't so simple, let me know and I can attempt to generalize it still further.</p>
+<p>For Microsoft Windows users, a sample Access database named oaicatdemo.mdb is included in the war file distribution. To test the JDBC implementation against this JDBC source, follow these steps:</p>
+<ol>
+<li>Copy oaicat.war to Tomcat's webapps directory, restart Tomcat, and test the default configuration.</li>
+<li>If it works, start the ODBC Data Source Administrator control panel, click the "System DSN" tab, then "Add...".</li>
+<li>Choose "Microsoft Access Driver (*.mdb).</li>
+<li>Type "oaicatdemo" in the Data Source Name box, and browse to select "oaicatdemo.mdb" located in the webapps/oaicat/ directory.</li>
+<li>Close the ODBC Data Source Administrator control panel.</li>
+<li>Delete webapps/oaicat/WEB-INF/oaicat.properties.</li>
+<li>Rename webapps/oaicat/WEB-INF/jdbc.properties to oaicat.properties.</li>
+<li>Restart Tomcat and test.</li>
+</ol>
+<p>If this works, change the values in webapps/oaicat/WEB-INF/oaicat.properties (formerly jdbc.properties) to suit your needs.</p>
+</td>
+</tr>
+<tr valign="top">
+<td bgcolor="#669933" height="1"></td>
+</tr>
+<tr valign="top">
+<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+</tr>
+<tr valign="top">
+<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+<!-- hhmts start -->
+Last modified: Mon Apr 21 15:38:16 EDT 2003
+<!-- hhmts end -->
+</span></td>
+</tr>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/docs/newton.html b/docs/newton.html
new file mode 100644
index 0000000..0e7a23b
--- /dev/null
+++ b/docs/newton.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<title>OAICat - Newton Implementation</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+<link href="oaicat.css" rel="stylesheet" type="text/css">
+</head>
+<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+<table width="100%" border="0" cellspacing="20" cellpadding="0">
+<tr valign="top">
+<td bgcolor="#669933">
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr valign="top">
+<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+</tr>
+</table>
+</td>
+</tr>
+<tr valign="top">
+<td>
+<h2>Newton Implementation</h2>
+<p>A Newton implementation is included with the OAICat package. Newton is a proprietary database bundled with OCLC's <a href="http://www.sitesearch.oclc.org/">SiteSearch</a> product. Contact me for details.</p>
+</td>
+</tr>
+<tr valign="top">
+<td bgcolor="#669933" height="1"></td>
+</tr>
+<tr valign="top">
+<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+</tr>
+<tr valign="top">
+<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+<!-- hhmts start -->
+Last modified: Mon Apr 21 15:38:11 EDT 2003
+<!-- hhmts end -->
+</span></td>
+</tr>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/docs/oaicat_icon.gif b/docs/oaicat_icon.gif
new file mode 100644
index 0000000..7ceef39
Binary files /dev/null and b/docs/oaicat_icon.gif differ
diff --git a/docs/properties.html b/docs/properties.html
new file mode 100644
index 0000000..08ed6e1
--- /dev/null
+++ b/docs/properties.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<title>OAICat - An OAI-PMH v2 Repository Framework</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+<link href="oaicat.css" rel="stylesheet" type="text/css">
+</head>
+<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+<table width="100%" border="0" cellspacing="20" cellpadding="0">
+<tr valign="top">
+<td bgcolor="#669933">
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr valign="top">
+<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+</tr>
+</table>
+</td>
+</tr>
+<tr valign="top">
+<td>
+	  <h2>Description of Entries in oaicat.properties</h2>
+          <ul>
+	    <li><b>OAIHandler.extraXmlns=[xmlns attributes] (Optional)</b>: This string gets inserted into the <oai:OAI-PMH> root element in ListRecords and GetRecord responses. It was added to make large-scale harvesting of DSpace METS records more efficient.</li>
+            <li><b>OAIHandler.serviceUnavailable=[true|false] (Optional)</b>: Forces the server to return a SC_SERVICE_UNAVAILABLE code to inform the user that it is currently being worked on.</li>
+            <li><b>OAIHandler.styleSheet=[path-to-xsl-stylesheet] (Optional)</b>: If present, a reference to this XSLT stylesheet will be included in the OAI responses.</li>
+            <li><b>OAIHandler.renderForOldBrowsers=[true|false] (Optional)</b>: If present and true, the XSLT stylesheet will be applied to the OAI response on the the server before sending back the response, if the client is an old browser. This is a good idea if you've defined OAIHandler.styleSheet because some old browsers don't do a very good job of rendering XSLT on the client side.</li>
+            <li><b>OAIHandler.forceRender=[true|false] (Optional)</b>: If present and true, the XSLT stylesheet will be applied to the OAI response on the the server before sending back the response. This is useful if you want to use OAICat as a web application framework, but want to disable OAI harvesting because the data is proprietary (e.g. <a href="http://alcme.oclc.org/gsafd/">The GSAFD Thesaurus</a>).</li>
+            <li><b>OAIHandler.baseURL=[OAI baseURL] (Optional)</b>: Force OAI responses to include the specified baseURL instead of getting it from the HttpServletRequest. This may be necessary if your firewall/router/port-mapper is messing with the request in some way.</li>
+          </ul>
+          <ul>
+            <li><b>AbstractCatalog.oaiCatalogClassName=[package.classname] (Required)</b>: The fully qualified Java class name that implements ORG.oclc.oai.server.catalog.AbstractCatalog.</li>
+            <li><b>AbstractCatalog.recordFactoryClassName=[package.classname] (Required)</b>: The fully qualified Java class name that implements ORG.oclc.oai.server.catalog.RecordFactory.</li>
+            <li><b>AbstractCatalog.secondsToLive=[# of seconds] (Optional)</b>: The number of seconds a resumptionToken is retained for reuse. The default is -1, which means that resumptionTokens are stateless and can be resent anytime.</li>
+            <li><b>AbstractCatalog.harvestable=[true|false] (Optional)</b>: If present and false, the ListRecords and ListIdentifiers verbs will send back an error condition. This might be useful if you want to provide access to the other verbs but want to discourage general harvesting.</li>
+            <li><b>AbstractCatalog.granularity=[YYYY-MM-DD|YYYY-MM-DDThh:mm:ssZ] (Required)</b>: The supported level of datetime granularity for from/until parameters.</li>
+          </ul>
+          <ul>
+            <li><b>Identify.repositoryName=[text] (Required)</b>: Text to use as the Identify verb's repositoryName value.</li>
+            <li><b>Identify.adminEmail[mailto:abc@xyz.org] (Required)</b>: Text to use as the Identify verb's adminEmail value.</li>
+            <li><b>Identify.earliestDatestamp=[datetime stamp of earliest] (Required)</b>: Text to use as the Identify verb's earliestDatestamp value.</li>
+            <li><b>Identify.deletedRecord=[no|transient|persistent] (Required)</b>: Text to use as the Identify verb's deletedRecord value.</li>
+            <li><b>Identify.description.[0-9]*=[xml fragment] (Optional; Repeatable)</b>: XML fragments (&lt;description&gt;) to include in the Identify verb's response.</li>
+          </ul>
+          <ul>
+            <li><b>Crosswalks.[metadataPrefix]=[package.classname] (Required; Repeatable)</b>: Java class(es) that implement the ORG.oclc.oai.server.crosswalk.Crosswalk class.</li>
+          </ul>
+          <ul>
+            <li><b>ExtensionVerbs.[verb name]=[package.classname] (Optional; Repeatable)</b>: Java class(es) that extend the ORG.oclc.oai.server.verb.ServerVerb class. This allow you to easily create new verbs to perform application functions beyond the set of OAI verbs. These verbs can be accessed by adding &quot;/extension&quot; to the baseURL.</li>
+          </ul>
+          <ul>
+            <li>Other properties that support various combinations of classes mentioned above.</li>
+          </ul>
+</td>
+</tr>
+<tr valign="top">
+<td bgcolor="#669933" height="1"></td>
+</tr>
+<tr valign="top">
+<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+</tr>
+<tr valign="top">
+<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+<!-- hhmts start -->
+Last modified: Fri Jul 29 10:03:16 Eastern Daylight Time 2005
+<!-- hhmts end -->
+</span></td>
+</tr>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/docs/sru.html b/docs/sru.html
new file mode 100644
index 0000000..57a01ca
--- /dev/null
+++ b/docs/sru.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+  <head>
+    <title>OAICat - SRW/U Implementation</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+    <link href="oaicat.css" rel="stylesheet" type="text/css">
+  </head>
+  <body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+    <table width="100%" border="0" cellspacing="20" cellpadding="0">
+      <tr valign="top">
+	<td bgcolor="#669933">
+	  <table width="100%" border="0" cellspacing="0" cellpadding="0">
+	    <tr valign="top">
+	      <td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+	      <td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+	    </tr>
+	  </table>
+	</td>
+      </tr>
+      <tr valign="top">
+	<td>
+	  <h2>SRW/U Implementation</h2>
+	  <p>An SRW/U implementation is included with the OAICat package. Any SRW/U database can be configured for OAI access if it conforms to the following (prototype) profile:
+	  </p>
+	  <ul>
+	    <li>An oai.identifier index</li>
+	    <li>An oai.datestamp index</li>
+	    <li>An oai.set scan index (not implemented yet)</li>
+	    <li>An OaiHeader recordSchema</li>
+	  </ul>
+	  <p>Caveat: the details of this profile are still being debated and could change.</p>
+	  <p>To configure OAICat to use the SRW/U implementation, simply rename the sru.properties file to oaicat.properties and bounce the server. If this demo works, edit the oaicat.properties file to change the SRWOAICatalog.sruURL to the target SRW/U service URL and bounce the server again.</p>
+	</td>
+      </tr>
+      <tr valign="top">
+	<td bgcolor="#669933" height="1"></td>
+      </tr>
+      <tr valign="top">
+	<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+      </tr>
+      <tr valign="top">
+	<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+	    <!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+	    <!-- hhmts start -->
+	    Last modified: Tue Oct 14 10:28:18 EDT 2003
+	    <!-- hhmts end -->
+	  </span></td>
+      </tr>
+    </table>
+  </body>
+</html>
\ No newline at end of file
diff --git a/docs/version b/docs/version
new file mode 100644
index 0000000..eb94b02
--- /dev/null
+++ b/docs/version
@@ -0,0 +1,321 @@
+OAICat version file. This file records major changes to the OAICat software.
+
+2013-10-18 1.5.62
+Bug fix:
+- Added a AbstractCatalog.paramRegex property to address an SQL injection vulnerability that was reported by Ward van Wanrooij. [https://code.google.com/p/oaicat/issues/detail?id=7]
+
+2013-04-18 1.5.61
+Enhancement:
+- change mechanism for locating the oaicat.properties configuration file.
+
+2011-04-22 1.5.60
+Bug Fix:
+- Add missing closure to 'if' condition when handling "global" attributes.
+
+2011-04-22 1.5.59
+Bug Fix:
+- Close java.sql.Statements after use in ExtendedJDBCOAICatalog.
+
+2011-03-31 1.5.58
+Bug Fix:
+- Deprecate old JDBC AbstractCatalog implementations in favor of ExtendedJDBCOAICatalog.
+Enhancement:
+- Cleaned up various build warnings.
+
+2009-03-26 1.5.57
+Bug Fix:
+- Fix persistentConnection bug in NewJDBCOAICatalog
+
+2008-10-27 1.5.56
+Bug Fix:
+- Change toolkit.xsd reference to the alcme.oclc.org domain since the VT version vanished
+
+2007-11-01 1.5.55
+Enhancement:
+- Make OAIHandler.getAttributes methods public
+
+2007-05-22 1.5.54
+Enhancement:
+- Switch license banners to Apache 2.0
+
+2007-01-25 1.5.53
+Enhancement:
+- Add support for java.sql.Date datestamps in the JDBC implementation.
+
+2007-01-25 1.5.52
+Enhancement:
+- add Getty implementation to support museums and archives.
+
+2006-12-20 1.5.51
+Enhancement:
+- add OAIHandler.missingVerbClassName parameter support
+
+2006-12-20 1.5.50
+Enhancement:
+- add OAIHandler.isServiceUnavailable so developers can decide how this gets
+done themselves.
+
+2006-11-06 1.5.49
+Enhancement:
+- add filterRequest() to OAIHandler to allow overriding classes to control
+which requests get through.
+
+2006-10-23 1.5.48
+Enhancement:
+- add a getDescriptions() method to the AbstractCatalog and Identify verb
+
+2006-09-06 1.5.47
+Bug Fix:
+- XML-encode attribute values
+- fixed a synchronization problem in XSLTCrosswalk
+
+2006-06-30 1.5.46
+Enhancement:
+- add support for multiple databases
+
+2006-06-22 1.5.45
+Cleanup:
+- Removed some issues that were cluttering my Eclipse "Problems" view.
+It was mainly things like "throws" that were declared but never used and
+private methods and variables that were never used. I'm fairly confident
+that their removal won't affect anyone's custom classes.
+
+2006-04-06 1.5.44
+Bug Fix:
+- Restore JDBCOAICatalog, JDBCLimitedOAICatalog, and ExtendedJDBCOAICatalog in favor of NewJDBCOAICatalog
+
+2006-04-04 1.5.43
+Bug Fix:
+- Add close statements to Statements and ResultSets in JDBCOAICatalog, JDBCLimitedOAICatalog, and ExtendedJDBCOAICatalog to plug a memory leak
+
+2006-02-06 1.5.42
+Enhancement:
+- Add support for repeatable fields to JDBC2oai_dc.java
+
+2005-12-08 1.5.41
+Bug Fix:
+- Add support for multiple metadataPrefixes that share the same schemaLocation
+
+2005-09-26 1.5.40
+Enhancement:
+- Allow configuration files to reside on the classpath as an alternative to
+hardcoding them relative to $TOMCAT_HOME
+
+2005-09-08 1.5.39
+Bug Fix:
+- Exit the loop in RecordHandler so no more than one metadata record gets created.
+
+2005-07-29 1.5.38
+Enhancement:
+- Added optional OAIHandler.extraXmlns parameter to oaicat.properties handler. This string will be inserted into the <oai:OAI-PMH> to allow extra xmlns declarations to be included in the response.
+
+2005-07-21 1.5.37
+Bug Fix:
+- Omit XML declaration from FileMap2oai_dc identity transformer
+
+2005-06-07 1.5.36
+Enhancement:
+- Added a feature contributed by Peter Guldbaek to support an alternative method of loading the Properties object.
+
+2005-03-25 1.5.35
+Enhancement:
+- Make the xsl stylesheet optional on FileMap2.oai_dc to make it easier to store oai_dc records instead of oai_etdms.
+
+2005-02-28 1.5.34
+Bug Fix:
+- Force identifier parameters to conform to URI syntax or else throw a badArgument error.
+
+2005-02-04 1.5.33
+Enhancement:
+- Add an optional String parameter to the Crosswalk constructor to tell it which metadataPrefix it is being configured for.
+
+2005-01-31 1.5.32
+Bug Fix:
+- Don't assume the objects in JDBCRecordFactory are strings.
+
+2004-10-11 1.5.31
+Bug Fix:
+- Fix BadArgumentException for missing metadataPrefix in ListRecords and ListIdentifiers
+- Change various RecordFactory implementations to use a public fromOAIIdentifier method rather than protected.
+
+2004-10-11 1.5.30
+Bug Fix:
+- Remove Java 1.4 String.split() dependency.
+
+2004-10-06 1.5.29
+Bug Fix:
+- Fix JDBC2oai_dc crosswalk to exclude empty fields from the result.
+
+2004-09-15 1.5.28
+Enhancement:
+- Add dateFormat property support for JDBCOAICatalog and JDBCLimitedOAICatalog
+
+2004-08-31 1.5.27
+Enhancement:
+- Add SRUOAICatalog support
+
+2004-06-10 1.5.26
+Enhancement:
+- Add support for dynamic crosswalks.
+
+2004-06-07 1.5.25
+Enhancement:
+- Add OAIHandler.urlEncodeSetSpec argument
+
+2004-05-21 1.5.24
+Enhancement:
+- Construct verbs using a HashMap rather than a ServletContext so they can be called in a more generic way.
+- Add a few set method to AbstractCatalog so implementations can be created directly rather than being forced to use the factory method.
+
+2004-05-10 1.5.23
+Bug Fix:
+- Changed ExtendedJDBCOAICatalog and JDBCOAICatalog to use '!' instead of ':' as the resumptionToken separator to accommodate long datestamps.
+Enhancement:
+- Make OAIHandler getWriter and OAIHandler.getResult public.
+
+2004-05-05 1.5.22
+Bug Fix:
+- Changed ExtendedJDBCRecordFactory.getLocalIdentifier to convert value to String rather than assume it is a string.
+
+2004-04-06 1.5.21
+Enhancement:
+- Add Copymets crosswalk.
+
+2004-03-15 1.5.20
+Bug Fix:
+- Fix incorrect setName in ListSets for NewFileSystemOAIHandler.
+
+2004-03-08 1.5.19
+Bug Fix:
+- Fix ListMetadataFormats(identifier) for XMLFileOAICatalog
+
+2004-02-18 1.5.18
+Bug Fix:
+- Add Opera to the list of User-Agents that have XSL stylesheets applied on the browser.
+
+2003-12-22 1.5.17
+Enhancement:
+- Add OAIHandler.xmlEncodeSetSpec property option
+
+2003-11-05 1.5.16
+Bug Fix:
+- Added synchronized to javax.xml.transform.Transformer invocations.
+
+2003-10-22 1.5.15
+Enhancement:
+- Added oaicatnewton.jar to the distribution.
+
+2003-10-14 1.5.14
+Enhancement:
+- Added "info:ofi/pro" to the list of namespaces that can be parsed by XMLFileOAICatalog.
+- Added XSLTproCrosswalk
+- Changed the Redirect extension verb to return the oai_dc record if no dc:identifier is found.
+
+2003-06-13 1.5.13
+Bug Fix:
+- Allow for missing User-Agent in HTTP header.
+
+2003-06-05 1.5.12
+Bug Fix:
+- XML Encode the <identifier> portion of the response.
+
+2003-06-03 1.5.11
+Bug Fix:
+- Fixed a few more bugs in JDBCOAICatalog where OAI-type Exceptions were
+  being caught and transformed into Error 500 failures.
+
+2003-05-14 1.5.10
+Bug Fix:
+- Fixed a bug in JDBCOAICatalog where invalid identifiers weren't being reported properly from GetRecord verbs.
+
+2003-05-05 1.5.10
+Enhancement:
+- Add call to ServerVerb.init for extension verbs to allow them to initialize themselves.
+- Add call to HttpServletRequest.setCharacterEncoding("UTF-8") to handle escaped characters in the URL.
+
+2003-04-29 1.5.9
+Bug Fix:
+- Moved extension verbs so they only work from baseURL + "/extension". This should satisfy those who worry about adding non-OAI verbs to the baseURL. Also changed the property from ServerVerbs.* to ExtensionVerbs.*
+- Fixed ListRecords and ListIdentifiers to convert blanks back to '+'. Hopefully, this won't cause people too much grief.
+
+2003-04-21 1.5.8
+Enhancements:
+- Added an optional Redirect verb to support POIs (http://www.ukoln.ac.uk/distributed-systems/poi/resolver-guidelines/.
+- Added JUnit tests to the Ant build scripts.
+- Added a little more documentation
+- Improved version control.
+- Fixed a bug where setting the HTTP Content-Encoding prevented the return of HTTP status codes.
+
+2003-04-17 1.5.7
+Bg fixes:
+- Removed URLEncoder.encode() calls in RecordFactory and ServerVerb because URLEncoding setSpecs isn't the right solution. I don't recall why I did this in the first place, and now I worry that it will break someone's existing installation because they are gone. If so, hopefully they will contact me and we can fix it propertly.
+Enhancements
+- Add an OAIHarvester.forceRender property to discourage the harvesting of proprietary data.
+- Add a RecordFactory.encodeSetSpec parameter (default=true) since it's not really supposed to encode colons. Eventually, this will need to be fixed properly to encode only those characters that actually require it.
+
+2003-03-28 1.5.6
+Enhancements
+- Allow for additional verbs to be configured in oaicat.properties.
+- Added an optional GetMetadata verb.
+
+2003-03-06 1.5.5
+Enhancements
+- Added an optional OAIHandler.renderForOldBrowsers property to recognize old "Mozilla" browsers and render the response into HTML if an OAIHandler.styleSheet property is present.
+
+2002-11-18 1.5.4
+Bug fixes:
+- Removed 'compress' compression option since there is some question about its correctness.
+- URLEncoded the request element's set value.
+- Fixed a bug in the JDBC implementation that caused resumptionTokens to always respond with initial results.
+Enhancements
+- Added a FileSystem implementation and made it the default instead of the silly useless DummyOAICatalog.
+- Moved the toolkit element to the bottom of the Identify response to make it less prominent.
+
+2002-11-01 1.5.3
+Bug fixes:
+- Made JDBC2oai_dc real.
+- Change setSpec handling to allow them to be collected from the database itself.
+- Use a HashMap to represent a record rather than depending on the cursor of an entire resultSet.
+- Changes too numerous to mention regarding JDBCOAICatalog. If you're using it, consider using JDBCLimitedOAICatalog instead. It should be more memory effecient.
+- Added a new class named NoRecordMatchException which is a more appropriate name than NoItemsMatchException. Both work, though.
+- Fixed a bug in the Identify response where the toolkit description wasn't passing validation.
+- Changed the default 'from' and 'until' dates to something more realistic.
+
+
+2002-10-22 1.5.2
+Bug Fixes:
+- Fixed a bug in the JDBC implementation that caused resumptionToken type
+  requests to return the initial set of records each time.
+- Made resumptionTokens in the JDBC implementation idempotent.
+- Changed the default 'from' date from 01/01/0000 to 01/01/0001 to
+  accommodate Oracle JDBC limitations.
+- Fixed a problem with ListMetadataFormats in the JDBC implementation where it didn't recognize oaiidentifiers that weren't in sets.
+
+2002-10-16 1.5.1
+Enhancements:
+- Added a JDBC implementation
+
+2002-10-04 1.5
+Enhancements:
+- Added toolkit description to the Identify response and version number tracking.
+
+2002-10-02 1.4
+Bug Fixes:
+- Added a check for bad arguments when they accompany resumptionTokens
+  to satisfy the OAI repository registration validator.
+
+2002-09-27 1.3
+Enhancements:
+- Added cusomization instructions to the Readme.txt file.
+- Improved javadocs.
+
+2002-09-25 1.2
+Bug Fixes:
+- Changed the way <description> elements are managed by the Identify verb.
+
+2002-09-20 1.1
+Bug Fixes:
+- Added checks for BadArgumentExceptions to satisfy Hussein's Repository
+  Explorer validation.
+
+Enhancements:
diff --git a/docs/xmlfile.html b/docs/xmlfile.html
new file mode 100644
index 0000000..8458141
--- /dev/null
+++ b/docs/xmlfile.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<title>OAICat - Single XML File Implementation</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+<link href="oaicat.css" rel="stylesheet" type="text/css">
+</head>
+<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
+<table width="100%" border="0" cellspacing="20" cellpadding="0">
+<tr valign="top">
+<td bgcolor="#669933">
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr valign="top">
+<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
+<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
+</tr>
+</table>
+</td>
+</tr>
+<tr valign="top">
+<td>
+<h2>Single XML File Implementation</h2>
+<p>A single XML file implementation is included with the OAICat package. The purpose is to provide OAI access to metadata records stored in a single XML file.</p>
+<p>Contact <a href="mailto:jyoung@oclc.org">Jeff Young</a> for details about setting this up.</p>
+</td>
+</tr>
+<tr valign="top">
+<td bgcolor="#669933" height="1"></td>
+</tr>
+<tr valign="top">
+<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
+</tr>
+<tr valign="top">
+<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
+<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
+<!-- hhmts start -->
+Last modified: Mon Apr 21 15:38:05 EDT 2003
+<!-- hhmts end -->
+</span></td>
+</tr>
+</table>
+</body>
+</html>
\ No newline at end of file
diff --git a/lib/Dbutils.jar b/lib/Dbutils.jar
new file mode 100644
index 0000000..5c6e0b5
Binary files /dev/null and b/lib/Dbutils.jar differ
diff --git a/lib/Pears.jar b/lib/Pears.jar
new file mode 100644
index 0000000..9c334e2
Binary files /dev/null and b/lib/Pears.jar differ
diff --git a/lib/SRW.jar b/lib/SRW.jar
new file mode 100644
index 0000000..806ac9b
Binary files /dev/null and b/lib/SRW.jar differ
diff --git a/lib/SRWPears.jar b/lib/SRWPears.jar
new file mode 100644
index 0000000..4dc8901
Binary files /dev/null and b/lib/SRWPears.jar differ
diff --git a/lib/commons-logging.jar b/lib/commons-logging.jar
new file mode 100644
index 0000000..b99c937
Binary files /dev/null and b/lib/commons-logging.jar differ
diff --git a/lib/servlet-api.jar b/lib/servlet-api.jar
new file mode 100644
index 0000000..dd326d3
Binary files /dev/null and b/lib/servlet-api.jar differ
diff --git a/lib/xalan.jar b/lib/xalan.jar
new file mode 100644
index 0000000..73cf175
Binary files /dev/null and b/lib/xalan.jar differ
diff --git a/lib/xercesImpl.jar b/lib/xercesImpl.jar
new file mode 100644
index 0000000..14c3162
Binary files /dev/null and b/lib/xercesImpl.jar differ
diff --git a/lib/xml-apis.jar b/lib/xml-apis.jar
new file mode 100644
index 0000000..2dd8377
Binary files /dev/null and b/lib/xml-apis.jar differ
diff --git a/oaicat.war b/oaicat.war
new file mode 100644
index 0000000..8dcdfb8
Binary files /dev/null and b/oaicat.war differ
diff --git a/src/ORG/oclc/oai/server/OAIHandler.java b/src/ORG/oclc/oai/server/OAIHandler.java
new file mode 100644
index 0000000..2db5019
--- /dev/null
+++ b/src/ORG/oclc/oai/server/OAIHandler.java
@@ -0,0 +1,480 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.SocketException;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Properties;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.verb.BadVerb;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.server.verb.ServerVerb;
+
+/**
+ * OAIHandler is the primary Servlet for OAICat.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class OAIHandler extends HttpServlet {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+    
+    public static final String PROPERTIES_SERVLET_CONTEXT_ATTRIBUTE = OAIHandler.class.getName() + ".properties";
+    
+    private static final String VERSION = "1.5.62";
+    private static boolean debug = false;
+
+//    private Transformer transformer = null;
+//    private boolean serviceUnavailable = false;
+//    private boolean monitor = false;
+//    private boolean forceRender = false;
+    protected HashMap attributesMap = new HashMap();
+//    private HashMap serverVerbs = null;
+//    private HashMap extensionVerbs = null;
+//    private String extensionPath = null;
+    
+//    private static Logger logger = Logger.getLogger(OAIHandler.class);
+//    static {
+//        BasicConfigurator.configure();
+//    }
+    
+    private Log log = LogFactory.getLog(OAIHandler.class);
+    
+    /**
+     * Get the VERSION number
+     */
+    public static String getVERSION() { return VERSION; }
+    
+    /**
+     * init is called one time when the Servlet is loaded. This is the
+     * place where one-time initialization is done. Specifically, we
+     * load the properties file for this application, and create the
+     * AbstractCatalog object for subsequent use.
+     *
+     * @param config servlet configuration information
+     * @exception ServletException there was a problem with initialization
+     */
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+        
+        try {
+            HashMap attributes = null;
+            ServletContext context = getServletContext();
+            Properties properties = (Properties) context.getAttribute(PROPERTIES_SERVLET_CONTEXT_ATTRIBUTE);
+            if (properties == null) {
+                final String PROPERTIES_INIT_PARAMETER = "properties";
+                log.debug("OAIHandler.init(..): No '" + PROPERTIES_SERVLET_CONTEXT_ATTRIBUTE + "' servlet context attribute. Trying to use init parameter '" + PROPERTIES_INIT_PARAMETER + "'");
+                
+                String fileName = config.getServletContext().getInitParameter(PROPERTIES_INIT_PARAMETER);
+                InputStream in;
+                try {
+                    log.debug("fileName=" + fileName);
+                    in = new FileInputStream(fileName);
+                } catch (FileNotFoundException e) {
+                    log.debug("file not found. Try the classpath: " + fileName);
+                    in = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);
+                }
+                if (in != null) {
+                    log.debug("file was found: Load the properties");
+                    properties = new Properties();
+                    properties.load(in);
+                    attributes = getAttributes(properties);
+                    if (debug) System.out.println("OAIHandler.init: fileName=" + fileName);
+                }
+            } else {
+                log.debug("Load context properties");
+                attributes = getAttributes(properties);
+            }
+
+            log.debug("Store global properties");
+            attributesMap.put("global", attributes);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+            throw new ServletException(e.getMessage());
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+            throw new ServletException(e.getMessage());
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+            throw new ServletException(e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new ServletException(e.getMessage());
+        } catch (Throwable e) {
+            e.printStackTrace();
+            throw new ServletException(e.getMessage());
+        }
+    }
+    
+    public HashMap getAttributes(Properties properties)
+    throws Throwable {
+        HashMap attributes = new HashMap();
+        Enumeration attrNames = getServletContext().getAttributeNames();
+        while (attrNames.hasMoreElements()) {
+            String attrName = (String)attrNames.nextElement();
+            attributes.put(attrName, getServletContext().getAttribute(attrName));
+        }
+        attributes.put("OAIHandler.properties", properties);
+//        String temp = properties.getProperty("OAIHandler.debug");
+//        if ("true".equals(temp)) debug = true;
+        String missingVerbClassName = properties.getProperty("OAIHandler.missingVerbClassName", "ORG.oclc.oai.server.verb.BadVerb");
+        Class missingVerbClass = Class.forName(missingVerbClassName);
+        attributes.put("OAIHandler.missingVerbClass", missingVerbClass);
+        if (!"true".equals(properties.getProperty("OAIHandler.serviceUnavailable"))) {
+            attributes.put("OAIHandler.version", VERSION);
+            AbstractCatalog abstractCatalog = AbstractCatalog.factory(properties, getServletContext());
+            attributes.put("OAIHandler.catalog", abstractCatalog);
+        }
+        boolean forceRender = false;
+        if ("true".equals(properties.getProperty("OAIHandler.forceRender"))) {
+            forceRender = true;
+        }
+        String xsltName = properties.getProperty("OAIHandler.styleSheet");
+        String appBase = properties.getProperty("OAIHandler.appBase");
+        if (appBase == null) appBase = "webapps";
+        if (xsltName != null
+                && ("true".equalsIgnoreCase(properties.getProperty("OAIHandler.renderForOldBrowsers"))
+                        || forceRender)) {
+            InputStream is;
+            try {
+                is = new FileInputStream(appBase + "/" + xsltName);
+            } catch (FileNotFoundException e) {
+                // This is a silly way to skip the context name in the xsltName
+                is = new FileInputStream(getServletContext().getRealPath(xsltName.substring(xsltName.indexOf("/", 1)+1)));
+            }
+            StreamSource xslSource = new StreamSource(is);
+            TransformerFactory tFactory = TransformerFactory.newInstance();
+            Transformer transformer = tFactory.newTransformer(xslSource);
+            attributes.put("OAIHandler.transformer", transformer);
+        }
+        return attributes;
+    }
+    
+    public HashMap getAttributes(String pathInfo) {
+        HashMap attributes = null;
+        log.debug("pathInfo=" + pathInfo);
+        if (pathInfo != null && pathInfo.length() > 0) {
+            if (attributesMap.containsKey(pathInfo)) {
+                log.debug("attributesMap containsKey");
+                attributes = (HashMap) attributesMap.get(pathInfo);
+            } else {
+                log.debug("!attributesMap containsKey");
+                try {
+                    String fileName = pathInfo.substring(1) + ".properties";
+                    log.debug("attempting load of " + fileName);
+                    InputStream in = Thread.currentThread()
+                    .getContextClassLoader()
+                    .getResourceAsStream(fileName);
+                    if (in != null) {
+                        log.debug("file found");
+                        Properties properties = new Properties();
+                        properties.load(in);
+                        attributes = getAttributes(properties);
+                    } else {
+                        log.debug("file not found");
+                    }
+                    attributesMap.put(pathInfo, attributes);
+                } catch (Throwable e) {
+                    log.debug("Couldn't load file", e);
+                    // do nothing
+                }
+            }
+        }
+        if (attributes == null) {
+            log.debug("use global attributes");
+            attributes = (HashMap) attributesMap.get("global");
+        }
+        return attributes;
+    }
+
+    /**
+     * Peform the http GET action. Note that POST is shunted to here as well.
+     * The verb widget is taken from the request and used to invoke an
+     * OAIVerb object of the corresponding kind to do the actual work of the verb.
+     *
+     * @param request the servlet's request information
+     * @param response the servlet's response information
+     * @exception IOException an I/O error occurred
+     */
+    public void doGet(HttpServletRequest request,
+            HttpServletResponse response)
+    throws IOException {
+        HashMap attributes = getAttributes(request.getPathInfo());
+        if (!filterRequest(request, response)) {
+            return;
+        }
+        log.debug("attributes=" + attributes);
+        Properties properties =
+            (Properties) attributes.get("OAIHandler.properties");
+        boolean monitor = false;
+        if (properties.getProperty("OAIHandler.monitor") != null) {
+            monitor = true;
+        }
+        boolean serviceUnavailable = isServiceUnavailable(properties);
+        String extensionPath = properties.getProperty("OAIHandler.extensionPath", "/extension");
+        
+        HashMap serverVerbs = ServerVerb.getVerbs(properties);
+        HashMap extensionVerbs = ServerVerb.getExtensionVerbs(properties);
+        
+        Transformer transformer =
+            (Transformer) attributes.get("OAIHandler.transformer");
+        
+        boolean forceRender = false;
+        if ("true".equals(properties.getProperty("OAIHandler.forceRender"))) {
+            forceRender = true;
+        }
+        
+//      try {
+        request.setCharacterEncoding("UTF-8");
+//      } catch (UnsupportedEncodingException e) {
+//      e.printStackTrace();
+//      throw new IOException(e.getMessage());
+//      }
+        Date then = null;
+        if (monitor) then = new Date();
+        if (debug) {
+            Enumeration headerNames = request.getHeaderNames();
+            System.out.println("OAIHandler.doGet: ");
+            while (headerNames.hasMoreElements()) {
+                String headerName = (String)headerNames.nextElement();
+                System.out.print(headerName);
+                System.out.print(": ");
+                System.out.println(request.getHeader(headerName));
+            }
+        }
+        if (serviceUnavailable) {
+            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
+            "Sorry. This server is down for maintenance");
+        } else {
+            try {
+                String userAgent = request.getHeader("User-Agent");
+                if (userAgent == null) {
+                    userAgent = "";
+                } else {
+                    userAgent = userAgent.toLowerCase();
+                }
+                Transformer serverTransformer = null;
+                if (transformer != null) {
+                    
+                    // return HTML if the client is an old browser
+                    if (forceRender
+                            || userAgent.indexOf("opera") != -1
+                            || (userAgent.startsWith("mozilla")
+                                    && userAgent.indexOf("msie 6") == -1
+                            /* && userAgent.indexOf("netscape/7") == -1 */)) {
+                        serverTransformer = transformer;
+                    }
+                }
+                String result = getResult(attributes, request, response, serverTransformer, serverVerbs, extensionVerbs, extensionPath);
+//              log.debug("result=" + result);
+                
+//              if (serverTransformer) { // render on the server
+//              response.setContentType("text/html; charset=UTF-8");
+//              StringReader stringReader = new StringReader(getResult(request));
+//              StreamSource streamSource = new StreamSource(stringReader);
+//              StringWriter stringWriter = new StringWriter();
+//              transformer.transform(streamSource, new StreamResult(stringWriter));
+//              result = stringWriter.toString();
+//              } else { // render on the client
+//              response.setContentType("text/xml; charset=UTF-8");
+//              result = getResult(request);
+//              }
+                
+                Writer out = getWriter(request, response);
+                out.write(result);
+                out.close();
+            } catch (FileNotFoundException e) {
+                if (debug) {
+                    e.printStackTrace();
+                    System.out.println("SC_NOT_FOUND: " + e.getMessage());
+                }
+                response.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
+            } catch (TransformerException e) {
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+            } catch (OAIInternalServerError e) {
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+            } catch (SocketException e) {
+                System.out.println(e.getMessage());
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+            } catch (Throwable e) {
+                e.printStackTrace();
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+            }
+        }
+        if (monitor) {
+            StringBuffer reqUri = new StringBuffer(request.getRequestURI().toString());
+            String queryString = request.getQueryString();   // d=789
+            if (queryString != null) {
+                reqUri.append("?").append(queryString);
+            }
+            Runtime rt = Runtime.getRuntime();
+            System.out.println(rt.freeMemory() + "/" + rt.totalMemory() + " "
+                    + ((new Date()).getTime()-then.getTime()) + "ms: "
+                    + reqUri.toString());
+        }
+    }
+    
+    /**
+     * Should the server report itself down for maintenance? Override this
+     * method if you want to do this check another way.
+     * @param properties
+     * @return true=service is unavailable, false=service is available
+     */
+    protected boolean isServiceUnavailable(Properties properties) {
+        if (properties.getProperty("OAIHandler.serviceUnavailable") != null) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Override to do any prequalification; return false if
+     * the response should be returned immediately, without
+     * further action.
+     * 
+     * @param request
+     * @param response
+     * @return false=return immediately, true=continue
+     */
+    protected boolean filterRequest(HttpServletRequest request,
+            HttpServletResponse response) {
+        return true;
+    }
+
+    public static String getResult(HashMap attributes,
+            HttpServletRequest request,
+            HttpServletResponse response,
+            Transformer serverTransformer,
+            HashMap serverVerbs,
+            HashMap extensionVerbs,
+            String extensionPath)
+    throws Throwable {
+        try {
+            boolean isExtensionVerb = extensionPath.equals(request.getPathInfo());
+            String verb = request.getParameter("verb");
+            if (debug) {
+                System.out.println("OAIHandler.g<etResult: verb=>" + verb + "<");
+            }
+            String result;
+            Class verbClass = null;
+            if (isExtensionVerb) {
+                verbClass = (Class)extensionVerbs.get(verb);
+            } else {
+                verbClass = (Class)serverVerbs.get(verb);
+            }
+            if (verbClass == null) {
+                verbClass = (Class) attributes.get("OAIHandler.missingVerbClass");
+            }
+            Method construct = verbClass.getMethod("construct",
+                    new Class[] {HashMap.class,
+                    HttpServletRequest.class,
+                    HttpServletResponse.class,
+                    Transformer.class});
+            try {
+                result = (String)construct.invoke(null,
+                        new Object[] {attributes,
+                        request,
+                        response,
+                        serverTransformer});
+            } catch (InvocationTargetException e) {
+                throw e.getTargetException();
+            }
+            if (debug) {
+                System.out.println(result);
+            }
+            return result;
+        } catch (NoSuchMethodException e) {
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (IllegalAccessException e) {
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Get a response Writer depending on acceptable encodings
+     * @param request the servlet's request information
+     * @param response the servlet's response information
+     * @exception IOException an I/O error occurred
+     */
+    public static Writer getWriter(HttpServletRequest request, HttpServletResponse response)
+    throws IOException {
+        Writer out;
+        String encodings = request.getHeader("Accept-Encoding");
+        if (debug) {
+            System.out.println("encodings=" + encodings);
+        }
+        if (encodings != null && encodings.indexOf("gzip") != -1) {
+//          System.out.println("using gzip encoding");
+//          log.debug("using gzip encoding");
+            response.setHeader("Content-Encoding", "gzip");
+            out = new OutputStreamWriter(new GZIPOutputStream(response.getOutputStream()),
+            "UTF-8");
+//          } else if (encodings != null && encodings.indexOf("compress") != -1) {
+//          //  	    System.out.println("using compress encoding");
+//          response.setHeader("Content-Encoding", "compress");
+//          ZipOutputStream zos = new ZipOutputStream(response.getOutputStream());
+//          zos.putNextEntry(new ZipEntry("dummy name"));
+//          out = new OutputStreamWriter(zos, "UTF-8");
+        } else if (encodings != null && encodings.indexOf("deflate") != -1) {
+//          System.out.println("using deflate encoding");
+//          log.debug("using deflate encoding");
+            response.setHeader("Content-Encoding", "deflate");
+            out = new OutputStreamWriter(new DeflaterOutputStream(response.getOutputStream()),
+            "UTF-8");
+        } else {
+//          log.debug("using no encoding");
+            out = response.getWriter();
+        }
+        return out;
+    }
+    
+    /**
+     * Peform a POST action. Actually this gets shunted to GET
+     *
+     * @param request the servlet's request information
+     * @param response the servlet's response information
+     * @exception IOException an I/O error occurred
+     */
+    public void doPost(HttpServletRequest request,
+            HttpServletResponse response)
+    throws IOException {
+        doGet(request, response);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/AbstractCatalog.java b/src/ORG/oclc/oai/server/catalog/AbstractCatalog.java
new file mode 100644
index 0000000..bb62f6c
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/AbstractCatalog.java
@@ -0,0 +1,660 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ORG.oclc.oai.server.catalog;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Vector;
+
+import javax.servlet.ServletContext;
+
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+import ORG.oclc.oai.server.verb.BadArgumentException;
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.server.verb.ServerVerb;
+
+/**
+ * AbstractCatalog is the generic interface between OAICat and any arbitrary
+ * database. Implement this interface to have OAICat work with your database.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public abstract class AbstractCatalog {
+    private static final boolean debug = false;
+    /**
+     * The RecordFactory that understands how to convert this database's
+     * native "item" to the various metadataFormats to be supported.
+     */
+    private RecordFactory recordFactory;
+
+    /**
+     * is this repository harvestable?
+     */
+    private boolean harvestable = true;
+
+    /**
+     * optional property to limit the life of resumptionTokens (<0 indicates no limit)
+     **/
+    private int millisecondsToLive = -1;
+
+    /**
+     * Index into VALID_GRANULARITIES and FROM_GRANULARITIES
+     */
+    private int supportedGranularityOffset = -1;
+
+    /**
+     * All possible valid granularities
+     */
+    private static final String[] VALID_GRANULARITIES = {
+        "YYYY-MM-DD",
+        "YYYY-MM-DDThh:mm:ssZ"
+    };
+
+    /**
+     * minimum valid 'from' granularities
+     */
+    private static final String[] FROM_GRANULARITIES = {
+        "0000-01-01",
+        "0000-01-01T00:00:00Z"
+    };
+
+    /**
+     * Default regex pattern for validating parameters (can be overridden using an AbstractCatalog.paramRegex property
+     */
+    private String paramRegex = "^[a-zA-Z0-9\\.\\/\\:\\_\\-]*$";
+
+    /**
+     * return a handle to the RecordFactory
+     * @return guess
+     */
+    public RecordFactory getRecordFactory() { return recordFactory; }
+
+    public void setHarvestable(boolean harvestable) {
+        this.harvestable = harvestable;
+    }
+
+    public void setParamRegex(String s) {
+    	this.paramRegex = s;
+    }
+
+    /**
+     * Is this repository harvestable?
+     * @return true if harvestable, false otherwise.
+     */
+    public boolean isHarvestable() { return harvestable; }
+
+    /**
+     * get the optional millisecondsToLive property (<0 indicates no limit)
+     **/
+    public int getMillisecondsToLive() { return millisecondsToLive; }
+
+    public void setRecordFactory(RecordFactory recordFactory) {
+        this.recordFactory = recordFactory;
+    }
+
+    public void setSupportedGranularityOffset(int i) {
+        supportedGranularityOffset = i;
+    }
+
+    /**
+     * Convert the requested 'from' parameter to the finest granularity supported
+     * by this repository.
+     * @exception BadArgumentException one or more of the arguments are bad.
+     */
+    public String toFinestFrom(String from)
+    throws BadArgumentException {
+        if (debug) {
+            System.out.println("AbstractCatalog.toFinestFrom: from=" + from);
+            System.out.println("                            target=" + VALID_GRANULARITIES[supportedGranularityOffset]);
+        }
+        if (from.length() > VALID_GRANULARITIES[supportedGranularityOffset].length()) {
+            throw new BadArgumentException();
+        }
+        if (from.length() != VALID_GRANULARITIES[supportedGranularityOffset].length()) {
+            StringBuffer sb = new StringBuffer(from);
+            if (sb.charAt(sb.length()-1) == 'Z')
+                sb.setLength(sb.length()-1);
+
+            sb.append(FROM_GRANULARITIES[supportedGranularityOffset].substring(sb.length()));
+            from = sb.toString();
+        }
+
+        if (!isValidGranularity(from)) {
+            throw new BadArgumentException();
+        }
+
+        return from;
+    }
+
+    /**
+     * Convert the requested 'until' paramter to the finest granularity supported
+     * by this repository
+     * @exception BadArgumentException one or more of the arguments are bad.
+     */
+    public String toFinestUntil(String until)
+    throws BadArgumentException {
+        if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
+            if (!isValidGranularity(until))
+                throw new BadArgumentException();
+            return until;
+        }
+        if (until.length() > VALID_GRANULARITIES[supportedGranularityOffset].length()) {
+            throw new BadArgumentException();
+        }
+
+        StringBuffer sb = new StringBuffer(until);
+        if (sb.charAt(sb.length()-1) == 'Z')
+            sb.setLength(sb.length()-1);
+
+        if (sb.length() < VALID_GRANULARITIES[0].length()) {
+            while (sb.length() < 4) sb.append("9");
+            switch (sb.length()) {
+            case 4: // YYYY
+                sb.append("-");
+            case 5: // YYYY-
+                sb.append("12");
+            case 7: // YYYY-MM
+                sb.append("-");
+            case 8: // YYYY-MM-
+                sb.append("31");
+                break;
+
+            case 6: // YYYY-M
+            case 9: // YYYY-MM-D
+                throw new BadArgumentException();
+            }
+        }
+
+        until = sb.toString();
+        if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
+            if (!isValidGranularity(until))
+                throw new BadArgumentException();
+            return until;
+        }
+
+        if (sb.length() < VALID_GRANULARITIES[1].length()) {
+            switch (sb.length()) {
+            case 10: // YYYY-MM-DD
+                sb.append("T");
+            case 11: // YYYY-MM-DDT
+                sb.append("23");
+            case 13: // YYYY-MM-DDThh
+                sb.append(":");
+            case 14: // YYYY-MM-DDThh:
+                sb.append("59");
+//              case 16: // YYYY-MM-DDThh:mm
+//              sb.append("Z");
+//              break;
+
+//              case 12: // YYYY-MM-DDTh
+//              case 15: // YYYY-MM-DDThh:m
+//              throw new BadGranularityException();
+//              }
+//              }
+
+//              until = sb.toString();
+//              if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
+//              if (!isValidGranularity(until))
+//              throw new BadGranularityException();
+//              return until;
+//              }
+
+//              if (sb.charAt(sb.length()-1) == 'Z')
+//              sb.setLength(sb.length()-1); // remove the trailing 'Z'
+
+//              if (sb.length() < VALID_GRANULARITIES[2].length()) {
+//              switch (sb.length()) {
+            case 16: // YYYY-MM-DDThh:mm
+                sb.append(":");
+            case 17: // YYYY-MM-DDThh:mm:
+                sb.append("59");
+            case 19: // YYYY-MM-DDThh:mm:ss
+                sb.append("Z");
+                break;
+
+            case 18: // YYYY-MM-DDThh:mm:s
+                throw new BadArgumentException();
+            }
+        }
+
+//      until = sb.toString();
+//      if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
+//      if (!isValidGranularity(until))
+//      throw new BadGranularityException();
+//      return until;
+//      }
+
+//      if (sb.charAt(sb.length()-1) == 'Z')
+//      sb.setLength(sb.length()-1); // remove the trailing 'Z'
+
+//      switch (sb.length()) {
+//      case 19: // YYYY-MM-DDThh:mm:ss
+//      sb.append(".");
+//      case 20: // YYYY-MM-DDThh:mm:ss.
+//      sb.append("0");
+//      case 21: // YYYY-MM-DDThh:mm:ss.s
+//      sb.append("Z");
+//      break;
+//      }
+
+        until = sb.toString();
+        if (!isValidGranularity(until))
+            throw new BadArgumentException();
+        return until;
+    }
+
+    /**
+     * Does the specified date conform to the supported granularity of this repository?
+     * @param date a UTC date
+     * @return true if date conforms to the supported granularity of this repository,
+     * false otherwise.
+     */
+    private boolean isValidGranularity(String date) {
+        if (date.length() > VALID_GRANULARITIES[supportedGranularityOffset].length())
+            return false;
+
+        if (date.length() < VALID_GRANULARITIES[0].length()
+                || !Character.isDigit(date.charAt(0)) // YYYY
+                || !Character.isDigit(date.charAt(1))
+                || !Character.isDigit(date.charAt(2))
+                || !Character.isDigit(date.charAt(3))
+                || date.charAt(4) != '-'
+                    || !Character.isDigit(date.charAt(5)) // MM
+                    || !Character.isDigit(date.charAt(6))
+                    || date.charAt(7) != '-'
+                        || !Character.isDigit(date.charAt(8)) // DD
+                        || !Character.isDigit(date.charAt(9))) {
+            return false;
+        }
+
+        if (date.length() > VALID_GRANULARITIES[0].length()) {
+            if (date.charAt(10) != 'T'
+                || date.charAt(date.length()-1) != 'Z'
+                    || !Character.isDigit(date.charAt(11)) // hh
+                    || !Character.isDigit(date.charAt(12))
+                    || date.charAt(13) != ':'
+                        || !Character.isDigit(date.charAt(14)) // mm
+                        || !Character.isDigit(date.charAt(15))
+//                      ) {
+//                      return false;
+//                      }
+//                      }
+
+//                      if (date.length() > VALID_GRANULARITIES[1].length()) {
+//                      if (
+                        || date.charAt(16) != ':'
+                            || !Character.isDigit(date.charAt(17)) // ss
+                            || !Character.isDigit(date.charAt(18))) {
+                return false;
+            }
+        }
+
+//      if (date.length() > VALID_GRANULARITIES[2].length()) {
+//      if (date.charAt(19) != '.'
+//      || !Character.isDigit(date.charAt(20))) { // s
+//      return false;
+//      }
+
+//      }
+        return true;
+    }
+
+    /**
+     * Retrieve the Crosswalks property
+     *
+     * @return the Crosswalks object containing a detailed list of oai
+     * formats supported by this application.
+     */
+    public Crosswalks getCrosswalks() { return recordFactory.getCrosswalks(); }
+
+    /**
+     * Retrieve the list of supported Sets. This should probably be initialized
+     * by the constructor from the properties object that is passed to it.
+     *
+     * @return a Map object containing <setSpec> values as the Map keys and
+     * <setName> values for the corresponding the Map values.
+     * @exception NoSetHierarchyException No sets are defined for this repository
+     * @exception OAIInternalServerError An error occurred
+     */
+    public abstract Map listSets() throws NoSetHierarchyException, OAIInternalServerError;
+
+    /**
+     * Retrieve the next cluster of supported sets.
+     * @return a Map object containing <setSpec> values as the Map keys and
+     * <setName> values for the corresponding the Map values.
+     * @exception BadResumptionTokenException The resumptionToken is bad.
+     * @exception OAIInternalServerError An error occurred
+     */
+    public abstract Map listSets(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError;
+
+    /**
+     * Factory method for creating an AbstractCatalog instance. The properties
+     * object must contain the following entries:
+     * <ul>
+     *   <li><b>AbstractCatalog.className</b> property which points to a class
+     *       that implements the AbstractCatalog interface. Note that this class
+     *       must have a constructor that accepts a properties object as a
+     *       parameter.</li>
+     *   <li><b>Crosswalks.&lt;supported formats&gt;</b> properties which
+     *       satisfy the constructor for the Crosswalks class</li>
+     * </ul>
+     *
+     * @param properties Properties object containing entries necessary to
+     * initialize the class
+     * to be created.
+     * @return on object instantiating the AbstractCatalog interface.
+     * @exception Throwable some sort of problem occurred.
+     */
+    public static AbstractCatalog factory(Properties properties,
+            ServletContext context)
+    throws Throwable {
+        AbstractCatalog oaiCatalog = null;
+        String oaiCatalogClassName =
+            properties.getProperty("AbstractCatalog.oaiCatalogClassName");
+        String recordFactoryClassName =
+            properties.getProperty("AbstractCatalog.recordFactoryClassName");
+        if (oaiCatalogClassName == null) {
+            throw new ClassNotFoundException(
+            "AbstractCatalog.oaiCatalogClassName is missing from properties file");
+        }
+        if (recordFactoryClassName == null) {
+            throw new ClassNotFoundException(
+            "AbstractCatalog.recordFactoryClassName is missing from properties file");
+        }
+        Class oaiCatalogClass = Class.forName(oaiCatalogClassName);
+        try {
+            Constructor oaiCatalogConstructor = null;
+            try {
+                oaiCatalogConstructor =
+                    oaiCatalogClass.getConstructor(new Class[] {Properties.class,
+                            ServletContext.class});
+                oaiCatalog =
+                    (AbstractCatalog)oaiCatalogConstructor.newInstance(new Object[]
+                                                                                  {properties, context});
+            } catch (NoSuchMethodException e) {
+                oaiCatalogConstructor =
+                    oaiCatalogClass.getConstructor(new Class[] {Properties.class});
+                oaiCatalog =
+                    (AbstractCatalog)oaiCatalogConstructor.newInstance(new Object[]
+                                                                                  {properties});
+            }
+            if (debug) {
+                System.out.println("AbstractCatalog.factory: recordFactoryClassName="
+                        + recordFactoryClassName);
+            }
+            Class recordFactoryClass = Class.forName(recordFactoryClassName);
+            Constructor recordFactoryConstructor =
+                recordFactoryClass.getConstructor(new Class[] {Properties.class});
+            oaiCatalog.recordFactory =
+                (RecordFactory)recordFactoryConstructor.newInstance(new Object[] {properties});
+            if (debug) {
+                System.out.println("AbstractCatalog.factory: recordFactory=" + oaiCatalog.recordFactory);
+            }
+            String harvestable = properties.getProperty("AbstractCatalog.harvestable");
+            if (harvestable != null && harvestable.equals("false")) {
+                oaiCatalog.harvestable = false;
+            }
+            String secondsToLive =
+                properties.getProperty("AbstractCatalog.secondsToLive");
+            if (secondsToLive != null) {
+                oaiCatalog.millisecondsToLive = Integer.parseInt(secondsToLive) * 1000;
+            }
+            String granularity = properties.getProperty("AbstractCatalog.granularity");
+            for (int i = 0; granularity != null && i < VALID_GRANULARITIES.length; ++i) {
+                if (granularity.equalsIgnoreCase(VALID_GRANULARITIES[i])) {
+                    oaiCatalog.supportedGranularityOffset = i;
+                    break;
+                }
+            }
+            if (oaiCatalog.supportedGranularityOffset == -1) {
+                oaiCatalog.supportedGranularityOffset = 0;
+                System.err.println("AbstractCatalog.factory: Invalid or missing AbstractCatalog.granularity property. Setting value to default: " +
+                        VALID_GRANULARITIES[oaiCatalog.supportedGranularityOffset]);
+            }
+        } catch (InvocationTargetException e) {
+            throw e.getTargetException();
+        }
+        return oaiCatalog;
+    }
+
+    /**
+     * Allow the database to return some Identify &lt;description&gt; elements
+     *
+     * @return an XML String fragment containing description elements
+     */
+    public String getDescriptions() {
+        return null;
+    }
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * identifier.
+     *
+     * @param identifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception IdDoesNotExistException The specified identifier doesn't exist.
+     * @exception NoMetadataFormatsException The identifier exists, but no metadataFormats are
+     * provided for it.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public abstract Vector getSchemaLocations(String identifier)
+    throws IdDoesNotExistException, NoMetadataFormatsException, OAIInternalServerError;
+
+    /**
+     * Retrieve a list of Identifiers that satisfy the criteria parameters
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "headers" Map object. The "headers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception BadArgumentException one or more of the arguments are bad.
+     * @exception CannotDisseminateFormatException the requested metadataPrefix isn't supported
+     * @exception NoItemsMatchException no items fit the criteria
+     * @exception NoSetHierarchyException sets aren't defined for this repository
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public abstract Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+    throws BadArgumentException, CannotDisseminateFormatException, NoItemsMatchException,
+    NoSetHierarchyException, OAIInternalServerError;
+
+    /**
+     * Retrieve the next set of Identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "headers" Map object. The "headers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception BadResumptionTokenException The resumptionToken is bad.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public abstract Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError;
+
+    /**
+     * Retrieve the specified metadata for the specified identifier
+     *
+     * @param identifier the OAI identifier
+     * @return the String containing the result record.
+     * @exception IdDoesNotExistException The specified identifier doesn't exist.
+     * @exception CannotDisseminateFormatException The identifier exists, but doesn't support
+     * the specified metadataPrefix.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public abstract String getRecord(String identifier, String metadataPrefix)
+    throws IdDoesNotExistException, CannotDisseminateFormatException, OAIInternalServerError;
+
+    /**
+     * Retrieve the specified metadata for the specified identifier
+     *
+     * @param identifier the OAI identifier
+     * @return the String containing the result record.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @throws CannotDisseminateFormatException
+     * @throws IdDoesNotExistException
+     * @throws IdDoesNotExistException
+     */
+    public String getMetadata(String identifier, String metadataPrefix)
+    throws OAIInternalServerError, IdDoesNotExistException,
+    IdDoesNotExistException, CannotDisseminateFormatException {
+        throw new OAIInternalServerError("You need to override AbstractCatalog.getMetadata()");
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception BadArgumentException one or more of the arguments are bad.
+     * @exception CannotDisseminateFormatException the requested metadataPrefix isn't supported
+     * @exception NoItemsMatchException no items fit the criteria
+     * @exception NoSetHierarchyException sets aren't defined for this repository
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listRecords(String from, String until, String set, String metadataPrefix)
+    throws BadArgumentException, CannotDisseminateFormatException, NoItemsMatchException,
+    NoSetHierarchyException, OAIInternalServerError {
+        if (debug) {
+            System.out.println("in AbstractCatalog.listRecords");
+        }
+        Map listIdentifiersMap = listIdentifiers(from, until, set, metadataPrefix);
+        String resumptionToken = (String)listIdentifiersMap.get("resumptionToken");
+        Iterator identifiers = (Iterator)listIdentifiersMap.get("identifiers");
+
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+
+        while (identifiers.hasNext()) {
+            String identifier = (String)identifiers.next();
+            try {
+                records.add(getRecord(identifier, metadataPrefix));
+            } catch (IdDoesNotExistException e) {
+                throw new OAIInternalServerError("GetRecord failed to retrieve identifier '"
+                        + identifier + "'");
+            }
+        }
+        listRecordsMap.put("records", records.iterator());
+        if (resumptionToken != null) {
+            listRecordsMap.put("resumptionToken", resumptionToken);
+        }
+        return listRecordsMap;
+    }
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception BadResumptionTokenException The resumptionToken is bad.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        Map listIdentifiersMap = listIdentifiers(resumptionToken);
+        resumptionToken = (String)listIdentifiersMap.get("resumptionToken");
+        Iterator identifiers = (Iterator)listIdentifiersMap.get("identifiers");
+        String metadataPrefix = (String)listIdentifiersMap.get("metadataPrefix");
+
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+
+        while (identifiers.hasNext()) {
+            String identifier = (String)identifiers.next();
+            try {
+                records.add(getRecord(identifier, metadataPrefix));
+            } catch (IdDoesNotExistException e) {
+                throw new OAIInternalServerError("GetRecord failed to retrieve identifier '"
+                        + identifier + "'");
+            } catch (CannotDisseminateFormatException e) {
+                // someone cheated
+                throw new BadResumptionTokenException();
+            }
+        }
+        listRecordsMap.put("records", records.iterator());
+        if (resumptionToken != null) {
+            listRecordsMap.put("resumptionToken", resumptionToken);
+        }
+        return listRecordsMap;
+    }
+
+    public Map getResumptionMap(String resumptionToken) {
+        return getResumptionMap(resumptionToken, -1, -1);
+    }
+
+    public Map getResumptionMap(String resumptionToken, int completeListSize, int cursor) {
+        Map resumptionMap = null;
+        if (resumptionToken != null) {
+            resumptionMap = new HashMap();
+            resumptionMap.put("resumptionToken", resumptionToken);
+            if (millisecondsToLive > 0) {
+//              Date now = new Date();
+                Date then = new Date((new Date()).getTime() + millisecondsToLive);
+                resumptionMap.put("expirationDate", ServerVerb.createResponseDate(then));
+            }
+            if (completeListSize >= 0) {
+                resumptionMap.put("completeListSize", Integer.toString(completeListSize));
+            }
+            if (cursor >= 0) {
+                resumptionMap.put("cursor", Integer.toString(cursor));
+            }
+        }
+        return resumptionMap;
+    }
+
+    /**
+     * close the repository
+     */
+    public abstract void close();
+
+    /**
+     * Validate the parameter value
+     * The default regex pattern can be overridden by setting a AbstractCatalog.paramRegex property
+     *
+     * @param key
+     * @param value
+     * @return true if the parameter is valid, false if not
+     */
+	public boolean isValidParam(String key, String value) {
+		if (paramRegex != null && paramRegex.length() > 0) {
+			return value.matches(paramRegex);
+		}
+		return true;
+	}
+}
diff --git a/src/ORG/oclc/oai/server/catalog/ExtendedJDBCOAICatalog.java b/src/ORG/oclc/oai/server/catalog/ExtendedJDBCOAICatalog.java
new file mode 100644
index 0000000..31dbbd2
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/ExtendedJDBCOAICatalog.java
@@ -0,0 +1,1410 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * ExtendedJDBCOAICatalog is an example of how to implement the AbstractCatalog interface
+ * for JDBC. Pattern an implementation of the AbstractCatalog interface after this class
+ * to have OAICat work with your JDBC database.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class ExtendedJDBCOAICatalog extends AbstractCatalog {
+    private static final boolean debug = true;
+
+    /**
+     * SQL identifier query (loaded from properties)
+     * \\i -> localIdentifier, \\o -> oaiIdentifier
+     */
+    private String identifierQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until
+     */
+    private String rangeQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until, \\s -> set
+     */
+    private String rangeSetQuery = null;
+
+    /**
+     * SQL query to get a list of available sets
+     */
+    private String setQuery = null;
+
+    /**
+     * SQL query to get a list of available sets that apply to a particular identifier
+     */
+    private String setSpecQuery = null;
+
+    /**
+     * SQL query to get a list of available abouts that apply to a particular identifier
+     */
+    private String aboutQuery = null;
+
+    /**
+     * SQL column labels containing the values of particular interest
+     */
+    private String aboutValueLabel = null;
+    private String setSpecItemLabel = null;
+    private String setSpecListLabel = null;
+    private String setNameLabel = null;
+    private String setDescriptionLabel = null;
+
+    /**
+     * maximum number of entries to return for ListRecords and ListIdentifiers
+     * (loaded from properties)
+     */
+    private int maxListSize;
+
+    /**
+     * Set Strings to be loaded from the properties file
+     * (if they are to be loaded from properties rather than queried from the database)
+     */
+    ArrayList sets = new ArrayList();
+
+    /**
+     * The JDBC Connection
+     */
+    private boolean isPersistentConnection = true;
+    private Connection persistentConnection;
+    private String jdbcURL = null;
+    private String jdbcLogin = null;
+    private String jdbcPasswd = null;
+
+    /**
+     * pending resumption tokens
+     */
+    private HashMap resumptionResults = new HashMap();
+
+    /**
+     * Construct a ExtendedJDBCOAICatalog object
+     *
+     * @param properties a properties object containing initialization parameters
+     * @exception IOException an I/O error occurred during database initialization.
+     */
+    public ExtendedJDBCOAICatalog(Properties properties) throws IOException {
+        String maxListSize = properties.getProperty("ExtendedJDBCOAICatalog.maxListSize");
+        if (maxListSize == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.maxListSize is missing from the properties file");
+        } else {
+            this.maxListSize = Integer.parseInt(maxListSize);
+        }
+
+        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
+        if (paramRegex != null) {
+            setParamRegex(paramRegex);
+        }
+        String jdbcDriverName = properties.getProperty("ExtendedJDBCOAICatalog.jdbcDriverName");
+        if (jdbcDriverName == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcDriverName is missing from the properties file");
+        }
+        jdbcURL = properties.getProperty("ExtendedJDBCOAICatalog.jdbcURL");
+        if (jdbcURL == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcURL is missing from the properties file");
+        }
+
+        jdbcLogin = properties.getProperty("ExtendedJDBCOAICatalog.jdbcLogin");
+        if (jdbcLogin == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcLogin is missing from the properties file");
+        }
+
+        jdbcPasswd = properties.getProperty("ExtendedJDBCOAICatalog.jdbcPasswd");
+        if (jdbcPasswd == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcPasswd is missing from the properties file");
+        }
+
+        rangeQuery = properties.getProperty("ExtendedJDBCOAICatalog.rangeQuery");
+        if (rangeQuery == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.rangeQuery is missing from the properties file");
+        }
+
+        rangeSetQuery = properties.getProperty("ExtendedJDBCOAICatalog.rangeSetQuery");
+        if (rangeSetQuery == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.rangeSetQuery is missing from the properties file");
+        }
+
+        identifierQuery = properties.getProperty("ExtendedJDBCOAICatalog.identifierQuery");
+        if (identifierQuery == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.identifierQuery is missing from the properties file");
+        }
+
+        aboutQuery = properties.getProperty("ExtendedJDBCOAICatalog.aboutQuery");
+        if (aboutQuery != null) {
+            aboutValueLabel = properties.getProperty("ExtendedJDBCOAICatalog.aboutValueLabel");
+            if (aboutValueLabel == null) {
+                throw new IllegalArgumentException("ExtendedJDBCOAICatalog.aboutValueLabel is missing from the properties file");
+            }
+        }
+
+        setSpecQuery = properties.getProperty("ExtendedJDBCOAICatalog.setSpecQuery");
+        setSpecItemLabel = properties.getProperty("ExtendedJDBCOAICatalog.setSpecItemLabel");
+        if (setSpecItemLabel == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setSpecItemLabel is missing from the properties file");
+        }
+        setSpecListLabel = properties.getProperty("ExtendedJDBCOAICatalog.setSpecListLabel");
+        if (setSpecListLabel == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setSpecListLabel is missing from the properties file");
+        }
+        setNameLabel = properties.getProperty("ExtendedJDBCOAICatalog.setNameLabel");
+        if (setNameLabel == null) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setNameLabel is missing from the properties file");
+        }
+        setDescriptionLabel = properties.getProperty("ExtendedJDBCOAICatalog.setDescriptionLabel");
+//      if (setDescriptionLabel == null) {
+//      throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setDescriptionLabel is missing from the properties file");
+//      }
+
+        // See if a setQuery exists
+        setQuery = properties.getProperty("ExtendedJDBCOAICatalog.setQuery");
+        if (setQuery == null) {
+            // if not, load the set Strings from the properties file (if present)
+            String propertyPrefix = "Sets.";
+            Enumeration propNames = properties.propertyNames();
+            while (propNames.hasMoreElements()) {
+                String propertyName = (String)propNames.nextElement();
+                if (propertyName.startsWith(propertyPrefix)) {
+                    sets.add(properties.get(propertyName));
+                }
+            }
+        }
+
+        String temp = properties.getProperty("ExtendedJDBCOAICatalog.isPersistentConnection");
+        if ("false".equalsIgnoreCase(temp))
+            isPersistentConnection = false;
+
+        // open the connection
+        try {
+            Class.forName(jdbcDriverName);
+        } catch (ClassNotFoundException e) {
+            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcDriverName is invalid: "
+                    + jdbcDriverName);
+        }
+
+        if (isPersistentConnection) {
+            try {
+                persistentConnection = getNewConnection();
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new IOException(e.getMessage());
+            }
+        }
+    }
+
+    private Connection getNewConnection() throws SQLException {
+        // open the connection
+        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
+    }
+
+    private Connection startConnection() throws SQLException {
+        if (persistentConnection != null) {
+            if (persistentConnection.isClosed())
+                persistentConnection = getNewConnection();
+            return persistentConnection;
+        } else {
+            return getNewConnection();
+        }
+    }
+
+    private void endConnection(Connection con) throws OAIInternalServerError {
+        try {
+            if (persistentConnection == null)
+                con.close();
+        } catch (SQLException e) {
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
+     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
+     * but the item is flagged as deleted and thus no schemaLocations (i.e.
+     * metadataFormats) can be produced.
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
+        Connection con = null;
+        try {
+            con = startConnection();
+            Statement stmt = con.createStatement();
+            ResultSet rs =
+                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
+            /*
+             * Let your recordFactory decide which schemaLocations
+             * (i.e. metadataFormats) it can produce from the record.
+             * Doing so will preserve the separation of database access
+             * (which happens here) from the record content interpretation
+             * (which is the responsibility of the RecordFactory implementation).
+             */
+            if (!rs.next()) {
+                stmt.close();
+                endConnection(con);
+                throw new IdDoesNotExistException(oaiIdentifier);
+            } else {
+                /* Make sure the identifierQuery returns the columns you need
+                 * (if any) to determine the supported schemaLocations for this item */
+                HashMap nativeItem = new HashMap();
+                nativeItem.put("coreResult", getColumnValues(rs));
+                stmt.close();
+                endConnection(con);
+                return getRecordFactory().getSchemaLocations(nativeItem);
+            }
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Get remainder of the nativeItem. By default, it is assumed
+     * the entire nativeItem is produced by the default query.
+     * If other tables must be consulted, extend this class and
+     * override this method. For best results, create one new entry
+     * in the nativeItem HashMap for each additional query peformed.
+     */
+    protected void extendItem(Connection con, HashMap nativeItem) {
+    }
+
+    /**
+     * Since the columns should only be read once, copy them into a
+     * HashMap and consider that to be the "record"
+     * @param rs The ResultSet row
+     * @return a HashMap mapping column names with values
+     */
+    private HashMap getColumnValues(ResultSet rs)
+    throws SQLException {
+        ResultSetMetaData mdata = rs.getMetaData();
+        int count = mdata.getColumnCount();
+        HashMap tableItems = new HashMap(count);
+        for (int i=1; i<=count; ++i) {
+            String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
+            tableItems.put(fieldName, rs.getObject(i));
+            if (debug) System.out.println(fieldName + "=" + tableItems.get(fieldName));
+//          if (debug) {
+//          System.out.println("ExtendedJDBCOAICatalog.getColumnValues");
+//          System.out.println("catalogName=" + mdata.getCatalogName(i));
+//          System.out.println("columnLabel=" + mdata.getColumnLabel(i));
+//          System.out.println("columnName=" + mdata.getColumnName(i));
+//          System.out.println("schemaName=" + mdata.getSchemaName(i));
+//          System.out.println("tableName=" + mdata.getTableName(i));
+//          }
+        }
+        return tableItems;
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the rangeQuery String
+     * NOTE! This retrieves an extra record so we can decide if EOF has been reached.
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateRangeQuery(String from, String until, String set, int offset, int count)
+    throws OAIInternalServerError {
+        StringBuffer sb = new StringBuffer();
+        StringTokenizer tokenizer;
+        if (set == null || set.length() == 0)
+            tokenizer = new StringTokenizer(rangeQuery, "\\");
+        else
+            tokenizer = new StringTokenizer(rangeSetQuery, "\\");
+
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid query");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'a':
+                sb.append(Integer.toString(offset));
+                break;
+            case 'b':
+//              sb.append(Integer.toString(count));
+                sb.append(Integer.toString(count+1)); // grab an extra record to decide if EOF
+                break;
+            case 'f':  // what are the chances someone would use this to indicate a form feed?
+                sb.append(formatFromDate(from));
+                break;
+            case 'u':
+                sb.append(formatUntilDate(until));
+                break;
+            case 's':
+                sb.append(set);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatFromDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatUntilDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Change the String from UTC to SQL format
+     * If this method doesn't suit your needs, extend this class and override
+     * the method rather than change this code directly.
+     */
+    protected String formatDate(String date) {
+        StringBuffer sb = new StringBuffer();
+        sb.append(date.substring(5, 7));
+        sb.append("/");
+        sb.append(date.substring(8));
+        sb.append("/");
+        sb.append(date.substring(0, 4));
+        if (debug) System.out.println("ExtendedJDBCOAICatalog.formatDate: from " + date + " to " + sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateIdentifierQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateSetSpecQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateAboutQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Retrieve a list of identifiers that satisfy the specified criteria
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * It may seem strange for the map to include both "headers" and "identifiers"
+     * since the identifiers can be obtained from the headers. This may be true, but
+     * AbstractCatalog.listRecords() can operate quicker if it doesn't
+     * need to parse identifiers from the XML headers itself. Better
+     * still, do like I do below and override AbstractCatalog.listRecords().
+     * AbstractCatalog.listRecords() is relatively inefficient because given the list
+     * of identifiers, it must call getRecord() individually for each as it constructs
+     * its response. It's much more efficient to construct the entire response in one fell
+     * swoop by overriding listRecords() as I've done here.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+    throws NoItemsMatchException, OAIInternalServerError {
+//      purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        Connection con = null;
+
+        try {
+            con = startConnection();
+            /* Get some records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
+//          rs.last();
+//          int numRows = rs.getRow();
+//          rs.beforeFirst();
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count=0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = new HashMap();
+                nativeItem.put("coreResult", getColumnValues(rs));
+                /* Use the RecordFactory to extract header/identifier pairs for each item */
+//                RecordFactory rf = getRecordFactory();
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            if (count == 0) {
+                stmt.close();
+                endConnection(con);
+                throw new NoItemsMatchException();
+            }
+
+            /* decide if you're done */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+//              String resumptionId = getResumptionId();
+//              resumptionResults.put(resumptionId, rs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append("!");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append("!");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        0));
+                //          listIdentifiersMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+            }
+            stmt.close();
+            endConnection(con);
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+//      purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+
+        /****************************
+         * parse your resumptionToken
+         ****************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+//      String resumptionId;
+        String from;
+        String until;
+        String set;
+        int oldCount;
+        String metadataPrefix;
+//      int numRows;
+        try {
+//          resumptionId = tokenizer.nextToken();
+            from = tokenizer.nextToken();
+            until = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+            if (set.equals("."))
+                set = null;
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+//          numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            if (debug) {
+                System.out.println("ExtendedJDBCOAICatalog.listIdentifiers: set=>"
+                        + set + "<");
+            }
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        Connection con = null;
+        try {
+            con = startConnection();
+            /* Get some more records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
+                    oldCount, maxListSize));
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = new HashMap();
+                nativeItem.put("coreResult", getColumnValues(rs));
+                /* Use the RecordFactory to extract header/identifier pairs for each item */
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            /* decide if you're done. */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append("!");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append("!");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        oldCount));
+                //          listIdentifiersMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+            }
+            stmt.close();
+            endConnection(con);
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return the <record/> portion of the XML response.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix is not
+     * supported by the item.
+     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+    throws OAIInternalServerError, CannotDisseminateFormatException,
+    IdDoesNotExistException {
+        Connection con = null;
+        try {
+            con = startConnection();
+            Statement stmt = con.createStatement();
+            ResultSet rs =
+                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
+            if (!rs.next()) {
+                endConnection(con);
+                throw new IdDoesNotExistException(oaiIdentifier);
+            }
+            HashMap nativeItem = new HashMap();
+            nativeItem.put("coreResult", getColumnValues(rs));
+            extendItem(con, nativeItem);
+            stmt.close();
+            endConnection(con);
+            return constructRecord(nativeItem, metadataPrefix);
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria. Note, though,
+     * that unlike the other OAI verb type methods implemented here, both of the
+     * listRecords methods are already implemented in AbstractCatalog rather than
+     * abstracted. This is because it is possible to implement ListRecords as a
+     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
+     * I suggest that you override both the AbstractCatalog.listRecords methods
+     * here since it will probably improve the performance if you create the
+     * response in one fell swoop rather than construct it one GetRecord at a time.
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for a "records" Iterator object
+     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix isn't
+     * supported by the item.
+     */
+    public Map listRecords(String from, String until, String set, String metadataPrefix)
+    throws CannotDisseminateFormatException, NoItemsMatchException,
+    OAIInternalServerError {
+//      purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        Connection con = null;
+
+        try {
+            con = startConnection();
+            /* Get some records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
+                    ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
+//          rs.last();
+//          int numRows = rs.getRow();
+//          rs.beforeFirst();
+            int count;
+
+//          if (debug) System.out.println("ExtendedJDBCOAICatalog.listRecords: numRows=" + numRows);
+
+            /* load the records ArrayList */
+            for (count=0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = new HashMap();
+                nativeItem.put("coreResult", getColumnValues(rs));
+                extendItem(con, nativeItem);
+                String record = constructRecord(nativeItem, metadataPrefix);
+                records.add(record);
+            }
+
+            if (count == 0) {
+                endConnection(con);
+                throw new NoItemsMatchException();
+            }
+
+            /* decide if you're done */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+//              String resumptionId = getResumptionId();
+//              resumptionResults.put(resumptionId, rs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append("!");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append("!");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append("!");
+//              resumptionTokenSb.append(Integer.toString(numRows));
+//              resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        0));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
+            }
+            stmt.close();
+            endConnection(con);
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception BadResumptionTokenException the value of the resumptionToken argument
+     * is invalid or expired.
+     */
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+//      purge(); // clean out old resumptionTokens
+
+        /****************************
+         * parse your resumptionToken
+         ****************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+//      String resumptionId;
+        String from;
+        String until;
+        String set;
+        int oldCount;
+//      int numRows;
+        String metadataPrefix;
+        try {
+//          resumptionId = tokenizer.nextToken();
+            from = tokenizer.nextToken();
+            until = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+            if (set.equals("."))
+                set = null;
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+//          numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        Connection con = null;
+        try {
+            con = startConnection();
+            /* Get some more records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
+                    oldCount, maxListSize));
+
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && rs.next(); ++count) {
+                try {
+                    HashMap nativeItem = new HashMap();
+                    nativeItem.put("coreResult", getColumnValues(rs));
+                    extendItem(con, nativeItem);
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                } catch (CannotDisseminateFormatException e) {
+                    /* the client hacked the resumptionToken beyond repair */
+                    endConnection(con);
+                    throw new BadResumptionTokenException();
+                }
+            }
+
+            /* decide if you're done */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append("!");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append("!");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append("!");
+//              resumptionTokenSb.append(Integer.toString(numRows));
+//              resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        oldCount));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+            }
+            stmt.close();
+            endConnection(con);
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(HashMap nativeItem, String metadataPrefix)
+    throws CannotDisseminateFormatException, OAIInternalServerError {
+        String schemaURL = null;
+        Iterator setSpecs = getSetSpecs(nativeItem);
+        Iterator abouts = getAbouts(nativeItem);
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * Retrieve a list of sets that satisfy the specified criteria
+     *
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception OAIBadRequestException signals an http status code 400 problem
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
+        if (setQuery == null) {
+            if (sets.size() == 0)
+                throw new NoSetHierarchyException();
+            Map listSetsMap = new HashMap();
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            Connection con = null;
+            try {
+                if (debug) System.out.println(setQuery);
+
+                con = startConnection();
+                /* Get some records from your database */
+                Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+                ResultSet rs = stmt.executeQuery(setQuery);
+                rs.last();
+                int numRows = rs.getRow();
+                rs.beforeFirst();
+                int count;
+
+//              if (debug) System.out.println("ExtendedJDBCOAICatalog.listSets: numRows=" + numRows);
+
+                /* load the sets ArrayLists. */
+                for (count=0; count < maxListSize && rs.next(); ++count) {
+                    /* Use the RecordFactory to extract header/set pairs for each item */
+                    HashMap nativeItem = getColumnValues(rs);
+                    sets.add(getSetXML(nativeItem));
+                    if (debug) System.out.println("ExtendedJDBCOAICatalog.listSets: adding an entry");
+                }
+
+                /* decide if you're done */
+                if (count < numRows) {
+                    String resumptionId = getResumptionId();
+                    resumptionResults.put(resumptionId, rs);
+
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(count));
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            0));
+                    //          listSetsMap.put("resumptionMap",
+                    //                                 getResumptionMap(resumptionTokenSb.toString()));
+                }
+                stmt.close();
+                endConnection(con);
+            } catch (SQLException e) {
+                if (con != null)
+                    endConnection(con);
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * Retrieve the next set of sets associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listSets() Map result.
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets(String resumptionToken)
+    throws OAIInternalServerError, BadResumptionTokenException {
+        if (setQuery == null) {
+            throw new BadResumptionTokenException();
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            /****************************
+             * parse your resumptionToken
+             ****************************/
+            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+            String resumptionId;
+            int oldCount;
+            int numRows;
+            try {
+                resumptionId = tokenizer.nextToken();
+                oldCount = Integer.parseInt(tokenizer.nextToken());
+                numRows = Integer.parseInt(tokenizer.nextToken());
+            } catch (NoSuchElementException e) {
+                throw new BadResumptionTokenException();
+            }
+
+            try {
+                /* Get some more records from your database */
+                ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
+                if (rs == null) {
+                    throw new BadResumptionTokenException();
+                }
+
+                if (rs.getRow() != oldCount) {
+//                  System.out.println("ExtendedJDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                    rs.absolute(oldCount);
+                }
+
+                int count;
+
+                /* load the sets ArrayLists. */
+                for (count = 0; count < maxListSize && rs.next(); ++count) {
+                    HashMap nativeItem = getColumnValues(rs);
+                    /* Use the RecordFactory to extract set for each item */
+                    sets.add(getSetXML(nativeItem));
+                }
+
+                /* decide if you're done. */
+                if (oldCount+count < numRows) {
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(oldCount + count));
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            oldCount));
+                    //          listSetsMap.put("resumptionMap",
+                    //                                 getResumptionMap(resumptionTokenSb.toString()));
+                }
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(HashMap nativeItem)
+    throws OAIInternalServerError {
+        Connection con = null;
+        try {
+            ArrayList setSpecs = new ArrayList();
+            if (setSpecQuery != null) {
+                con = startConnection();
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                Statement stmt = con.createStatement();
+                ResultSet rs = stmt.executeQuery(populateSetSpecQuery(oaiIdentifier));
+                while (rs.next()) {
+                    HashMap setMap = getColumnValues(rs);
+                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
+                }
+                stmt.close();
+                endConnection(con);
+            }
+            return setSpecs.iterator();
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(HashMap nativeItem)
+    throws OAIInternalServerError {
+        Connection con = null;
+        try {
+            ArrayList abouts = new ArrayList();
+            if (aboutQuery != null) {
+                con = startConnection();
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                Statement stmt = con.createStatement();
+                ResultSet rs = stmt.executeQuery(populateAboutQuery(oaiIdentifier));
+                while (rs.next()) {
+                    HashMap aboutMap = getColumnValues(rs);
+                    abouts.add(aboutMap.get(aboutValueLabel));
+                }
+                stmt.close();
+                endConnection(con);
+            }
+            return abouts.iterator();
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Extract &lt;set&gt; XML string from setItem object
+     *
+     * @param setItem individual set instance in native format
+     * @return an XML String containing the XML &lt;set&gt; content
+     */
+    public String getSetXML(HashMap setItem)
+    throws IllegalArgumentException {
+//      try {
+        String setSpec = getSetSpec(setItem);
+        String setName = getSetName(setItem);
+        String setDescription = getSetDescription(setItem);
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("<set>");
+        sb.append("<setSpec>");
+        sb.append(OAIUtil.xmlEncode(setSpec));
+        sb.append("</setSpec>");
+        sb.append("<setName>");
+        sb.append(OAIUtil.xmlEncode(setName));
+        sb.append("</setName>");
+        if (setDescription != null) {
+            sb.append("<setDescription>");
+            sb.append(OAIUtil.xmlEncode(setDescription));
+            sb.append("</setDescription>");
+        }
+        sb.append("</set>");
+        return sb.toString();
+//      } catch (SQLException e) {
+//      e.printStackTrace();
+//      throw new IllegalArgumentException(e.getMessage());
+//      }
+    }
+
+    /**
+     * get the setSpec XML string. Extend this class and override this method
+     * if the setSpec can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setSpec&gt; content
+     */
+    protected String getSetSpec(HashMap setItem) {
+        try {
+            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+            return "UnsupportedEncodingException";
+        }
+    }
+
+    /**
+     * get the setName XML string. Extend this class and override this method
+     * if the setName can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setName&gt; content
+     */
+    protected String getSetName(HashMap setItem) {
+        return (String)setItem.get(setNameLabel);
+    }
+
+    /**
+     * get the setDescription XML string. Extend this class and override this method
+     * if the setDescription can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setDescription&gt; content
+     */
+    protected String getSetDescription(HashMap setItem) {
+        if (setDescriptionLabel == null)
+            return null;
+        return (String)setItem.get(setDescriptionLabel);
+    }
+
+    /**
+     * close the repository
+     */
+    public void close() {
+        try {
+            if (persistentConnection != null)
+                persistentConnection.close();
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        persistentConnection = null;
+    }
+
+    /**
+     * Purge tokens that are older than the configured time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date now = new Date();
+        Iterator keySet = resumptionResults.keySet().iterator();
+        while (keySet.hasNext()) {
+            String key = (String)keySet.next();
+            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            String key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a String version of the current time
+     */
+    private synchronized static String getResumptionId() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/ExtendedJDBCRecordFactory.java b/src/ORG/oclc/oai/server/catalog/ExtendedJDBCRecordFactory.java
new file mode 100644
index 0000000..17e3c7d
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/ExtendedJDBCRecordFactory.java
@@ -0,0 +1,170 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+/**
+ * ExtendedJDBCRecordFactory converts JDBC "items" to "record" Strings.
+ */
+public class ExtendedJDBCRecordFactory extends RecordFactory {
+    private String repositoryIdentifier = null;
+    protected String identifierLabel = null;
+    protected String datestampLabel = null;
+    
+    /**
+     * Construct an ExtendedJDBCRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public ExtendedJDBCRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+	repositoryIdentifier = properties.getProperty("ExtendedJDBCRecordFactory.repositoryIdentifier");
+	if (repositoryIdentifier == null) {
+	    throw new IllegalArgumentException("ExtendedJDBCRecordFactory.repositoryIdentifier is missing from the properties file");
+	}
+	identifierLabel = properties.getProperty("ExtendedJDBCRecordFactory.identifierLabel");
+	if (identifierLabel == null) {
+	    throw new IllegalArgumentException("ExtendedJDBCRecordFactory.identifierLabel is missing from the properties file");
+	}
+	datestampLabel = properties.getProperty("ExtendedJDBCRecordFactory.datestampLabel");
+	if (datestampLabel == null) {
+	    throw new IllegalArgumentException("ExtendedJDBCRecordFactory.datestampLabel is missing from the properties file");
+	}
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param oaiIdentifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String oaiIdentifier) {
+	StringTokenizer tokenizer = new StringTokenizer(oaiIdentifier, ":");
+	try {
+	    tokenizer.nextToken();
+	    tokenizer.nextToken();
+	    return tokenizer.nextToken();
+	} catch (java.util.NoSuchElementException e) {
+	    return null;
+	}
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+// 	throws IllegalArgumentException {
+// 	try {
+	    HashMap table = (HashMap)((HashMap)nativeItem).get("coreResult");
+	    return table.get(identifierLabel).toString();
+// 	} catch (SQLException e) {
+// 	    e.printStackTrace();
+// 	    throw new IllegalArgumentException(e.getMessage());
+// 	}
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem)
+	throws IllegalArgumentException {
+	StringBuffer sb = new StringBuffer();
+	sb.append("oai:");
+	sb.append(repositoryIdentifier);
+	sb.append(":");
+	sb.append(getLocalIdentifier(nativeItem));
+	return sb.toString();
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem) {
+// 	throws IllegalArgumentException  {
+// 	try {
+	    HashMap table = (HashMap)((HashMap)nativeItem).get("coreResult");
+	    return ((Timestamp)table.get(datestampLabel)).toString().substring(0, 10);
+// 	} catch (SQLException e) {
+// 	    e.printStackTrace();
+// 	    throw new IllegalArgumentException(e.getMessage());
+// 	}
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item. Null if setSpecs aren't
+     * derived from the nativeItem.
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+	return null;
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item. Null if
+     * abouts aren't derived from the nativeItem
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+	// Don't perform quick creates
+	return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/FileRecordFactory.java b/src/ORG/oclc/oai/server/catalog/FileRecordFactory.java
new file mode 100644
index 0000000..c0cd559
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/FileRecordFactory.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+/**
+ * FileRecordFactory converts native XML "items" to "record" Strings.
+ * This factory assumes the native XML item looks exactly like the <record>
+ * element of an OAI GetRecord response, with the possible exception that the
+ * <metadata> element contains multiple metadataFormats from which to choose.
+ */
+public class FileRecordFactory extends RecordFactory {
+    private String repositoryIdentifier = null;
+    
+    /**
+     * Construct an FileRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public FileRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+	repositoryIdentifier = properties.getProperty("FileRecordFactory.repositoryIdentifier");
+	if (repositoryIdentifier == null) {
+	    throw new IllegalArgumentException("FileRecordFactory.repositoryIdentifier is missing from the properties file");
+	}
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+        try {
+            StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
+            tokenizer.nextToken();
+            tokenizer.nextToken();
+            return tokenizer.nextToken();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+	StringBuffer sb = new StringBuffer();
+	sb.append("oai:");
+	sb.append(repositoryIdentifier);
+	sb.append(":");
+	sb.append(getLocalIdentifier(nativeItem));
+	return sb.toString();
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+        return (String)((HashMap)nativeItem).get("localIdentifier");
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException  {
+        return (String)((HashMap)nativeItem).get("lastModified");
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+	return null;
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+	// Don't perform quick creates
+	return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/FileSystemOAICatalog.java b/src/ORG/oclc/oai/server/catalog/FileSystemOAICatalog.java
new file mode 100644
index 0000000..aaa017a
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/FileSystemOAICatalog.java
@@ -0,0 +1,686 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+// import ORG.oclc.oai.util.*;
+// import org.xml.sax.*;
+
+
+/**
+ * FileSystemOAICatalog is an implementation of AbstractCatalog interface
+ * with the data sitting in a directory on a filesystem.
+ *
+ * @author Ralph LeVan, OCLC Online Computer Library Center
+ */
+
+public class FileSystemOAICatalog extends AbstractCatalog {
+    static final boolean debug=false;
+
+    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
+    protected String           homeDir;
+    private HashMap          fileDateMap = new HashMap();
+    private HashMap          resumptionResults=new HashMap();
+    private int              maxListSize;
+    private boolean hideExtension = false;
+
+    public FileSystemOAICatalog(Properties properties) {
+        String          temp;
+
+        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        temp=properties.getProperty("FileSystemOAICatalog.maxListSize");
+        if (temp==null)
+            throw new IllegalArgumentException("FileSystemOAICatalog."+
+                "maxListSize is missing from the properties file");
+        maxListSize = Integer.parseInt(temp);
+        if(debug)
+            System.out.println("in FileSystemOAICatalog(): maxListSize="+
+                maxListSize);
+
+	hideExtension = "true".equalsIgnoreCase(properties.getProperty("FileSystemOAICatalog.hideExtension"));
+
+        homeDir=properties.getProperty("FileSystemOAICatalog.homeDir");
+        if (homeDir==null)
+            throw new IllegalArgumentException("FileSystemOAICatalog."+
+                "homeDir is missing from the properties file");
+        if(debug)
+            System.out.println("in FileSystemOAICatalog(): homeDir="+
+                homeDir);
+
+
+	File homeFile = new File(homeDir);
+	int homeDirLen = homeFile.getPath().length()+1;
+	loadFileMap(homeDirLen, homeFile);
+// 	Iterator iterator = fileDateMap.entrySet().iterator();
+// 	while (iterator.hasNext()) {
+// 	    Map.Entry entry = (Map.Entry)iterator.next();
+// 	    System.out.println(entry.getKey() + ":" + entry.getValue());
+// 	}
+    }
+
+    private void loadFileMap(int homeDirLen, File currentDir) {
+	String[] list = currentDir.list();
+	for (int i=0; i<list.length; ++i) {
+	    File child = new File(currentDir, list[i]);
+	    if (child.isDirectory()) {
+		loadFileMap(homeDirLen, child);
+	    } else if (isMetadataFile(child)) {
+		String localIdentifier = file2LocalIdentifier(homeDirLen, child);
+                String datestamp = date2OAIDatestamp(new Date(child.lastModified()));
+		fileDateMap.put(localIdentifier, datestamp);
+	    }
+	}
+    }
+
+    /**
+     * Override this method if some files exist in the
+     * filesystem that aren't metadata records.
+     *
+     * @param child the File to be investigated
+     * @return true if it contains metadata, false otherwise
+     */
+    protected boolean isMetadataFile(File child) {
+        return true;
+    }
+
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param homeDirLen the length of the home directory path
+     * @param file the File object containing the native record
+     * @return localIdentifier
+     */
+    protected String file2LocalIdentifier(int homeDirLen, File file) {
+	String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
+	if (hideExtension && fileName.endsWith(".xml")) {
+	    fileName = fileName.substring(0, fileName.lastIndexOf(".xml"));
+	}
+	return fileName;
+    }
+
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
+     * @return the File object containing the native record
+     */
+    protected File localIdentifier2File(String localIdentifier) {
+        String fileName = localIdentifier.replace('/', File.separatorChar);
+	if (hideExtension) {
+	    fileName = fileName + ".xml";
+	}
+	return new File(homeDir, fileName);
+    }
+
+    private String date2OAIDatestamp(Date date) {
+        return dateFormatter.format(date);
+    }
+
+    private HashMap getNativeHeader(String localIdentifier) {
+        HashMap recordMap = null;
+        if (fileDateMap.containsKey(localIdentifier)) {
+            recordMap = new HashMap();
+            recordMap.put("localIdentifier", localIdentifier);
+            recordMap.put("lastModified", fileDateMap.get(localIdentifier));
+            return recordMap;
+        }
+        return recordMap;
+    }
+    
+    private HashMap getNativeRecord(String localIdentifier)
+        throws IOException {
+        HashMap recordMap = getNativeHeader(localIdentifier);
+        if (recordMap == null) {
+            return null;
+        } else {
+            File file = localIdentifier2File(localIdentifier);
+            try {
+                FileInputStream fis = new FileInputStream(file);
+                BufferedInputStream bis = new BufferedInputStream(fis);
+                byte[] buffer = new byte[(int)file.length()];
+                bis.read(buffer, 0, (int)file.length());
+                recordMap.put("recordBytes", buffer);
+                return recordMap;
+            } catch (FileNotFoundException e) {
+                return null;
+            }
+        }
+    }
+    
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param     oaiIdentifier the OAI identifier
+     * @param     metadataPrefix the OAI metadataPrefix
+     * @return    the Record object containing the result.
+     * @exception CannotDisseminateFormatException signals an http status
+     *                code 400 problem
+     * @exception IdDoesNotExistException signals an http status code 404
+     *                problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *                problem
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+        throws IdDoesNotExistException, CannotDisseminateFormatException,
+               OAIInternalServerError {
+        HashMap nativeItem = null;
+        try {
+	    String localIdentifier
+		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+
+            nativeItem = getNativeRecord(localIdentifier);
+            if (nativeItem == null)
+                throw new IdDoesNotExistException(oaiIdentifier);
+            return constructRecord(nativeItem, metadataPrefix);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+    }
+
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * We get passed the ID for a record and are supposed to return a list
+     * of the formats that we can deliver the record in.  Since we are assuming
+     * that all the records in the directory have the same format, the
+     * response to this is static;
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAINotFoundException signals an http status code 404 problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+      throws IdDoesNotExistException, OAIInternalServerError, NoMetadataFormatsException {
+        HashMap nativeItem = null;
+        try {
+	    String localIdentifier
+		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+            nativeItem = getNativeRecord(localIdentifier);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+        
+        if (nativeItem != null) {
+            return getRecordFactory().getSchemaLocations(nativeItem);
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+
+
+    /**
+     * Retrieve a list of Identifiers that satisfy the criteria parameters
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+        throws NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+	Iterator iterator = fileDateMap.entrySet().iterator();
+	int numRows = fileDateMap.entrySet().size();
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            if (fileDate.compareTo(from) >= 0
+                && fileDate.compareTo(until) <= 0) {
+                HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
+                String[] header = getRecordFactory().createHeader(nativeHeader);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+                count++;
+            }
+	}
+
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listIdentifiersMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listIdentifiersMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of Identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+      throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("FileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+	    iterator = fileDateMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the headers and identifiers ArrayLists. */
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+	    HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
+		String[] header = getRecordFactory().createHeader(nativeHeader);
+		headers.add(header[0]);
+		identifiers.add(header[1]);
+		count++;
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listIdentifiersMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(HashMap nativeItem, String metadataPrefix)
+        throws CannotDisseminateFormatException {
+        String schemaURL = null;
+	Iterator setSpecs = getSetSpecs(nativeItem);
+	Iterator abouts = getAbouts(nativeItem);
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(HashMap nativeItem) {
+        return null;
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(HashMap nativeItem) {
+        return null;
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listRecords(String from, String until, String set,
+                                    String metadataPrefix)
+      throws CannotDisseminateFormatException,
+      OAIInternalServerError, NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+	Iterator iterator = fileDateMap.entrySet().iterator();
+	int numRows = fileDateMap.entrySet().size();
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            if (fileDate.compareTo(from) >= 0
+                && fileDate.compareTo(until) <= 0) {
+                try {
+                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                    count++;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            }
+	}
+        
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listRecordsMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listRecordsMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listRecords(String resumptionToken)
+      throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("FileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
+	    iterator = fileDateMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the records ArrayLists. */
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+	    try {
+                HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                String record = constructRecord(nativeItem, metadataPrefix);
+                records.add(record);
+                count++;
+            } catch (CannotDisseminateFormatException e) {
+                /* the client hacked the resumptionToken beyond repair */
+                throw new BadResumptionTokenException();
+            } catch (IOException e) {
+                /* the file is probably missing */
+                throw new BadResumptionTokenException();
+	    }
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listRecordsMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    public Map listSets() throws NoSetHierarchyException {
+	throw new NoSetHierarchyException();
+//         Map listSetsMap = new HashMap();
+//         listSetsMap.put("sets", setsList.iterator());
+//         return listSetsMap;
+    }
+
+
+    public Map listSets(String resumptionToken)
+      throws BadResumptionTokenException {
+	throw new BadResumptionTokenException();
+    }
+
+
+    /**
+     * close the repository
+     */
+    public void close() { }
+
+
+    /**
+     * Purge tokens that are older than the time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date      then, now = new Date();
+        Iterator  keySet = resumptionResults.keySet().iterator();
+        String    key;
+
+        while (keySet.hasNext()) {
+            key=(String)keySet.next();
+            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a long integer version of the current time
+     */
+    private synchronized static String getRSName() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/FolderOAICatalog.java b/src/ORG/oclc/oai/server/catalog/FolderOAICatalog.java
new file mode 100644
index 0000000..954f9a5
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/FolderOAICatalog.java
@@ -0,0 +1,891 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import javax.servlet.ServletContext;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.xpath.XPathAPI;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.util.OAIUtil;
+
+
+/**
+ * NewFileSystemOAICatalog is an implementation of AbstractCatalog interface
+ * with the data sitting in a directory on a filesystem.
+ *
+ * @author Jeff Young, OCLC Online Computer Library Center
+ */
+
+public class FolderOAICatalog extends AbstractCatalog {
+    static final boolean debug=false;
+    
+    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
+    protected String           homeDir;
+    private HashMap          datestampMap = new HashMap();
+    private HashMap identifierMap = new HashMap();
+    private HashMap setMap = new HashMap();
+    private HashMap          resumptionResults=new HashMap();
+    private int              maxListSize;
+    private ArrayList sets = null;
+    private ServletContext context = null;
+    
+    public FolderOAICatalog(Properties properties, ServletContext context)
+    throws IOException {
+        this.context = context;
+        String          temp;
+        
+        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        temp=properties.getProperty("NewFileSystemOAICatalog.maxListSize");
+        if (temp==null)
+            throw new IllegalArgumentException("NewFileSystemOAICatalog."+
+            "maxListSize is missing from the properties file");
+        maxListSize = Integer.parseInt(temp);
+        if(debug)
+            System.out.println("in NewFileSystemOAICatalog(): maxListSize="+
+                    maxListSize);
+        
+        homeDir=properties.getProperty("NewFileSystemOAICatalog.homeDir");
+        if (homeDir!=null) {
+            if(debug)
+                System.out.println("in NewFileSystemOAICatalog(): homeDir="+
+                        homeDir);
+            
+            
+            File homeFile = new File(homeDir);
+            int homeDirLen = homeFile.getPath().length()+1;
+            loadFileMap(homeDirLen, homeFile);
+        } else {
+            /* Try looking in a known location */
+            Set resourcePaths = context.getResourcePaths("/WEB-INF/DATA/");
+            if (resourcePaths == null || resourcePaths.size() == 0) {
+                throw new IllegalArgumentException("NewFileSystemOAICatalog."+
+                "homeDir is missing from the properties file");
+            }
+            String earliestDatestamp = loadFileMap(context, resourcePaths);
+            properties.setProperty("Identify.earliestDatestamp", earliestDatestamp);
+        }
+//      Iterator iterator = datestampMap.entrySet().iterator();
+//      while (iterator.hasNext()) {
+//      Map.Entry entry = (Map.Entry)iterator.next();
+//      System.out.println(entry.getKey() + ":" + entry.getValue());
+//      }
+        sets = getSets(properties);
+    }
+    
+    private static ArrayList getSets(Properties properties) {
+        InputStream is = Thread.currentThread().getContextClassLoader()
+        .getResourceAsStream("sets.properties");
+        Properties setProps = new Properties();
+        try {
+            setProps.load(is);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        TreeMap treeMap = new TreeMap();
+        Enumeration propNames = setProps.propertyNames();
+        while (propNames.hasMoreElements()) {
+            String propertyName = (String)propNames.nextElement();
+            String s = new StringBuffer("<set><setSpec>")
+            .append(OAIUtil.xmlEncode(propertyName))
+            .append("</setSpec><setName>")
+            .append(OAIUtil.xmlEncode((String) setProps.get(propertyName)))
+            .append("</setName></set>")
+            .toString();
+            treeMap.put(propertyName, s);
+        }
+        return new ArrayList(treeMap.values());
+    }
+    
+    private String loadFileMap(ServletContext context, Set resourcePaths)
+    throws IOException {
+        String earliestDatestamp = "9999-12-31";
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Iterator iter = resourcePaths.iterator();
+            while (iter.hasNext()) {
+                String resourcePath = (String)iter.next();
+                if (!resourcePath.endsWith("/")) {
+                    InputStream is = context.getResourceAsStream(resourcePath);
+                    InputSource data = new InputSource(is);
+                    Node doc = builder.parse(data);
+                    is.close();
+                    
+//                    Node datestampNode = XPathAPI.selectSingleNode(doc, "/record/header/datestamp");
+                    String datestamp = XPathAPI.eval(doc, "/record/header/datestamp").str();
+                    if (datestamp.compareTo(earliestDatestamp) < 0) {
+                        earliestDatestamp = datestamp;
+                    }
+                    String identifier = XPathAPI.eval(doc, "/record/header/identifier").str();
+                    datestampMap.put(resourcePath,datestamp);
+                    identifierMap.put(identifier, resourcePath);
+                    NodeList setNodes = XPathAPI.selectNodeList(doc, "/record/header/setSpec");
+                    for (int j=0; j<setNodes.getLength(); ++j) {
+                        Node setSpecNode = setNodes.item(j);
+                        String setSpec = XPathAPI.eval(setSpecNode,
+                        "string()").str();
+                        ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
+                        if (setSpecList == null) {
+                            setSpecList = new ArrayList();
+                            setMap.put(setSpec, setSpecList);
+                        }
+                        setSpecList.add(resourcePath);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new IOException(e.getMessage());
+        }
+        return earliestDatestamp;
+    }
+    
+    private void loadFileMap(int homeDirLen, File currentDir)
+    throws IOException {
+        try {
+            String[] list = currentDir.list();
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            for (int i=0; i<list.length; ++i) {
+                File child = new File(currentDir, list[i]);
+                if (child.isDirectory() && !"CVS".equals(child.getName())) {
+                    loadFileMap(homeDirLen, child);
+                } else if (isMetadataFile(child)) {
+                    String path = file2path(homeDirLen, child);
+                    System.out.println("parsing " + path);
+                    //                 String datestamp = date2OAIDatestamp(new Date(child.lastModified()));
+                    File file = localIdentifier2File(path);
+                    FileInputStream fis = new FileInputStream(file);
+                    InputSource data = new InputSource(fis);
+                    Node doc = builder.parse(data);
+                    fis.close();
+                    
+                    Node datestampNode = XPathAPI.selectSingleNode(doc, "/record/header/datestamp");
+                    datestampMap.put(path,
+                            XPathAPI.eval(datestampNode, "string()").str());
+                    NodeList setNodes = XPathAPI.selectNodeList(doc, "/record/header/setSpec");
+                    for (int j=0; j<setNodes.getLength(); ++j) {
+                        Node setSpecNode = setNodes.item(j);
+                        String setSpec = XPathAPI.eval(setSpecNode,
+                        "string()").str();
+                        ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
+                        if (setSpecList == null) {
+                            setSpecList = new ArrayList();
+                            setMap.put(setSpec, setSpecList);
+                        }
+                        setSpecList.add(path);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new IOException(e.getMessage());
+        }
+    }
+    
+    /**
+     * Override this method if some files exist in the
+     * filesystem that aren't metadata records.
+     *
+     * @param child the File to be investigated
+     * @return true if it contains metadata, false otherwise
+     */
+    protected boolean isMetadataFile(File child) {
+        return true;
+    }
+    
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param homeDirLen the length of the home directory path
+     * @param file the File object containing the native record
+     * @return localIdentifier
+     */
+    protected String file2path(int homeDirLen, File file) {
+        String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
+        return fileName;
+    }
+    
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
+     * @return the File object containing the native record
+     */
+    protected File localIdentifier2File(String localIdentifier) {
+        String fileName = localIdentifier.replace('/', File.separatorChar);
+        return new File(homeDir, fileName);
+    }
+    
+//  private String date2OAIDatestamp(Date date) {
+//  return dateFormatter.format(date);
+//  }
+    
+//    private HashMap getNativeHeader(String path) {
+//        HashMap recordMap = null;
+//        if (datestampMap.containsKey(path)) {
+//            recordMap = new HashMap();
+//            recordMap.put("localIdentifier", path.substring(0, path.lastIndexOf(".")));
+//            recordMap.put("lastModified", datestampMap.get(path));
+//            ArrayList setSpecs = new ArrayList();
+//            Iterator keySet = setMap.keySet().iterator();
+//            while (keySet.hasNext()) {
+//                String key = (String)keySet.next();
+//                ArrayList identifierList = (ArrayList)setMap.get(key);
+//                if (identifierList.contains(path)) {
+//                    setSpecs.add(key);
+//                }
+//            }
+//            recordMap.put("setSpecs", setSpecs.iterator());
+//            return recordMap;
+//        }
+//        return recordMap;
+//    }
+    
+    private Document getNativeRecord(String path) {
+        Document nativeRecord = null;
+//        HashMap recordMap = null;
+        if (datestampMap.containsKey(path)) {
+//            recordMap = new HashMap();
+//            File file = localIdentifier2File(path);
+            try {
+                InputStream is = context.getResourceAsStream(path);
+//                FileInputStream fis = new FileInputStream(file);
+                nativeRecord = OAIUtil.parse(is);
+//                Node node = XPathAPI.selectSingleNode(doc, "/record/header");
+//                String header = OAIUtil.toString(node);
+//                recordMap.put("headerNode", node);
+//                FileInputStream fis = new FileInputStream(file);
+//                BufferedInputStream bis = new BufferedInputStream(fis);
+//                byte[] buffer = new byte[(int)file.length()];
+//                bis.read(buffer, 0, (int)file.length());
+//                recordMap.put("recordBytes", buffer);
+//                bis.close();
+//                fis.close();
+//                return recordMap;
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
+        }
+        return nativeRecord;
+    }
+    
+    private ArrayList getExtensionList(String localIdentifier) {
+//      System.out.println("getExtensionList: localIdentifier="
+//      + localIdentifier);
+        ArrayList list = new ArrayList();
+        Iterator iterator = datestampMap.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry entry = (Map.Entry)iterator.next();
+//          System.out.println("entry.getKey()=" + entry.getKey());
+            if (((String)entry.getKey()).startsWith(localIdentifier)) {
+                list.add(((String)entry.getKey()).substring(localIdentifier.length()+1));
+            }
+        }
+        return list;
+    }
+    
+//    private HashMap getNativeRecord(String path)
+//    throws IOException {
+//        HashMap recordMap = getNativeHeader(path);
+//        if (recordMap == null) {
+//            return null;
+//        } else {
+//            File file = localIdentifier2File(path);
+//            try {
+//                FileInputStream fis = new FileInputStream(file);
+//                BufferedInputStream bis = new BufferedInputStream(fis);
+//                byte[] buffer = new byte[(int)file.length()];
+//                bis.read(buffer, 0, (int)file.length());
+//                recordMap.put("recordBytes", buffer);
+//                bis.close();
+//                fis.close();
+//                return recordMap;
+//            } catch (FileNotFoundException e) {
+//                return null;
+//            }
+//        }
+//    }
+    
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param     oaiIdentifier the OAI identifier
+     * @param     metadataPrefix the OAI metadataPrefix
+     * @return    the Record object containing the result.
+     * @exception CannotDisseminateFormatException signals an http status
+     *                code 400 problem
+     * @exception IdDoesNotExistException signals an http status code 404
+     *                problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *                problem
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+    throws IdDoesNotExistException, CannotDisseminateFormatException {
+        Document nativeItem = null;
+//        try {
+        String resourcePath = (String) identifierMap.get(oaiIdentifier);
+            
+            nativeItem = getNativeRecord(resourcePath);
+            if (nativeItem == null)
+                throw new IdDoesNotExistException(oaiIdentifier);
+            return constructRecord(nativeItem, metadataPrefix);
+//        } catch (IOException e) {
+//            e.printStackTrace();
+//            throw new OAIInternalServerError("Database Failure");
+//        }
+    }
+    
+    
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * We get passed the ID for a record and are supposed to return a list
+     * of the formats that we can deliver the record in.  Since we are assuming
+     * that all the records in the directory have the same format, the
+     * response to this is static;
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAINotFoundException signals an http status code 404 problem
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+    throws IdDoesNotExistException, NoMetadataFormatsException {
+        ArrayList extensionList = null;
+//      try {
+        String localIdentifier
+        = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+        extensionList = getExtensionList(localIdentifier);
+//      } catch (IOException e) {
+//      e.printStackTrace();
+//      throw new OAIInternalServerError("Database Failure");
+//      }
+        
+        if (extensionList != null) {
+            return getRecordFactory().getSchemaLocations(extensionList);
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+    
+    
+    /**
+     * Retrieve a list of Identifiers that satisfy the criteria parameters
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+    throws NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        Iterator iterator = datestampMap.entrySet().iterator();
+        int numRows = datestampMap.entrySet().size();
+        int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+        while (count < maxListSize && iterator.hasNext()) {
+            Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            String path = (String)entryDateMap.getKey();
+            if (fileDate.compareTo(from) >= 0
+                    && fileDate.compareTo(until) <= 0
+                    && (setIdentifiers == null || setIdentifiers.contains(path))) {
+                Document nativeRecord = getNativeRecord((String)entryDateMap.getKey());
+                String[] header = getRecordFactory().createHeader(nativeRecord);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+                count++;
+            }
+        }
+        
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+        /* decide if you're done */
+        if (iterator.hasNext()) {
+            String resumptionId = getRSName();
+            resumptionResults.put(resumptionId, iterator);
+            
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(numRows));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(set);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listIdentifiersMap.put("resumptionMap",
+                    getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            0));
+//          listIdentifiersMap.put("resumptionMap",
+//          getResumptionMap(resumptionTokenSb.toString()));
+        }
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+    
+    /**
+     * Retrieve the next set of Identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+        int numRows;
+        String set;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+        
+        /* Get some more records from your database */
+        Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+        if (iterator == null) {
+            System.out.println("NewFileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+            iterator = datestampMap.entrySet().iterator();
+            for (int i = 0; i<oldCount; ++i)
+                iterator.next();
+        }
+        
+        /* load the headers and identifiers ArrayLists. */
+        int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+        while (count < maxListSize && iterator.hasNext()) {
+            Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String path = (String)entryDateMap.getKey();
+            if (setIdentifiers == null || setIdentifiers.contains(path)) {
+                Document nativeRecord = getNativeRecord((String)entryDateMap.getKey());
+                String[] header = getRecordFactory().createHeader(nativeRecord);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+                count++;
+            }
+        }
+        
+        /* decide if you're done. */
+        if (iterator.hasNext()) {
+            resumptionId = getRSName();
+            resumptionResults.put(resumptionId, iterator);
+            
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(oldCount + count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(numRows));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(set);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                    numRows,
+                    oldCount));
+            //          listIdentifiersMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSb.toString()));
+        }
+        
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+    
+    
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(Document nativeItem, String metadataPrefix)
+    throws CannotDisseminateFormatException {
+        String schemaURL = null;
+        Iterator setSpecs = getSetSpecs(nativeItem);
+        Iterator abouts = getAbouts(nativeItem);
+        
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+    
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(Document nativeItem) {
+        return null;
+    }
+    
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(Document nativeItem) {
+        return null;
+    }
+    
+    /**
+     * Retrieve a list of records that satisfy the specified criteria
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listRecords(String from, String until, String set,
+            String metadataPrefix)
+    throws CannotDisseminateFormatException,
+    NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        Iterator iterator = datestampMap.entrySet().iterator();
+        int numRows = datestampMap.entrySet().size();
+        int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+        while (count < maxListSize && iterator.hasNext()) {
+            Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            String path = (String)entryDateMap.getKey();
+//            String extension = path.substring(path.lastIndexOf(".")+1);
+            if (fileDate.compareTo(from) >= 0
+                    && fileDate.compareTo(until) <= 0
+//                    && extension.equals(metadataPrefix)
+                    && (setIdentifiers == null || setIdentifiers.contains(path))) {
+//                try {
+                    Document nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                    count++;
+//                } catch (IOException e) {
+//                    e.printStackTrace();
+//                    throw new OAIInternalServerError(e.getMessage());
+//                }
+            }
+        }
+        
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+        /* decide if you're done */
+        if (iterator.hasNext()) {
+            String resumptionId = getRSName();
+            resumptionResults.put(resumptionId, iterator);
+            
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(numRows));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(set);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listRecordsMap.put("resumptionMap",
+                    getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            0));
+//          listRecordsMap.put("resumptionMap",
+//          getResumptionMap(resumptionTokenSb.toString()));
+        }
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+    
+    
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+        int numRows;
+        String set;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+        
+        /* Get some more records from your database */
+        Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+        if (iterator == null) {
+            System.out.println("NewFileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
+            iterator = datestampMap.entrySet().iterator();
+            for (int i = 0; i<oldCount; ++i)
+                iterator.next();
+        }
+        
+        /* load the records ArrayLists. */
+        int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+        while (count < maxListSize && iterator.hasNext()) {
+            Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String path = (String)entryDateMap.getKey();
+            if (setIdentifiers == null || setIdentifiers.contains(path)) {
+                try {
+                    Document nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                    count++;
+                } catch (CannotDisseminateFormatException e) {
+                    /* the client hacked the resumptionToken beyond repair */
+                    throw new BadResumptionTokenException();
+//                } catch (IOException e) {
+//                    /* the file is probably missing */
+//                    throw new BadResumptionTokenException();
+                }
+            }
+        }
+        
+        /* decide if you're done. */
+        if (iterator.hasNext()) {
+            resumptionId = getRSName();
+            resumptionResults.put(resumptionId, iterator);
+            
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(oldCount + count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(numRows));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(set);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                    numRows,
+                    oldCount));
+            //          listRecordsMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSb.toString()));
+        }
+        
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+    
+    
+    public Map listSets() throws NoSetHierarchyException {
+//      throw new NoSetHierarchyException();
+        if (sets.size() == 0)
+            throw new NoSetHierarchyException();
+        Map listSetsMap = new LinkedHashMap();
+        listSetsMap.put("sets", sets.iterator());
+        return listSetsMap;
+//      ArrayList result = new ArrayList();
+//      Iterator i = sets.values().iterator();
+//      while (i.hasNext()) {
+//      String value = (String)i.next();
+//      result.add(value);
+//      }
+//      Map listSetsMap = new HashMap();
+//      listSetsMap.put("sets", result.iterator());
+//      return listSetsMap;
+    }
+    
+    
+    public Map listSets(String resumptionToken)
+    throws BadResumptionTokenException {
+        throw new BadResumptionTokenException();
+    }
+    
+    
+    /**
+     * close the repository
+     */
+    public void close() { }
+    
+    
+    /**
+     * Purge tokens that are older than the time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date      then, now = new Date();
+        Iterator  keySet = resumptionResults.keySet().iterator();
+        String    key;
+        
+        while (keySet.hasNext()) {
+            key=(String)keySet.next();
+            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+    
+    
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a long integer version of the current time
+     */
+    private synchronized static String getRSName() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/FolderRecordFactory.java b/src/ORG/oclc/oai/server/catalog/FolderRecordFactory.java
new file mode 100644
index 0000000..a18ba17
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/FolderRecordFactory.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import javax.xml.transform.TransformerException;
+
+import org.apache.xpath.XPathAPI;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.traversal.NodeIterator;
+
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * NewFileRecordFactory converts native XML "items" to "record" Strings.
+ * This factory assumes the native XML item looks exactly like the <record>
+ * element of an OAI GetRecord response, with the possible exception that the
+ * <metadata> element contains multiple metadataFormats from which to choose.
+ */
+public class FolderRecordFactory extends RecordFactory {
+    private String repositoryIdentifier = null;
+    
+    /**
+     * Construct an NewFileRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public FolderRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+	repositoryIdentifier = properties.getProperty("NewFileRecordFactory.repositoryIdentifier");
+	if (repositoryIdentifier == null) {
+	    throw new IllegalArgumentException("NewFileRecordFactory.repositoryIdentifier is missing from the properties file");
+	}
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+        try {
+            StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
+            tokenizer.nextToken();
+            tokenizer.nextToken();
+            return tokenizer.nextToken();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+        Document doc = (Document)nativeItem;
+        try {
+            return XPathAPI.eval(doc, "record/header/identifier").str();
+        } catch (TransformerException e) {
+            return e.getMessage();
+        }
+//	StringBuffer sb = new StringBuffer();
+//	sb.append("oai:");
+//	sb.append(repositoryIdentifier);
+//	sb.append(":");
+//	sb.append(getLocalIdentifier(nativeItem));
+//	return sb.toString();
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+        return "foo";
+//        return (String)((Document)nativeItem).get("localIdentifier");
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException  {
+        Document doc = (Document)nativeItem;
+        try {
+            return XPathAPI.eval(doc, "/record/header/datestamp").str();
+        } catch (TransformerException e) {
+            return e.getMessage();
+        }
+//        return (String)((HashMap)nativeItem).get("lastModified");
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+        ArrayList list = new ArrayList();
+        Document doc = (Document) nativeItem;
+        try {
+            NodeIterator iter = XPathAPI.selectNodeIterator(doc, "/record/header/setSpec");
+            Node node = null;
+            while ((node = iter.nextNode()) != null) {
+                list.add(XPathAPI.eval(node, ".").str());
+            }
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            return null;
+        }
+        return list.iterator();
+//        return (Iterator)((HashMap)nativeItem).get("setSpecs");
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+        return null;
+//	Document doc = (Document)nativeItem;
+//            String result = new String((byte[])((HashMap)nativeItem).get("recordBytes"), "UTF-8");
+//            if (result.startsWith("<?")) {
+//                int offset = result.indexOf("?>");
+//                result = result.substring(offset+2);
+//            }
+//            return result;
+//        try {
+//            return OAIUtil.toString(doc);
+//        } catch (TransformerException e) {
+//            // TODO Auto-generated catch block
+//            e.printStackTrace();
+//            return null;
+//        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/IMSXMLRecordFactory.java b/src/ORG/oclc/oai/server/catalog/IMSXMLRecordFactory.java
new file mode 100644
index 0000000..ac36d58
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/IMSXMLRecordFactory.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+/**
+ * IMSXMLRecordFactory converts native XML items to Record objects and vice versa.
+ * This factory assumes the native XML item looks exactly like the <record>
+ * element of an OAI GetRecord response, with the possible exception that the
+ * <metadata> element contains multiple metadataFormats from which to choose.
+ */
+public class IMSXMLRecordFactory extends RecordFactory {
+    /**
+     * Construct an IMSXMLRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     */
+    public IMSXMLRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier
+     * @return local identifier
+     */
+    public String fromOAIIdentifier(String identifier) {
+	StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
+	tokenizer.nextToken();
+	tokenizer.nextToken();
+	return tokenizer.nextToken();
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem)
+	throws IllegalArgumentException  {
+	throw new IllegalArgumentException("Identifier isn't available in native item");
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException  {
+	throw new IllegalArgumentException("Datestamp isn't available in native item");
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+	throw new IllegalArgumentException("SetSpecs aren't available in native item");
+    }
+
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	throw new IllegalArgumentException("IsDeleted isn't available in native item");
+    }
+
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataFormat) {
+	// Can't do quickCreates?
+	return null;
+    }
+
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	throw new IllegalArgumentException("getAbouts isn't available in native item");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/JDBCLimitedOAICatalog.java b/src/ORG/oclc/oai/server/catalog/JDBCLimitedOAICatalog.java
new file mode 100644
index 0000000..27a664f
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/JDBCLimitedOAICatalog.java
@@ -0,0 +1,1394 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * JDBCLimitedOAICatalog is an example of how to implement the AbstractCatalog interface
+ * for JDBC. Pattern an implementation of the AbstractCatalog interface after this class
+ * to have OAICat work with your JDBC database.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ * @deprecated Use ExtendedJDBCOAICatalog instead
+ */
+public class JDBCLimitedOAICatalog extends AbstractCatalog {
+    private static final boolean debug = false;
+
+    /**
+     * SQL identifier query (loaded from properties)
+     * \\i -> localIdentifier, \\o -> oaiIdentifier
+     */
+    private String identifierQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until
+     */
+    private String rangeQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until, \\s -> set
+     */
+    private String rangeSetQuery = null;
+
+    /**
+     * SQL query to get a list of available sets
+     */
+    private String setQuery = null;
+
+    /**
+     * SQL query to get a list of available sets that apply to a particular identifier
+     */
+    private String setSpecQuery = null;
+
+    /**
+     * SQL query to get a list of available abouts that apply to a particular identifier
+     */
+    private String aboutQuery = null;
+
+    /**
+     * SQL column labels containing the values of particular interest
+     */
+    private String aboutValueLabel = null;
+    private String setSpecItemLabel = null;
+    private String setSpecListLabel = null;
+    private String setNameLabel = null;
+    private String setDescriptionLabel = null;
+
+    /**
+     * maximum number of entries to return for ListRecords and ListIdentifiers
+     * (loaded from properties)
+     */
+    private int maxListSize;
+
+    /**
+     * The format required for dates in SQL queries
+     * "UTC" = YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ
+     * other = YYYY/MM/DD
+     */
+    private String dateFormat = null;
+
+    /**
+     * Set Strings to be loaded from the properties file
+     * (if they are to be loaded from properties rather than queried from the database)
+     */
+    ArrayList sets = new ArrayList();
+
+    /**
+     * The JDBC Connection
+     */
+    private boolean isPersistentConnection = true;
+    private Connection persistentConnection;
+    private String jdbcURL = null;
+    private String jdbcLogin = null;
+    private String jdbcPasswd = null;
+
+    /**
+     * pending resumption tokens
+     */
+    private HashMap resumptionResults = new HashMap();
+
+    /**
+     * Construct a JDBCLimitedOAICatalog object
+     *
+     * @param properties a properties object containing initialization parameters
+     * @exception IOException an I/O error occurred during database initialization.
+     */
+    public JDBCLimitedOAICatalog(Properties properties) throws IOException {
+        dateFormat = properties.getProperty("JDBCOAICatalog.dateFormat");
+        String maxListSize = properties.getProperty("JDBCLimitedOAICatalog.maxListSize");
+        if (maxListSize == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.maxListSize is missing from the properties file");
+        } else {
+            this.maxListSize = Integer.parseInt(maxListSize);
+        }
+
+        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
+        if (paramRegex != null) {
+            setParamRegex(paramRegex);
+        }
+        String jdbcDriverName = properties.getProperty("JDBCLimitedOAICatalog.jdbcDriverName");
+        if (jdbcDriverName == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcDriverName is missing from the properties file");
+        }
+        jdbcURL = properties.getProperty("JDBCLimitedOAICatalog.jdbcURL");
+        if (jdbcURL == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcURL is missing from the properties file");
+        }
+
+        jdbcLogin = properties.getProperty("JDBCLimitedOAICatalog.jdbcLogin");
+        if (jdbcLogin == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcLogin is missing from the properties file");
+        }
+
+        jdbcPasswd = properties.getProperty("JDBCLimitedOAICatalog.jdbcPasswd");
+        if (jdbcPasswd == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcPasswd is missing from the properties file");
+        }
+
+        rangeQuery = properties.getProperty("JDBCLimitedOAICatalog.rangeQuery");
+        if (rangeQuery == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.rangeQuery is missing from the properties file");
+        }
+
+        rangeSetQuery = properties.getProperty("JDBCLimitedOAICatalog.rangeSetQuery");
+        if (rangeSetQuery == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.rangeSetQuery is missing from the properties file");
+        }
+
+        identifierQuery = properties.getProperty("JDBCLimitedOAICatalog.identifierQuery");
+        if (identifierQuery == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.identifierQuery is missing from the properties file");
+        }
+
+        aboutQuery = properties.getProperty("JDBCLimitedOAICatalog.aboutQuery");
+        if (aboutQuery != null) {
+            aboutValueLabel = properties.getProperty("JDBCLimitedOAICatalog.aboutValueLabel");
+            if (aboutValueLabel == null) {
+                throw new IllegalArgumentException("JDBCLimitedOAICatalog.aboutValueLabel is missing from the properties file");
+            }
+        }
+
+        setSpecQuery = properties.getProperty("JDBCLimitedOAICatalog.setSpecQuery");
+        setSpecItemLabel = properties.getProperty("JDBCLimitedOAICatalog.setSpecItemLabel");
+        if (setSpecQuery != null && setSpecItemLabel == null) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.setSpecItemLabel is missing from the properties file");
+        }
+        setDescriptionLabel = properties.getProperty("JDBCLimitedOAICatalog.setDescriptionLabel");
+//      if (setDescriptionLabel == null) {
+//      throw new IllegalArgumentException("JDBCLimitedOAICatalog.setDescriptionLabel is missing from the properties file");
+//      }
+
+        // See if a setQuery exists
+        setQuery = properties.getProperty("JDBCLimitedOAICatalog.setQuery");
+        if (setQuery == null) {
+            // if not, load the set Strings from the properties file (if present)
+            String propertyPrefix = "Sets.";
+            Enumeration propNames = properties.propertyNames();
+            while (propNames.hasMoreElements()) {
+                String propertyName = (String)propNames.nextElement();
+                if (propertyName.startsWith(propertyPrefix)) {
+                    sets.add(properties.get(propertyName));
+                }
+            }
+        } else {
+            setSpecListLabel = properties.getProperty("JDBCLimitedOAICatalog.setSpecListLabel");
+            if (setSpecListLabel == null) {
+                throw new IllegalArgumentException("JDBCLimitedOAICatalog.setSpecListLabel is missing from the properties file");
+            }
+            setNameLabel = properties.getProperty("JDBCLimitedOAICatalog.setNameLabel");
+            if (setNameLabel == null) {
+                throw new IllegalArgumentException("JDBCLimitedOAICatalog.setNameLabel is missing from the properties file");
+            }
+        }
+
+        String temp = properties.getProperty("JDBCLimitedOAICatalog.isPersistentConnection");
+        if ("false".equalsIgnoreCase(temp))
+            isPersistentConnection = false;
+
+        // open the connection
+        try {
+            Class.forName(jdbcDriverName);
+        } catch (ClassNotFoundException e) {
+            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcDriverName is invalid: "
+                    + jdbcDriverName);
+        }
+
+        if (isPersistentConnection) {
+            try {
+                persistentConnection = getNewConnection();
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new IOException(e.getMessage());
+            }
+        }
+    }
+
+    private Connection getNewConnection() throws SQLException {
+        // open the connection
+        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
+    }
+
+    private Connection startConnection() throws SQLException {
+        if (persistentConnection != null) {
+            if (persistentConnection.isClosed())
+                persistentConnection = getNewConnection();
+            return persistentConnection;
+        } else {
+            return getNewConnection();
+        }
+    }
+
+    private void endConnection(Connection con) throws OAIInternalServerError {
+        try {
+            if (persistentConnection == null)
+                con.close();
+        } catch (SQLException e) {
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
+     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
+     * but the item is flagged as deleted and thus no schemaLocations (i.e.
+     * metadataFormats) can be produced.
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
+        Connection con = null;
+        try {
+            con = startConnection();
+            Statement stmt = con.createStatement();
+            ResultSet rs =
+                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
+            /*
+             * Let your recordFactory decide which schemaLocations
+             * (i.e. metadataFormats) it can produce from the record.
+             * Doing so will preserve the separation of database access
+             * (which happens here) from the record content interpretation
+             * (which is the responsibility of the RecordFactory implementation).
+             */
+            if (!rs.next()) {
+                endConnection(con);
+                throw new IdDoesNotExistException(oaiIdentifier);
+            } else {
+                /* Make sure the identifierQuery returns the columns you need
+                 * (if any) to determine the supported schemaLocations for this item */
+                HashMap nativeItem = getColumnValues(rs);
+                endConnection(con);
+                return getRecordFactory().getSchemaLocations(nativeItem);
+            }
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Since the columns should only be read once, copy them into a
+     * HashMap and consider that to be the "record"
+     * @param rs The ResultSet row
+     * @return a HashMap mapping column names with values
+     */
+    private HashMap getColumnValues(ResultSet rs)
+    throws SQLException {
+        ResultSetMetaData mdata = rs.getMetaData();
+        int count = mdata.getColumnCount();
+        HashMap nativeItem = new HashMap(count);
+        for (int i=1; i<=count; ++i) {
+            String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
+            nativeItem.put(fieldName, rs.getObject(i));
+            if (debug) System.out.println(fieldName + "=" + nativeItem.get(fieldName));
+//          if (debug) {
+//          System.out.println("JDBCLimitedOAICatalog.getColumnValues");
+//          System.out.println("catalogName=" + mdata.getCatalogName(i));
+//          System.out.println("columnLabel=" + mdata.getColumnLabel(i));
+//          System.out.println("columnName=" + mdata.getColumnName(i));
+//          System.out.println("schemaName=" + mdata.getSchemaName(i));
+//          System.out.println("tableName=" + mdata.getTableName(i));
+//          }
+        }
+        return nativeItem;
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the rangeQuery String
+     * NOTE! This retrieves an extra record so we can decide if EOF has been reached.
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateRangeQuery(String from, String until, String set, int offset, int count)
+    throws OAIInternalServerError {
+        StringBuffer sb = new StringBuffer();
+        StringTokenizer tokenizer;
+        if (set == null || set.length() == 0)
+            tokenizer = new StringTokenizer(rangeQuery, "\\");
+        else
+            tokenizer = new StringTokenizer(rangeSetQuery, "\\");
+
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid query");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'a':
+                sb.append(Integer.toString(offset));
+                break;
+            case 'b':
+//              sb.append(Integer.toString(count));
+                sb.append(Integer.toString(count+1)); // grab an extra record to decide if EOF
+                break;
+            case 'f':  // what are the chances someone would use this to indicate a form feed?
+                sb.append(formatFromDate(from));
+                break;
+            case 'u':
+                sb.append(formatUntilDate(until));
+                break;
+            case 's':
+                sb.append(set);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatFromDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatUntilDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Change the String from UTC to SQL format
+     * If this method doesn't suit your needs, extend this class and override
+     * the method rather than change this code directly.
+     */
+    protected String formatDate(String date) {
+        if ("UTC".equals(dateFormat)) {
+            return date;
+        } else {
+            StringBuffer sb = new StringBuffer();
+            sb.append(date.substring(5, 7));
+            sb.append("/");
+            sb.append(date.substring(8));
+            sb.append("/");
+            sb.append(date.substring(0, 4));
+            if (debug) System.out.println("JDBCLimitedOAICatalog.formatDate: from " + date + " to " + sb.toString());
+            return sb.toString();
+        }
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateIdentifierQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateSetSpecQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateAboutQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Retrieve a list of identifiers that satisfy the specified criteria
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * It may seem strange for the map to include both "headers" and "identifiers"
+     * since the identifiers can be obtained from the headers. This may be true, but
+     * AbstractCatalog.listRecords() can operate quicker if it doesn't
+     * need to parse identifiers from the XML headers itself. Better
+     * still, do like I do below and override AbstractCatalog.listRecords().
+     * AbstractCatalog.listRecords() is relatively inefficient because given the list
+     * of identifiers, it must call getRecord() individually for each as it constructs
+     * its response. It's much more efficient to construct the entire response in one fell
+     * swoop by overriding listRecords() as I've done here.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+    throws NoItemsMatchException, OAIInternalServerError {
+//      purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        Connection con = null;
+
+        try {
+            con = startConnection();
+            /* Get some records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
+//          rs.last();
+//          int numRows = rs.getRow();
+//          rs.beforeFirst();
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count=0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = getColumnValues(rs);
+                /* Use the RecordFactory to extract header/identifier pairs for each item */
+//                RecordFactory rf = getRecordFactory();
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            if (count == 0) {
+                endConnection(con);
+                throw new NoItemsMatchException();
+            }
+
+            /* decide if you're done */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+//              String resumptionId = getResumptionId();
+//              resumptionResults.put(resumptionId, rs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append(":");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append(":");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        0));
+                //          listIdentifiersMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+                endConnection(con);
+            }
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+//      purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+
+        /****************************
+         * parse your resumptionToken
+         ****************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+//      String resumptionId;
+        String from;
+        String until;
+        String set;
+        int oldCount;
+        String metadataPrefix;
+//      int numRows;
+        try {
+//          resumptionId = tokenizer.nextToken();
+            from = tokenizer.nextToken();
+            until = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+            if (set.equals("."))
+                set = null;
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+//          numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            if (debug) {
+                System.out.println("JDBCLimitedOAICatalog.listIdentifiers: set=>"
+                        + set + "<");
+            }
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        Connection con = null;
+        try {
+            con = startConnection();
+            /* Get some more records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
+                    oldCount, maxListSize));
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = getColumnValues(rs);
+                /* Use the RecordFactory to extract header/identifier pairs for each item */
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            /* decide if you're done. */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append(":");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append(":");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        oldCount));
+                //          listIdentifiersMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+                endConnection(con);
+            }
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return the <record/> portion of the XML response.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix is not
+     * supported by the item.
+     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+    throws OAIInternalServerError, CannotDisseminateFormatException,
+    IdDoesNotExistException {
+        Connection con = null;
+        try {
+            con = startConnection();
+            Statement stmt = con.createStatement();
+            ResultSet rs =
+                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
+            if (!rs.next()) {
+                endConnection(con);
+                throw new IdDoesNotExistException(oaiIdentifier);
+            }
+            HashMap nativeItem = getColumnValues(rs);
+            endConnection(con);
+            return constructRecord(nativeItem, metadataPrefix);
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria. Note, though,
+     * that unlike the other OAI verb type methods implemented here, both of the
+     * listRecords methods are already implemented in AbstractCatalog rather than
+     * abstracted. This is because it is possible to implement ListRecords as a
+     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
+     * I suggest that you override both the AbstractCatalog.listRecords methods
+     * here since it will probably improve the performance if you create the
+     * response in one fell swoop rather than construct it one GetRecord at a time.
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for a "records" Iterator object
+     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix isn't
+     * supported by the item.
+     */
+    public Map listRecords(String from, String until, String set, String metadataPrefix)
+    throws CannotDisseminateFormatException, NoItemsMatchException,
+    OAIInternalServerError {
+//      purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        Connection con = null;
+
+        try {
+            con = startConnection();
+            /* Get some records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
+                    ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
+//          rs.last();
+//          int numRows = rs.getRow();
+//          rs.beforeFirst();
+            int count;
+
+//          if (debug) System.out.println("JDBCLimitedOAICatalog.listRecords: numRows=" + numRows);
+
+            /* load the records ArrayList */
+            for (count=0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = getColumnValues(rs);
+                String record = constructRecord(nativeItem, metadataPrefix);
+                records.add(record);
+            }
+
+            if (count == 0) {
+                endConnection(con);
+                throw new NoItemsMatchException();
+            }
+
+            /* decide if you're done */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+//              String resumptionId = getResumptionId();
+//              resumptionResults.put(resumptionId, rs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append(":");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append(":");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append(":");
+//              resumptionTokenSb.append(Integer.toString(numRows));
+//              resumptionTokenSb.append(":");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        0));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
+                endConnection(con);
+            }
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception BadResumptionTokenException the value of the resumptionToken argument
+     * is invalid or expired.
+     */
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+//      purge(); // clean out old resumptionTokens
+
+        /****************************
+         * parse your resumptionToken
+         ****************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+//      String resumptionId;
+        String from;
+        String until;
+        String set;
+        int oldCount;
+//      int numRows;
+        String metadataPrefix;
+        try {
+//          resumptionId = tokenizer.nextToken();
+            from = tokenizer.nextToken();
+            until = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+            if (set.equals("."))
+                set = null;
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+//          numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        Connection con = null;
+        try {
+            con = startConnection();
+            /* Get some more records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
+                    oldCount, maxListSize));
+
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && rs.next(); ++count) {
+                try {
+                    HashMap nativeItem = getColumnValues(rs);
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                } catch (CannotDisseminateFormatException e) {
+                    /* the client hacked the resumptionToken beyond repair */
+                    endConnection(con);
+                    throw new BadResumptionTokenException();
+                }
+            }
+
+            /* decide if you're done */
+//          if (count == maxListSize) {
+            if (rs.next()) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+//              resumptionTokenSb.append(resumptionId);
+//              resumptionTokenSb.append(":");
+                resumptionTokenSb.append(from);
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(until);
+                resumptionTokenSb.append(":");
+                if (set == null)
+                    resumptionTokenSb.append(".");
+                else
+                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
+                resumptionTokenSb.append(":");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append(":");
+//              resumptionTokenSb.append(Integer.toString(numRows));
+//              resumptionTokenSb.append(":");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        -1,
+                        oldCount));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+                endConnection(con);
+            }
+        } catch (UnsupportedEncodingException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(HashMap nativeItem, String metadataPrefix)
+    throws CannotDisseminateFormatException, OAIInternalServerError {
+        String schemaURL = null;
+        Iterator setSpecs = getSetSpecs(nativeItem);
+        Iterator abouts = getAbouts(nativeItem);
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * Retrieve a list of sets that satisfy the specified criteria
+     *
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception OAIBadRequestException signals an http status code 400 problem
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
+        if (setQuery == null) {
+            if (sets.size() == 0)
+                throw new NoSetHierarchyException();
+            Map listSetsMap = new HashMap();
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            Connection con = null;
+            try {
+                if (debug) System.out.println(setQuery);
+
+                con = startConnection();
+                /* Get some records from your database */
+                Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+                ResultSet rs = stmt.executeQuery(setQuery);
+                rs.last();
+                int numRows = rs.getRow();
+                rs.beforeFirst();
+                int count;
+
+//              if (debug) System.out.println("JDBCLimitedOAICatalog.listSets: numRows=" + numRows);
+
+                /* load the sets ArrayLists. */
+                for (count=0; count < maxListSize && rs.next(); ++count) {
+                    /* Use the RecordFactory to extract header/set pairs for each item */
+                    HashMap nativeItem = getColumnValues(rs);
+                    sets.add(getSetXML(nativeItem));
+                    if (debug) System.out.println("JDBCLimitedOAICatalog.listSets: adding an entry");
+                }
+
+                /* decide if you're done */
+                if (count < numRows) {
+                    String resumptionId = getResumptionId();
+                    resumptionResults.put(resumptionId, rs);
+
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append(":");
+                    resumptionTokenSb.append(Integer.toString(count));
+                    resumptionTokenSb.append(":");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            0));
+                    //          listSetsMap.put("resumptionMap",
+                    //                                 getResumptionMap(resumptionTokenSb.toString()));
+                    endConnection(con);
+                }
+            } catch (SQLException e) {
+                if (con != null)
+                    endConnection(con);
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * Retrieve the next set of sets associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listSets() Map result.
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets(String resumptionToken)
+    throws OAIInternalServerError, BadResumptionTokenException {
+        if (setQuery == null) {
+            throw new BadResumptionTokenException();
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            /****************************
+             * parse your resumptionToken
+             ****************************/
+            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+            String resumptionId;
+            int oldCount;
+            int numRows;
+            try {
+                resumptionId = tokenizer.nextToken();
+                oldCount = Integer.parseInt(tokenizer.nextToken());
+                numRows = Integer.parseInt(tokenizer.nextToken());
+            } catch (NoSuchElementException e) {
+                throw new BadResumptionTokenException();
+            }
+
+            try {
+                /* Get some more records from your database */
+                ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
+                if (rs == null) {
+                    throw new BadResumptionTokenException();
+                }
+
+                if (rs.getRow() != oldCount) {
+//                  System.out.println("JDBCLimitedOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                    rs.absolute(oldCount);
+                }
+
+                int count;
+
+                /* load the sets ArrayLists. */
+                for (count = 0; count < maxListSize && rs.next(); ++count) {
+                    HashMap nativeItem = getColumnValues(rs);
+                    /* Use the RecordFactory to extract set for each item */
+                    sets.add(getSetXML(nativeItem));
+                }
+
+                /* decide if you're done. */
+                if (oldCount+count < numRows) {
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append(":");
+                    resumptionTokenSb.append(Integer.toString(oldCount + count));
+                    resumptionTokenSb.append(":");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            oldCount));
+                    //          listSetsMap.put("resumptionMap",
+                    //                                 getResumptionMap(resumptionTokenSb.toString()));
+                }
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(HashMap nativeItem)
+    throws OAIInternalServerError {
+        Connection con = null;
+        try {
+            ArrayList setSpecs = new ArrayList();
+            if (setSpecQuery != null) {
+                con = startConnection();
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                Statement stmt = con.createStatement();
+                ResultSet rs = stmt.executeQuery(populateSetSpecQuery(oaiIdentifier));
+                while (rs.next()) {
+                    HashMap setMap = getColumnValues(rs);
+                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
+                }
+                endConnection(con);
+            }
+            return setSpecs.iterator();
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(HashMap nativeItem)
+    throws OAIInternalServerError {
+        Connection con = null;
+        try {
+            ArrayList abouts = new ArrayList();
+            if (aboutQuery != null) {
+                con = startConnection();
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                Statement stmt = con.createStatement();
+                ResultSet rs = stmt.executeQuery(populateAboutQuery(oaiIdentifier));
+                while (rs.next()) {
+                    HashMap aboutMap = getColumnValues(rs);
+                    abouts.add(aboutMap.get(aboutValueLabel));
+                }
+                endConnection(con);
+            }
+            return abouts.iterator();
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Extract &lt;set&gt; XML string from setItem object
+     *
+     * @param setItem individual set instance in native format
+     * @return an XML String containing the XML &lt;set&gt; content
+     */
+    public String getSetXML(HashMap setItem)
+    throws IllegalArgumentException {
+//      try {
+        String setSpec = getSetSpec(setItem);
+        String setName = getSetName(setItem);
+        String setDescription = getSetDescription(setItem);
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("<set>");
+        sb.append("<setSpec>");
+        sb.append(OAIUtil.xmlEncode(setSpec));
+        sb.append("</setSpec>");
+        sb.append("<setName>");
+        sb.append(OAIUtil.xmlEncode(setName));
+        sb.append("</setName>");
+        if (setDescription != null) {
+            sb.append("<setDescription>");
+            sb.append(OAIUtil.xmlEncode(setDescription));
+            sb.append("</setDescription>");
+        }
+        sb.append("</set>");
+        return sb.toString();
+//      } catch (SQLException e) {
+//      e.printStackTrace();
+//      throw new IllegalArgumentException(e.getMessage());
+//      }
+    }
+
+    /**
+     * get the setSpec XML string. Extend this class and override this method
+     * if the setSpec can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setSpec&gt; content
+     */
+    protected String getSetSpec(HashMap setItem) {
+        try {
+            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+            return "UnsupportedEncodingException";
+        }
+    }
+
+    /**
+     * get the setName XML string. Extend this class and override this method
+     * if the setName can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setName&gt; content
+     */
+    protected String getSetName(HashMap setItem) {
+        return (String)setItem.get(setNameLabel);
+    }
+
+    /**
+     * get the setDescription XML string. Extend this class and override this method
+     * if the setDescription can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setDescription&gt; content
+     */
+    protected String getSetDescription(HashMap setItem) {
+        if (setDescriptionLabel == null)
+            return null;
+        return (String)setItem.get(setDescriptionLabel);
+    }
+
+    /**
+     * close the repository
+     */
+    public void close() {
+        try {
+            if (persistentConnection != null)
+                persistentConnection.close();
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        persistentConnection = null;
+    }
+
+    /**
+     * Purge tokens that are older than the configured time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date now = new Date();
+        Iterator keySet = resumptionResults.keySet().iterator();
+        while (keySet.hasNext()) {
+            String key = (String)keySet.next();
+            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            String key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a String version of the current time
+     */
+    private synchronized static String getResumptionId() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/JDBCOAICatalog.java b/src/ORG/oclc/oai/server/catalog/JDBCOAICatalog.java
new file mode 100644
index 0000000..5042634
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/JDBCOAICatalog.java
@@ -0,0 +1,1324 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ORG.oclc.oai.server.catalog;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * JDBCOAICatalog is an example of how to implement the AbstractCatalog interface
+ * for JDBC. Pattern an implementation of the AbstractCatalog interface after this class
+ * to have OAICat work with your JDBC database.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ * @deprecated Use ExtendedJDBCOAICatalog instead
+ */
+public class JDBCOAICatalog extends AbstractCatalog {
+    private static final boolean debug = false;
+
+    /**
+     * SQL identifier query (loaded from properties)
+     * \\i -> localIdentifier, \\o -> oaiIdentifier
+     */
+    private String identifierQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until
+     */
+    private String rangeQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until, \\s -> set
+     */
+    private String rangeSetQuery = null;
+
+    /**
+     * SQL query to get a list of available sets
+     */
+    private String setQuery = null;
+
+    /**
+     * SQL query to get a list of available sets that apply to a particular identifier
+     */
+    private String setSpecQuery = null;
+
+    /**
+     * SQL query to get a list of available abouts that apply to a particular identifier
+     */
+    private String aboutQuery = null;
+
+    /**
+     * SQL column labels containing the values of particular interest
+     */
+    private String aboutValueLabel = null;
+    private String setSpecItemLabel = null;
+    private String setSpecListLabel = null;
+    private String setNameLabel = null;
+    private String setDescriptionLabel = null;
+
+    /**
+     * maximum number of entries to return for ListRecords and ListIdentifiers
+     * (loaded from properties)
+     */
+    private int maxListSize;
+
+    /**
+     * The format required for dates in SQL queries
+     * "UTC" = YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ
+     * other = YYYY/MM/DD
+     */
+    private String dateFormat = null;
+
+    /**
+     * Set Strings to be loaded from the properties file
+     * (if they are to be loaded from properties rather than queried from the database)
+     */
+    ArrayList sets = new ArrayList();
+
+    /**
+     * Assume the connection will be persistent unless configured otherwise
+     */
+    private boolean isPersistentConnection = true;
+
+    /**
+     * The JDBC Connection
+     */
+    private Connection persistentConnection = null;
+
+    /**
+     * pending resumption tokens
+     */
+    private HashMap resumptionResults = new HashMap();
+    private String jdbcURL = null;
+    private String jdbcLogin = null;
+    private String jdbcPasswd = null;
+
+    /**
+     * Construct a JDBCOAICatalog object
+     *
+     * @param properties a properties object containing initialization parameters
+     * @exception IOException an I/O error occurred during database initialization.
+     */
+    public JDBCOAICatalog(Properties properties) throws IOException {
+        dateFormat = properties.getProperty("JDBCOAICatalog.dateFormat");
+        String maxListSize = properties.getProperty("JDBCOAICatalog.maxListSize");
+        if (maxListSize == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.maxListSize is missing from the properties file");
+        } else {
+            this.maxListSize = Integer.parseInt(maxListSize);
+        }
+
+        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
+        if (paramRegex != null) {
+            setParamRegex(paramRegex);
+        }
+        String jdbcDriverName = properties.getProperty("JDBCOAICatalog.jdbcDriverName");
+        if (jdbcDriverName == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is missing from the properties file");
+        }
+        jdbcURL = properties.getProperty("JDBCOAICatalog.jdbcURL");
+        if (jdbcURL == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcURL is missing from the properties file");
+        }
+
+        jdbcLogin = properties.getProperty("JDBCOAICatalog.jdbcLogin");
+        if (jdbcLogin == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcLogin is missing from the properties file");
+        }
+
+        jdbcPasswd = properties.getProperty("JDBCOAICatalog.jdbcPasswd");
+        if (jdbcPasswd == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcPasswd is missing from the properties file");
+        }
+
+        rangeQuery = properties.getProperty("JDBCOAICatalog.rangeQuery");
+        if (rangeQuery == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.rangeQuery is missing from the properties file");
+        }
+
+        rangeSetQuery = properties.getProperty("JDBCOAICatalog.rangeSetQuery");
+        if (rangeSetQuery == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.rangeSetQuery is missing from the properties file");
+        }
+
+        identifierQuery = properties.getProperty("JDBCOAICatalog.identifierQuery");
+        if (identifierQuery == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.identifierQuery is missing from the properties file");
+        }
+
+        aboutQuery = properties.getProperty("JDBCOAICatalog.aboutQuery");
+        if (aboutQuery != null) {
+            aboutValueLabel = properties.getProperty("JDBCOAICatalog.aboutValueLabel");
+            if (aboutValueLabel == null) {
+                throw new IllegalArgumentException("JDBCOAICatalog.aboutValueLabel is missing from the properties file");
+            }
+        }
+
+        setSpecQuery = properties.getProperty("JDBCOAICatalog.setSpecQuery");
+        setSpecItemLabel = properties.getProperty("JDBCOAICatalog.setSpecItemLabel");
+        if (setSpecItemLabel == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.setSpecItemLabel is missing from the properties file");
+        }
+        setSpecListLabel = properties.getProperty("JDBCOAICatalog.setSpecListLabel");
+        if (setSpecListLabel == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.setSpecListLabel is missing from the properties file");
+        }
+        setNameLabel = properties.getProperty("JDBCOAICatalog.setNameLabel");
+        if (setNameLabel == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.setNameLabel is missing from the properties file");
+        }
+
+        String temp = properties.getProperty("JDBCOAICatalog.isPersistentConnection");
+        if ("false".equalsIgnoreCase(temp)) {
+            isPersistentConnection = false;
+        }
+
+        setDescriptionLabel = properties.getProperty("JDBCOAICatalog.setDescriptionLabel");
+//      if (setDescriptionLabel == null) {
+//      throw new IllegalArgumentException("JDBCOAICatalog.setDescriptionLabel is missing from the properties file");
+//      }
+
+        // See if a setQuery exists
+        setQuery = properties.getProperty("JDBCOAICatalog.setQuery");
+        if (setQuery == null) {
+            // if not, load the set Strings from the properties file (if present)
+            String propertyPrefix = "Sets.";
+            Enumeration propNames = properties.propertyNames();
+            while (propNames.hasMoreElements()) {
+                String propertyName = (String)propNames.nextElement();
+                if (propertyName.startsWith(propertyPrefix)) {
+                    sets.add(properties.get(propertyName));
+                }
+            }
+        }
+
+        try {
+            Class.forName(jdbcDriverName);
+        } catch (ClassNotFoundException e) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is invalid: "
+                    + jdbcDriverName);
+        }
+
+        if (isPersistentConnection) {
+            try {
+                persistentConnection = getNewConnection();
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new IOException(e.getMessage());
+            }
+        }
+    }
+
+    private Connection getNewConnection() throws SQLException {
+        // open the connection
+        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
+    }
+
+    private Connection startConnection() throws SQLException {
+        if (persistentConnection != null) {
+            if (persistentConnection.isClosed()) {
+                System.out.println("Persistent connection has expired.");
+                persistentConnection = getNewConnection();
+            }
+            return persistentConnection;
+        } else {
+            return getNewConnection();
+        }
+    }
+
+    private void endConnection(Connection con) throws OAIInternalServerError {
+        if (persistentConnection == null) {
+            try {
+                if (debug) System.out.println("ending connection");
+                con.close();
+            } catch (SQLException e) {
+                throw new OAIInternalServerError(e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
+     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
+     * but the item is flagged as deleted and thus no schemaLocations (i.e.
+     * metadataFormats) can be produced.
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
+        Connection con = null;
+        try {
+            con = startConnection();
+            Statement stmt = con.createStatement();
+            ResultSet rs =
+                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
+            /*
+             * Let your recordFactory decide which schemaLocations
+             * (i.e. metadataFormats) it can produce from the record.
+             * Doing so will preserve the separation of database access
+             * (which happens here) from the record content interpretation
+             * (which is the responsibility of the RecordFactory implementation).
+             */
+            if (!rs.next()) {
+                endConnection(con);
+                throw new IdDoesNotExistException(oaiIdentifier);
+            } else {
+                /* Make sure the identifierQuery returns the columns you need
+                 * (if any) to determine the supported schemaLocations for this item */
+                HashMap nativeItem = getColumnValues(rs);
+                endConnection(con);
+                return getRecordFactory().getSchemaLocations(nativeItem);
+            }
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Since the columns should only be read once, copy them into a
+     * HashMap and consider that to be the "record"
+     * @param rs The ResultSet row
+     * @return a HashMap mapping column names with values
+     */
+    private HashMap getColumnValues(ResultSet rs)
+    throws SQLException {
+        ResultSetMetaData mdata = rs.getMetaData();
+        int count = mdata.getColumnCount();
+        HashMap nativeItem = new HashMap(count);
+        for (int i=1; i<=count; ++i) {
+            String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
+            nativeItem.put(fieldName, rs.getObject(i));
+            if (debug) System.out.println(fieldName + "=" + nativeItem.get(fieldName));
+//          if (debug) {
+//          System.out.println("JDBCOAICatalog.getColumnValues");
+//          System.out.println("catalogName=" + mdata.getCatalogName(i));
+//          System.out.println("columnLabel=" + mdata.getColumnLabel(i));
+//          System.out.println("columnName=" + mdata.getColumnName(i));
+//          System.out.println("schemaName=" + mdata.getSchemaName(i));
+//          System.out.println("tableName=" + mdata.getTableName(i));
+//          }
+        }
+        return nativeItem;
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the rangeQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateRangeQuery(String from, String until, String set)
+    throws OAIInternalServerError {
+        StringBuffer sb = new StringBuffer();
+        StringTokenizer tokenizer;
+        if (set == null || set.length() == 0)
+            tokenizer = new StringTokenizer(rangeQuery, "\\");
+        else
+            tokenizer = new StringTokenizer(rangeSetQuery, "\\");
+
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid query");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'f':  // what are the chances someone would use this to indicate a form feed?
+                sb.append(formatFromDate(from));
+                break;
+            case 'u':
+                sb.append(formatUntilDate(until));
+                break;
+            case 's':
+                sb.append(set);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatFromDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatUntilDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Change the String from UTC to SQL format
+     * If this method doesn't suit your needs, extend this class and override
+     * the method rather than change this code directly.
+     */
+    protected String formatDate(String date) {
+        if ("UTC".equals(dateFormat)) {
+            return date;
+        } else {
+            StringBuffer sb = new StringBuffer();
+            sb.append(date.substring(5, 7));
+            sb.append("/");
+            sb.append(date.substring(8));
+            sb.append("/");
+            sb.append(date.substring(0, 4));
+            if (debug) System.out.println("JDBCOAICatalog.formatDate: from " + date + " to " + sb.toString());
+            return sb.toString();
+        }
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateIdentifierQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateSetSpecQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateAboutQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Retrieve a list of identifiers that satisfy the specified criteria
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * It may seem strange for the map to include both "headers" and "identifiers"
+     * since the identifiers can be obtained from the headers. This may be true, but
+     * AbstractCatalog.listRecords() can operate quicker if it doesn't
+     * need to parse identifiers from the XML headers itself. Better
+     * still, do like I do below and override AbstractCatalog.listRecords().
+     * AbstractCatalog.listRecords() is relatively inefficient because given the list
+     * of identifiers, it must call getRecord() individually for each as it constructs
+     * its response. It's much more efficient to construct the entire response in one fell
+     * swoop by overriding listRecords() as I've done here.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+    throws NoItemsMatchException, OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        Connection con = null;
+
+        try {
+            con = startConnection();
+            /* Get some records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set));
+            rs.last();
+            int numRows = rs.getRow();
+            if (numRows == 0) {
+                endConnection(con);
+                throw new NoItemsMatchException();
+            }
+            rs.beforeFirst();
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count=0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = getColumnValues(rs);
+                /* Use the RecordFactory to extract header/identifier pairs for each item */
+//                RecordFactory rf = getRecordFactory();
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            /* decide if you're done */
+            if (count < numRows) {
+                String resumptionId = getResumptionId();
+                resumptionResults.put(resumptionId, rs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        0));
+                //          listIdentifiersMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+            }
+            endConnection(con);
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+        int numRows;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        try {
+            /* Get some more records from your database */
+            ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
+            if (rs == null) {
+                throw new BadResumptionTokenException();
+            }
+            int count;
+
+            if (rs.getRow() != oldCount) {
+//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                rs.absolute(oldCount);
+            }
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = getColumnValues(rs);
+                /* Use the RecordFactory to extract header/identifier pairs for each item */
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            /* decide if you're done. */
+            if (oldCount+count < numRows) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        oldCount));
+                //          listIdentifiersMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return the <record/> portion of the XML response.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix is not
+     * supported by the item.
+     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+    throws OAIInternalServerError, CannotDisseminateFormatException,
+    IdDoesNotExistException {
+        Connection con = null;
+        try {
+            con = startConnection();
+            Statement stmt = con.createStatement();
+            ResultSet rs =
+                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
+            if (!rs.next()) {
+                endConnection(con);
+                throw new IdDoesNotExistException(oaiIdentifier);
+            }
+            HashMap nativeItem = getColumnValues(rs);
+            endConnection(con);
+            return constructRecord(nativeItem, metadataPrefix);
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria. Note, though,
+     * that unlike the other OAI verb type methods implemented here, both of the
+     * listRecords methods are already implemented in AbstractCatalog rather than
+     * abstracted. This is because it is possible to implement ListRecords as a
+     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
+     * I suggest that you override both the AbstractCatalog.listRecords methods
+     * here since it will probably improve the performance if you create the
+     * response in one fell swoop rather than construct it one GetRecord at a time.
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for a "records" Iterator object
+     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix isn't
+     * supported by the item.
+     */
+    public Map listRecords(String from, String until, String set, String metadataPrefix)
+    throws CannotDisseminateFormatException, NoItemsMatchException,
+    OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        Connection con = null;
+
+        try {
+            con = startConnection();
+            /* Get some records from your database */
+            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
+                    ResultSet.CONCUR_READ_ONLY);
+            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set));
+            rs.last();
+            int numRows = rs.getRow();
+            if (numRows == 0) {
+                endConnection(con);
+                throw new NoItemsMatchException();
+            }
+            rs.beforeFirst();
+            int count;
+
+//          if (debug) System.out.println("JDBCOAICatalog.listRecords: numRows=" + numRows);
+
+            /* load the records ArrayList */
+            for (count=0; count < maxListSize && rs.next(); ++count) {
+                HashMap nativeItem = getColumnValues(rs);
+                String record = constructRecord(nativeItem, metadataPrefix);
+                records.add(record);
+            }
+
+            /* decide if you're done */
+            if (count < numRows) {
+                String resumptionId = getResumptionId();
+                resumptionResults.put(resumptionId, rs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        0));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
+                endConnection(con);
+            }
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception BadResumptionTokenException the value of the resumptionToken argument
+     * is invalid or expired.
+     */
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        purge(); // clean out old resumptionTokens
+
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+        String resumptionId;
+        int oldCount;
+        int numRows;
+        String metadataPrefix;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        try {
+            /* Get some more records from your database */
+            ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
+            if (rs == null) {
+                throw new BadResumptionTokenException();
+            }
+
+            if (rs.getRow() != oldCount) {
+//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                rs.absolute(oldCount);
+            }
+
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && rs.next(); ++count) {
+                try {
+                    HashMap nativeItem = getColumnValues(rs);
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                } catch (CannotDisseminateFormatException e) {
+                    /* the client hacked the resumptionToken beyond repair */
+                    throw new BadResumptionTokenException();
+                }
+            }
+
+            /* decide if you're done */
+            if (oldCount+count < numRows) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        oldCount));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(HashMap nativeItem, String metadataPrefix)
+    throws CannotDisseminateFormatException, OAIInternalServerError {
+        String schemaURL = null;
+        Iterator setSpecs = getSetSpecs(nativeItem);
+        Iterator abouts = getAbouts(nativeItem);
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * Retrieve a list of sets that satisfy the specified criteria
+     *
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception OAIBadRequestException signals an http status code 400 problem
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
+        Connection con = null;
+        if (setQuery == null) {
+            if (sets.size() == 0)
+                throw new NoSetHierarchyException();
+            Map listSetsMap = new HashMap();
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            try {
+                con = startConnection();
+                if (debug) System.out.println(setQuery);
+
+                /* Get some records from your database */
+                Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+                ResultSet rs = stmt.executeQuery(setQuery);
+                rs.last();
+                int numRows = rs.getRow();
+                rs.beforeFirst();
+                int count;
+
+//              if (debug) System.out.println("JDBCOAICatalog.listSets: numRows=" + numRows);
+
+                /* load the sets ArrayLists. */
+                for (count=0; count < maxListSize && rs.next(); ++count) {
+                    /* Use the RecordFactory to extract header/set pairs for each item */
+                    HashMap nativeItem = getColumnValues(rs);
+                    sets.add(getSetXML(nativeItem));
+                    if (debug) System.out.println("JDBCOAICatalog.listSets: adding an entry");
+                }
+
+                /* decide if you're done */
+                if (count < numRows) {
+                    String resumptionId = getResumptionId();
+                    /*****************************************************************
+                     * Note that storing the ResultSet in the resumptionResult
+                     * means the token can't be reused.
+                     *****************************************************************/
+                    resumptionResults.put(resumptionId, rs);
+
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(count));
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            0));
+                    //          listSetsMap.put("resumptionMap",
+                    //                                 getResumptionMap(resumptionTokenSb.toString()));
+                    endConnection(con);
+                }
+            } catch (SQLException e) {
+                if (con != null)
+                    endConnection(con);
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * Retrieve the next set of sets associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listSets() Map result.
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets(String resumptionToken)
+    throws OAIInternalServerError, BadResumptionTokenException {
+        if (setQuery == null) {
+            throw new BadResumptionTokenException();
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            /**********************************************************************
+             * parse your resumptionToken and look it up in the resumptionResults,
+             * if necessary
+             **********************************************************************/
+            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+            String resumptionId;
+            int oldCount;
+            int numRows;
+            try {
+                resumptionId = tokenizer.nextToken();
+                oldCount = Integer.parseInt(tokenizer.nextToken());
+                numRows = Integer.parseInt(tokenizer.nextToken());
+            } catch (NoSuchElementException e) {
+                throw new BadResumptionTokenException();
+            }
+
+            try {
+                /* Get some more records from your database */
+                ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
+                if (rs == null) {
+                    throw new BadResumptionTokenException();
+                }
+
+                if (rs.getRow() != oldCount) {
+//                  System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                    rs.absolute(oldCount);
+                }
+
+                int count;
+
+                /* load the sets ArrayLists. */
+                for (count = 0; count < maxListSize && rs.next(); ++count) {
+                    HashMap nativeItem = getColumnValues(rs);
+                    /* Use the RecordFactory to extract set for each item */
+                    sets.add(getSetXML(nativeItem));
+                }
+
+                /* decide if you're done. */
+                if (oldCount+count < numRows) {
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(oldCount + count));
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            oldCount));
+                    //          listSetsMap.put("resumptionMap",
+                    //                                 getResumptionMap(resumptionTokenSb.toString()));
+                }
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(HashMap nativeItem)
+    throws OAIInternalServerError {
+        Connection con = null;
+        try {
+            ArrayList setSpecs = new ArrayList();
+            if (setSpecQuery != null) {
+                con = startConnection();
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                Statement stmt = con.createStatement();
+                ResultSet rs = stmt.executeQuery(populateSetSpecQuery(oaiIdentifier));
+                while (rs.next()) {
+                    HashMap setMap = getColumnValues(rs);
+                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
+                }
+                endConnection(con);
+            }
+            return setSpecs.iterator();
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(HashMap nativeItem)
+    throws OAIInternalServerError {
+        Connection con = null;
+        try {
+            ArrayList abouts = new ArrayList();
+            if (aboutQuery != null) {
+                con = startConnection();
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                Statement stmt = con.createStatement();
+                ResultSet rs = stmt.executeQuery(populateAboutQuery(oaiIdentifier));
+                while (rs.next()) {
+                    HashMap aboutMap = getColumnValues(rs);
+                    abouts.add(aboutMap.get(aboutValueLabel));
+                }
+                endConnection(con);
+            }
+            return abouts.iterator();
+        } catch (SQLException e) {
+            if (con != null)
+                endConnection(con);
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Extract &lt;set&gt; XML string from setItem object
+     *
+     * @param setItem individual set instance in native format
+     * @return an XML String containing the XML &lt;set&gt; content
+     */
+    public String getSetXML(HashMap setItem)
+    throws IllegalArgumentException {
+//      try {
+        String setSpec = getSetSpec(setItem);
+        String setName = getSetName(setItem);
+        String setDescription = getSetDescription(setItem);
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("<set>");
+        sb.append("<setSpec>");
+        sb.append(OAIUtil.xmlEncode(setSpec));
+        sb.append("</setSpec>");
+        sb.append("<setName>");
+        sb.append(OAIUtil.xmlEncode(setName));
+        sb.append("</setName>");
+        if (setDescription != null) {
+            sb.append("<setDescription>");
+            sb.append(OAIUtil.xmlEncode(setDescription));
+            sb.append("</setDescription>");
+        }
+        sb.append("</set>");
+        return sb.toString();
+//      } catch (SQLException e) {
+//      e.printStackTrace();
+//      throw new IllegalArgumentException(e.getMessage());
+//      }
+    }
+
+    /**
+     * get the setSpec XML string. Extend this class and override this method
+     * if the setSpec can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setSpec&gt; content
+     */
+    protected String getSetSpec(HashMap setItem) {
+        try {
+            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            return "UnsupportedEncodingException";
+        }
+    }
+
+    /**
+     * get the setName XML string. Extend this class and override this method
+     * if the setName can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setName&gt; content
+     */
+    protected String getSetName(HashMap setItem) {
+        return (String)setItem.get(setNameLabel);
+    }
+
+    /**
+     * get the setDescription XML string. Extend this class and override this method
+     * if the setDescription can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setDescription&gt; content
+     */
+    protected String getSetDescription(HashMap setItem) {
+        if (setDescriptionLabel == null)
+            return null;
+        return (String)setItem.get(setDescriptionLabel);
+    }
+
+    /**
+     * close the repository
+     */
+    public void close() {
+        try {
+            if (persistentConnection != null)
+                persistentConnection.close();
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        persistentConnection = null;
+    }
+
+    /**
+     * Purge tokens that are older than the configured time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date now = new Date();
+        Iterator keySet = resumptionResults.keySet().iterator();
+        while (keySet.hasNext()) {
+            String key = (String)keySet.next();
+            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            String key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a String version of the current time
+     */
+    private synchronized static String getResumptionId() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/JDBCRecordFactory.java b/src/ORG/oclc/oai/server/catalog/JDBCRecordFactory.java
new file mode 100644
index 0000000..d452e2c
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/JDBCRecordFactory.java
@@ -0,0 +1,180 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.sql.Date;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+/**
+ * JDBCRecordFactory converts JDBC "items" to "record" Strings.
+ */
+public class JDBCRecordFactory extends RecordFactory {
+    private String repositoryIdentifier = null;
+    protected String identifierLabel = null;
+    protected String datestampLabel = null;
+    
+    /**
+     * Construct an JDBCRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public JDBCRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+	repositoryIdentifier = properties.getProperty("JDBCRecordFactory.repositoryIdentifier");
+	if (repositoryIdentifier == null) {
+	    throw new IllegalArgumentException("JDBCRecordFactory.repositoryIdentifier is missing from the properties file");
+	}
+	identifierLabel = properties.getProperty("JDBCRecordFactory.identifierLabel");
+	if (identifierLabel == null) {
+	    throw new IllegalArgumentException("JDBCRecordFactory.identifierLabel is missing from the properties file");
+	}
+	datestampLabel = properties.getProperty("JDBCRecordFactory.datestampLabel");
+	if (datestampLabel == null) {
+	    throw new IllegalArgumentException("JDBCRecordFactory.datestampLabel is missing from the properties file");
+	}
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param oaiIdentifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String oaiIdentifier) {
+	StringTokenizer tokenizer = new StringTokenizer(oaiIdentifier, ":");
+	try {
+	    tokenizer.nextToken();
+	    tokenizer.nextToken();
+	    return tokenizer.nextToken();
+	} catch (java.util.NoSuchElementException e) {
+	    return null;
+	}
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+// 	throws IllegalArgumentException {
+// 	try {
+	    HashMap table = (HashMap)nativeItem;
+	    Object o = table.get(identifierLabel);
+	    return o.toString();
+// 	} catch (SQLException e) {
+// 	    e.printStackTrace();
+// 	    throw new IllegalArgumentException(e.getMessage());
+// 	}
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem)
+	throws IllegalArgumentException {
+	StringBuffer sb = new StringBuffer();
+	sb.append("oai:");
+	sb.append(repositoryIdentifier);
+	sb.append(":");
+	sb.append(getLocalIdentifier(nativeItem));
+	return sb.toString();
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem) {
+// 	throws IllegalArgumentException  {
+// 	try {
+	    HashMap table = (HashMap)nativeItem;
+        Object datestamp = table.get(datestampLabel);
+        if (datestamp instanceof Timestamp) { 
+            return ((Timestamp)datestamp).toString().substring(0, 10);
+        } else if (datestamp instanceof Date) {
+            return ((Date)datestamp).toString();
+        } else {
+            throw new IllegalArgumentException("Unrecognized datestamp format: " + datestamp.getClass().getName());
+        }
+        
+// 	} catch (SQLException e) {
+// 	    e.printStackTrace();
+// 	    throw new IllegalArgumentException(e.getMessage());
+// 	}
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item. Null if setSpecs aren't
+     * derived from the nativeItem.
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+	return null;
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item. Null if
+     * abouts aren't derived from the nativeItem
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+	// Don't perform quick creates
+	return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/NewFileRecordFactory.java b/src/ORG/oclc/oai/server/catalog/NewFileRecordFactory.java
new file mode 100644
index 0000000..f8dcdda
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/NewFileRecordFactory.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+/**
+ * NewFileRecordFactory converts native XML "items" to "record" Strings.
+ * This factory assumes the native XML item looks exactly like the <record>
+ * element of an OAI GetRecord response, with the possible exception that the
+ * <metadata> element contains multiple metadataFormats from which to choose.
+ */
+public class NewFileRecordFactory extends RecordFactory {
+    private String repositoryIdentifier = null;
+    
+    /**
+     * Construct an NewFileRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public NewFileRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+	repositoryIdentifier = properties.getProperty("NewFileRecordFactory.repositoryIdentifier");
+	if (repositoryIdentifier == null) {
+	    throw new IllegalArgumentException("NewFileRecordFactory.repositoryIdentifier is missing from the properties file");
+	}
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+        try {
+            StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
+            tokenizer.nextToken();
+            tokenizer.nextToken();
+            return tokenizer.nextToken();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+	StringBuffer sb = new StringBuffer();
+	sb.append("oai:");
+	sb.append(repositoryIdentifier);
+	sb.append(":");
+	sb.append(getLocalIdentifier(nativeItem));
+	return sb.toString();
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+        return (String)((HashMap)nativeItem).get("localIdentifier");
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException  {
+        return (String)((HashMap)nativeItem).get("lastModified");
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+        return (Iterator)((HashMap)nativeItem).get("setSpecs");
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+	// Don't perform quick creates
+        try {
+            String result = new String((byte[])((HashMap)nativeItem).get("recordBytes"), "UTF-8");
+            if (result.startsWith("<?")) {
+                int offset = result.indexOf("?>");
+                result = result.substring(offset+2);
+            }
+            return result;
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+            return null;
+        }
+// 	return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/NewFileSystemOAICatalog.java b/src/ORG/oclc/oai/server/catalog/NewFileSystemOAICatalog.java
new file mode 100644
index 0000000..0e396f4
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/NewFileSystemOAICatalog.java
@@ -0,0 +1,796 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ORG.oclc.oai.server.catalog;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.xpath.XPathAPI;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+
+/**
+ * NewFileSystemOAICatalog is an implementation of AbstractCatalog interface
+ * with the data sitting in a directory on a filesystem.
+ *
+ * @author Jeff Young, OCLC Online Computer Library Center
+ */
+
+public class NewFileSystemOAICatalog extends AbstractCatalog {
+    static final boolean debug=false;
+
+    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
+    protected String           homeDir;
+    protected HashMap          fileDateMap = new HashMap();
+    private HashMap setMap = new HashMap();
+    private HashMap          resumptionResults=new HashMap();
+    private int              maxListSize;
+    private ArrayList sets = null;
+
+    public NewFileSystemOAICatalog(Properties properties) throws IOException {
+        String          temp;
+
+        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        temp=properties.getProperty("NewFileSystemOAICatalog.maxListSize");
+        if (temp==null)
+            throw new IllegalArgumentException("NewFileSystemOAICatalog."+
+                "maxListSize is missing from the properties file");
+        maxListSize = Integer.parseInt(temp);
+        if(debug)
+            System.out.println("in NewFileSystemOAICatalog(): maxListSize="+
+                maxListSize);
+
+        homeDir=properties.getProperty("NewFileSystemOAICatalog.homeDir");
+        if (homeDir==null)
+            throw new IllegalArgumentException("NewFileSystemOAICatalog."+
+                "homeDir is missing from the properties file");
+        if(debug)
+            System.out.println("in NewFileSystemOAICatalog(): homeDir="+
+                homeDir);
+
+
+	File homeFile = new File(homeDir);
+	int homeDirLen = homeFile.getPath().length()+1;
+	loadFileMap(homeDirLen, homeFile);
+// 	Iterator iterator = fileDateMap.entrySet().iterator();
+// 	while (iterator.hasNext()) {
+// 	    Map.Entry entry = (Map.Entry)iterator.next();
+// 	    System.out.println(entry.getKey() + ":" + entry.getValue());
+// 	}
+	sets = getSets(properties);
+    }
+
+    private static ArrayList getSets(Properties properties) {
+        TreeMap treeMap = new TreeMap();
+	String propertyPrefix = "Sets.";
+	Enumeration propNames = properties.propertyNames();
+	while (propNames.hasMoreElements()) {
+	    String propertyName = (String)propNames.nextElement();
+	    if (propertyName.startsWith(propertyPrefix)) {
+                treeMap.put(propertyName, properties.get(propertyName));
+	    }
+	}
+ 	return new ArrayList(treeMap.values());
+    }
+    
+    private void loadFileMap(int homeDirLen, File currentDir)
+        throws IOException {
+        try {
+            String[] list = currentDir.list();
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            for (int i=0; i<list.length; ++i) {
+                File child = new File(currentDir, list[i]);
+                if (child.isDirectory() && !"CVS".equals(child.getName())) {
+                    loadFileMap(homeDirLen, child);
+                } else if (isMetadataFile(child)) {
+                    String path = file2path(homeDirLen, child);
+                    System.out.println("parsing " + path);
+                    //                 String datestamp = date2OAIDatestamp(new Date(child.lastModified()));
+                    File file = localIdentifier2File(path);
+                    FileInputStream fis = new FileInputStream(file);
+                    InputSource data = new InputSource(fis);
+                    Node doc = builder.parse(data);
+                    fis.close();
+                    
+                    Node datestampNode = XPathAPI.selectSingleNode(doc, "/record/header/datestamp");
+                    fileDateMap.put(path,
+                                    XPathAPI.eval(datestampNode, "string()").str());
+                    NodeList setNodes = XPathAPI.selectNodeList(doc, "/record/header/setSpec");
+                    for (int j=0; j<setNodes.getLength(); ++j) {
+                        Node setSpecNode = setNodes.item(j);
+                        String setSpec = XPathAPI.eval(setSpecNode,
+                                                       "string()").str();
+                        ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
+                        if (setSpecList == null) {
+                            setSpecList = new ArrayList();
+                            setMap.put(setSpec, setSpecList);
+                        }
+                        setSpecList.add(path);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new IOException(e.getMessage());
+        }
+    }
+
+    /**
+     * Override this method if some files exist in the
+     * filesystem that aren't metadata records.
+     *
+     * @param child the File to be investigated
+     * @return true if it contains metadata, false otherwise
+     */
+    protected boolean isMetadataFile(File child) {
+        return true;
+    }
+
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param homeDirLen the length of the home directory path
+     * @param file the File object containing the native record
+     * @return localIdentifier
+     */
+    protected String file2path(int homeDirLen, File file) {
+	String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
+	return fileName;
+    }
+
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
+     * @return the File object containing the native record
+     */
+    protected File localIdentifier2File(String localIdentifier) {
+        String fileName = localIdentifier.replace('/', File.separatorChar);
+	return new File(homeDir, fileName);
+    }
+
+//     private String date2OAIDatestamp(Date date) {
+//         return dateFormatter.format(date);
+//     }
+
+    private HashMap getNativeHeader(String path) {
+        HashMap recordMap = null;
+        if (fileDateMap.containsKey(path)) {
+            recordMap = new HashMap();
+            recordMap.put("localIdentifier", path.substring(0, path.lastIndexOf(".")));
+            recordMap.put("lastModified", fileDateMap.get(path));
+            ArrayList setSpecs = new ArrayList();
+            Iterator keySet = setMap.keySet().iterator();
+            while (keySet.hasNext()) {
+                String key = (String)keySet.next();
+                ArrayList identifierList = (ArrayList)setMap.get(key);
+                if (identifierList.contains(path)) {
+                    setSpecs.add(key);
+                }
+            }
+            recordMap.put("setSpecs", setSpecs.iterator());
+            return recordMap;
+        }
+        return recordMap;
+    }
+
+    private ArrayList getExtensionList(String localIdentifier) {
+//         System.out.println("getExtensionList: localIdentifier="
+//                            + localIdentifier);
+        ArrayList list = new ArrayList();
+        Iterator iterator = fileDateMap.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry entry = (Map.Entry)iterator.next();
+//             System.out.println("entry.getKey()=" + entry.getKey());
+            if (((String)entry.getKey()).startsWith(localIdentifier)) {
+                list.add(((String)entry.getKey()).substring(localIdentifier.length()+1));
+            }
+        }
+        return list;
+    }
+    
+    private HashMap getNativeRecord(String path)
+        throws IOException {
+        HashMap recordMap = getNativeHeader(path);
+        if (recordMap == null) {
+            return null;
+        } else {
+            File file = localIdentifier2File(path);
+            try {
+                FileInputStream fis = new FileInputStream(file);
+                BufferedInputStream bis = new BufferedInputStream(fis);
+                byte[] buffer = new byte[(int)file.length()];
+                bis.read(buffer, 0, (int)file.length());
+                recordMap.put("recordBytes", buffer);
+                bis.close();
+                fis.close();
+                return recordMap;
+            } catch (FileNotFoundException e) {
+                return null;
+            }
+        }
+    }
+    
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param     oaiIdentifier the OAI identifier
+     * @param     metadataPrefix the OAI metadataPrefix
+     * @return    the Record object containing the result.
+     * @exception CannotDisseminateFormatException signals an http status
+     *                code 400 problem
+     * @exception IdDoesNotExistException signals an http status code 404
+     *                problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *                problem
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+        throws IdDoesNotExistException, CannotDisseminateFormatException,
+               OAIInternalServerError {
+        HashMap nativeItem = null;
+        try {
+	    String localIdentifier
+		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+
+            nativeItem = getNativeRecord(localIdentifier + "." + metadataPrefix);
+            if (nativeItem == null)
+                throw new IdDoesNotExistException(oaiIdentifier);
+            return constructRecord(nativeItem, metadataPrefix);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+    }
+
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * We get passed the ID for a record and are supposed to return a list
+     * of the formats that we can deliver the record in.  Since we are assuming
+     * that all the records in the directory have the same format, the
+     * response to this is static;
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAINotFoundException signals an http status code 404 problem
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+      throws IdDoesNotExistException, NoMetadataFormatsException {
+        ArrayList extensionList = null;
+//         try {
+	    String localIdentifier
+		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+            extensionList = getExtensionList(localIdentifier);
+//         } catch (IOException e) {
+//             e.printStackTrace();
+//             throw new OAIInternalServerError("Database Failure");
+//         }
+        
+        if (extensionList != null) {
+            return getRecordFactory().getSchemaLocations(extensionList);
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+
+
+    /**
+     * Retrieve a list of Identifiers that satisfy the criteria parameters
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+        throws NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+	Iterator iterator = fileDateMap.entrySet().iterator();
+	int numRows = fileDateMap.entrySet().size();
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            String path = (String)entryDateMap.getKey();
+            if (fileDate.compareTo(from) >= 0
+                && fileDate.compareTo(until) <= 0
+                && (setIdentifiers == null || setIdentifiers.contains(path))) {
+                HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
+                String[] header = getRecordFactory().createHeader(nativeHeader);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+                count++;
+            }
+	}
+
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listIdentifiersMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listIdentifiersMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of Identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+      throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        String set;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("NewFileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+	    iterator = fileDateMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the headers and identifiers ArrayLists. */
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String path = (String)entryDateMap.getKey();
+            if (setIdentifiers == null || setIdentifiers.contains(path)) {
+                HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
+                String[] header = getRecordFactory().createHeader(nativeHeader);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+                count++;
+            }
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listIdentifiersMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(HashMap nativeItem, String metadataPrefix)
+        throws CannotDisseminateFormatException {
+        String schemaURL = null;
+	Iterator setSpecs = getSetSpecs(nativeItem);
+	Iterator abouts = getAbouts(nativeItem);
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(HashMap nativeItem) {
+        return null;
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(HashMap nativeItem) {
+        return null;
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listRecords(String from, String until, String set,
+                                    String metadataPrefix)
+      throws CannotDisseminateFormatException,
+      OAIInternalServerError, NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+	Iterator iterator = fileDateMap.entrySet().iterator();
+	int numRows = fileDateMap.entrySet().size();
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            String path = (String)entryDateMap.getKey();
+            String extension = path.substring(path.lastIndexOf(".")+1);
+            if (fileDate.compareTo(from) >= 0
+                && fileDate.compareTo(until) <= 0
+                && extension.equals(metadataPrefix)
+                && (setIdentifiers == null || setIdentifiers.contains(path))) {
+                try {
+                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                    count++;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            }
+	}
+        
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listRecordsMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listRecordsMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listRecords(String resumptionToken)
+      throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        String set;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("NewFileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
+	    iterator = fileDateMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the records ArrayLists. */
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String path = (String)entryDateMap.getKey();
+            if (setIdentifiers == null || setIdentifiers.contains(path)) {
+                try {
+                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                    count++;
+                } catch (CannotDisseminateFormatException e) {
+                    /* the client hacked the resumptionToken beyond repair */
+                    throw new BadResumptionTokenException();
+                } catch (IOException e) {
+                    /* the file is probably missing */
+                    throw new BadResumptionTokenException();
+                }
+            }
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listRecordsMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    public Map listSets() throws NoSetHierarchyException {
+// 	throw new NoSetHierarchyException();
+        if (sets.size() == 0)
+            throw new NoSetHierarchyException();
+         Map listSetsMap = new LinkedHashMap();
+         listSetsMap.put("sets", sets.iterator());
+         return listSetsMap;
+//         ArrayList result = new ArrayList();
+//         Iterator i = sets.values().iterator();
+//         while (i.hasNext()) {
+//             String value = (String)i.next();
+//             result.add(value);
+//         }
+//         Map listSetsMap = new HashMap();
+//         listSetsMap.put("sets", result.iterator());
+//         return listSetsMap;
+    }
+
+
+    public Map listSets(String resumptionToken)
+      throws BadResumptionTokenException {
+	throw new BadResumptionTokenException();
+    }
+
+
+    /**
+     * close the repository
+     */
+    public void close() { }
+
+
+    /**
+     * Purge tokens that are older than the time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date      then, now = new Date();
+        Iterator  keySet = resumptionResults.keySet().iterator();
+        String    key;
+
+        while (keySet.hasNext()) {
+            key=(String)keySet.next();
+            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a long integer version of the current time
+     */
+    private synchronized static String getRSName() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/NewJDBCOAICatalog.java b/src/ORG/oclc/oai/server/catalog/NewJDBCOAICatalog.java
new file mode 100644
index 0000000..b1d1d2d
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/NewJDBCOAICatalog.java
@@ -0,0 +1,1359 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * NewJDBCOAICatalog is like JDBCOAICatalog except it is more intelligent
+ * about the coupling between Statement and ResultSet.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ * @deprecated Use ExtendedJDBCOAICatalog instead
+ */
+public class NewJDBCOAICatalog extends AbstractCatalog {
+    /**
+     * The StatementResultSet inner class is used because Statement objects
+     * and ResultSet objects are tightly coupled and need to persist across
+     * multiple requests.
+     *
+     * @author Jeffrey A. Young
+     */
+    private class StatementResultSet {
+        private Statement stmt = null;
+        private ResultSet rs = null;
+
+        public StatementResultSet(String query) throws SQLException {
+            Connection con = getConnection();
+            stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            rs = stmt.executeQuery(query);
+        }
+
+        public Statement getStatement() { return stmt; }
+        public ResultSet getResultSet() { return rs; }
+
+        public void close() throws SQLException {
+            if (rs != null) {
+                rs.close();
+                rs = null;
+            }
+            if (stmt != null) {
+                stmt.close();
+                stmt = null;
+            }
+        }
+
+        /**
+         * Since the columns should only be read once, copy them into a
+         * HashMap and consider that to be the "record"
+         * @return a HashMap mapping column names with values
+         */
+        public HashMap getColumnValues()
+        throws SQLException {
+            ResultSetMetaData mdata = rs.getMetaData();
+            int count = mdata.getColumnCount();
+            HashMap nativeItem = new HashMap(count);
+            for (int i=1; i<=count; ++i) {
+                String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
+                nativeItem.put(fieldName, rs.getObject(i));
+                if (debug) System.out.println(fieldName + "=" + nativeItem.get(fieldName));
+            }
+            return nativeItem;
+        }
+
+        public boolean next() throws SQLException {
+            return rs.next();
+        }
+
+        public void last() throws SQLException {
+            rs.last();
+        }
+
+        public void beforeFirst() throws SQLException {
+            rs.beforeFirst();
+        }
+
+        public int getRow() throws SQLException {
+            return rs.getRow();
+        }
+
+        public boolean absolute(int oldCount) throws SQLException {
+            return rs.absolute(oldCount);
+        }
+    }
+
+    /**
+     * The JDBC Connection
+     */
+    private Connection persistentConnection = null;
+
+    private static final boolean debug = false;
+
+    /**
+     * SQL identifier query (loaded from properties)
+     * \\i -> localIdentifier, \\o -> oaiIdentifier
+     */
+    private String identifierQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until
+     */
+    private String rangeQuery = null;
+
+    /**
+     * SQL range query (loaded from properties)
+     * \\f -> from, \\u -> until, \\s -> set
+     */
+    private String rangeSetQuery = null;
+
+    /**
+     * SQL query to get a list of available sets
+     */
+    private String setQuery = null;
+
+    /**
+     * SQL query to get a list of available sets that apply to a particular identifier
+     */
+    private String setSpecQuery = null;
+
+    /**
+     * SQL query to get a list of available abouts that apply to a particular identifier
+     */
+    private String aboutQuery = null;
+
+    /**
+     * SQL column labels containing the values of particular interest
+     */
+    private String aboutValueLabel = null;
+    private String setSpecItemLabel = null;
+    private String setSpecListLabel = null;
+    private String setNameLabel = null;
+    private String setDescriptionLabel = null;
+
+    /**
+     * maximum number of entries to return for ListRecords and ListIdentifiers
+     * (loaded from properties)
+     */
+    private int maxListSize;
+
+    /**
+     * The format required for dates in SQL queries
+     * "UTC" = YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ
+     * other = YYYY/MM/DD
+     */
+    private String dateFormat = null;
+
+    /**
+     * Set Strings to be loaded from the properties file
+     * (if they are to be loaded from properties rather than queried from the database)
+     */
+    ArrayList sets = new ArrayList();
+
+    /**
+     * pending resumption tokens
+     */
+    private HashMap resumptionResults = new HashMap();
+    private String jdbcURL = null;
+    private String jdbcLogin = null;
+    private String jdbcPasswd = null;
+
+    /**
+     * Construct a JDBCOAICatalog object
+     *
+     * @param properties a properties object containing initialization parameters
+     */
+    public NewJDBCOAICatalog(Properties properties) {
+        dateFormat = properties.getProperty("JDBCOAICatalog.dateFormat");
+        String maxListSize = properties.getProperty("JDBCOAICatalog.maxListSize");
+        if (maxListSize == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.maxListSize is missing from the properties file");
+        } else {
+            this.maxListSize = Integer.parseInt(maxListSize);
+        }
+
+        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
+        if (paramRegex != null) {
+            setParamRegex(paramRegex);
+        }
+        String jdbcDriverName = properties.getProperty("JDBCOAICatalog.jdbcDriverName");
+        if (jdbcDriverName == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is missing from the properties file");
+        }
+        jdbcURL = properties.getProperty("JDBCOAICatalog.jdbcURL");
+        if (jdbcURL == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcURL is missing from the properties file");
+        }
+
+        jdbcLogin = properties.getProperty("JDBCOAICatalog.jdbcLogin");
+        if (jdbcLogin == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcLogin is missing from the properties file");
+        }
+
+        jdbcPasswd = properties.getProperty("JDBCOAICatalog.jdbcPasswd");
+        if (jdbcPasswd == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcPasswd is missing from the properties file");
+        }
+
+        rangeQuery = properties.getProperty("JDBCOAICatalog.rangeQuery");
+        if (rangeQuery == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.rangeQuery is missing from the properties file");
+        }
+
+        rangeSetQuery = properties.getProperty("JDBCOAICatalog.rangeSetQuery");
+        if (rangeSetQuery == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.rangeSetQuery is missing from the properties file");
+        }
+
+        identifierQuery = properties.getProperty("JDBCOAICatalog.identifierQuery");
+        if (identifierQuery == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.identifierQuery is missing from the properties file");
+        }
+
+        aboutQuery = properties.getProperty("JDBCOAICatalog.aboutQuery");
+        if (aboutQuery != null) {
+            aboutValueLabel = properties.getProperty("JDBCOAICatalog.aboutValueLabel");
+            if (aboutValueLabel == null) {
+                throw new IllegalArgumentException("JDBCOAICatalog.aboutValueLabel is missing from the properties file");
+            }
+        }
+
+        setSpecQuery = properties.getProperty("JDBCOAICatalog.setSpecQuery");
+        setSpecItemLabel = properties.getProperty("JDBCOAICatalog.setSpecItemLabel");
+        if (setSpecItemLabel == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.setSpecItemLabel is missing from the properties file");
+        }
+        setSpecListLabel = properties.getProperty("JDBCOAICatalog.setSpecListLabel");
+        if (setSpecListLabel == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.setSpecListLabel is missing from the properties file");
+        }
+        setNameLabel = properties.getProperty("JDBCOAICatalog.setNameLabel");
+        if (setNameLabel == null) {
+            throw new IllegalArgumentException("JDBCOAICatalog.setNameLabel is missing from the properties file");
+        }
+
+        setDescriptionLabel = properties.getProperty("JDBCOAICatalog.setDescriptionLabel");
+
+        // See if a setQuery exists
+        setQuery = properties.getProperty("JDBCOAICatalog.setQuery");
+        if (setQuery == null) {
+            // if not, load the set Strings from the properties file (if present)
+            String propertyPrefix = "Sets.";
+            Enumeration propNames = properties.propertyNames();
+            while (propNames.hasMoreElements()) {
+                String propertyName = (String)propNames.nextElement();
+                if (propertyName.startsWith(propertyPrefix)) {
+                    sets.add(properties.get(propertyName));
+                }
+            }
+        }
+
+        try {
+            Class.forName(jdbcDriverName);
+        } catch (ClassNotFoundException e) {
+            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is invalid: "
+                    + jdbcDriverName);
+        }
+    }
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
+     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
+     * but the item is flagged as deleted and thus no schemaLocations (i.e.
+     * metadataFormats) can be produced.
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
+        StatementResultSet stmtRs = null;
+        try {
+            stmtRs = new StatementResultSet(populateIdentifierQuery(oaiIdentifier));
+            /*
+             * Let your recordFactory decide which schemaLocations
+             * (i.e. metadataFormats) it can produce from the record.
+             * Doing so will preserve the separation of database access
+             * (which happens here) from the record content interpretation
+             * (which is the responsibility of the RecordFactory implementation).
+             */
+            if (!stmtRs.next()) {
+                throw new IdDoesNotExistException(oaiIdentifier);
+            } else {
+                /* Make sure the identifierQuery returns the columns you need
+                 * (if any) to determine the supported schemaLocations for this item */
+                HashMap nativeItem = stmtRs.getColumnValues();
+                return getRecordFactory().getSchemaLocations(nativeItem);
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } finally {
+            try {
+                if (stmtRs != null)
+                    stmtRs.close();
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the rangeQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateRangeQuery(String from, String until, String set)
+    throws OAIInternalServerError {
+        StringBuffer sb = new StringBuffer();
+        StringTokenizer tokenizer;
+        if (set == null || set.length() == 0)
+            tokenizer = new StringTokenizer(rangeQuery, "\\");
+        else
+            tokenizer = new StringTokenizer(rangeSetQuery, "\\");
+
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid query");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'f':  // what are the chances someone would use this to indicate a form feed?
+                sb.append(formatFromDate(from));
+                break;
+            case 'u':
+                sb.append(formatUntilDate(until));
+                break;
+            case 's':
+                sb.append(set);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatFromDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Extend this class and override this method if necessary.
+     */
+    protected String formatUntilDate(String date) {
+        return formatDate(date);
+    }
+
+    /**
+     * Change the String from UTC to SQL format
+     * If this method doesn't suit your needs, extend this class and override
+     * the method rather than change this code directly.
+     */
+    protected String formatDate(String date) {
+        if ("UTC".equals(dateFormat)) {
+            return date;
+        } else {
+            StringBuffer sb = new StringBuffer();
+            sb.append(date.substring(5, 7));
+            sb.append("/");
+            sb.append(date.substring(8));
+            sb.append("/");
+            sb.append(date.substring(0, 4));
+            if (debug) System.out.println("JDBCOAICatalog.formatDate: from " + date + " to " + sb.toString());
+            return sb.toString();
+        }
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateIdentifierQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateSetSpecQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * insert actual from, until, and set parameters into the identifierQuery String
+     *
+     * @param from the OAI from parameter
+     * @param until the OAI until paramter
+     * @param set the OAI set parameter
+     * @return a String containing an SQL query
+     */
+    private String populateAboutQuery(String oaiIdentifier)
+    throws OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
+        StringBuffer sb = new StringBuffer();
+        if (tokenizer.hasMoreTokens())
+            sb.append(tokenizer.nextToken());
+        else
+            throw new OAIInternalServerError("Invalid identifierQuery");
+
+        while (tokenizer.hasMoreTokens()) {
+            String token = tokenizer.nextToken();
+            switch (token.charAt(0)) {
+            case 'i':
+                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
+                break;
+            case 'o':
+                sb.append(oaiIdentifier);
+                break;
+            default: // ignore it
+                sb.append("\\");
+            sb.append(token.charAt(0));
+            }
+            sb.append(token.substring(1));
+        }
+        if (debug) System.out.println(sb.toString());
+        return sb.toString();
+    }
+
+    /**
+     * Retrieve a list of identifiers that satisfy the specified criteria
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * It may seem strange for the map to include both "headers" and "identifiers"
+     * since the identifiers can be obtained from the headers. This may be true, but
+     * AbstractCatalog.listRecords() can operate quicker if it doesn't
+     * need to parse identifiers from the XML headers itself. Better
+     * still, do like I do below and override AbstractCatalog.listRecords().
+     * AbstractCatalog.listRecords() is relatively inefficient because given the list
+     * of identifiers, it must call getRecord() individually for each as it constructs
+     * its response. It's much more efficient to construct the entire response in one fell
+     * swoop by overriding listRecords() as I've done here.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+    throws NoItemsMatchException,
+    OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        StatementResultSet stmtRs = null;
+        try {
+            /* Get some records from your database */
+            stmtRs = new StatementResultSet(populateRangeQuery(from, until, set));
+            stmtRs.last();
+            int numRows = stmtRs.getRow();
+            if (numRows == 0) {
+                throw new NoItemsMatchException();
+            }
+            stmtRs.beforeFirst();
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count=0; count < maxListSize && stmtRs.next(); ++count) {
+                HashMap nativeItem = stmtRs.getColumnValues();
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            /* decide if you're done */
+            if (count < numRows) {
+                String resumptionId = getResumptionId();
+                resumptionResults.put(resumptionId, stmtRs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        0));
+            } else {
+                stmtRs.close();
+                stmtRs = null;
+            }
+        } catch (SQLException e) {
+            if (stmtRs != null) {
+                try {
+                    stmtRs.close();
+                } catch (SQLException e1) {
+                    e1.printStackTrace();
+                }
+            }
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+        int numRows;
+        StatementResultSet stmtRs = null;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        try {
+            /* Get some more records from your database */
+            stmtRs = (StatementResultSet)resumptionResults.get(resumptionId);
+            if (stmtRs == null) {
+                throw new BadResumptionTokenException();
+            }
+            int count;
+
+            if (stmtRs.getRow() != oldCount) {
+//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                stmtRs.absolute(oldCount);
+            }
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && stmtRs.next(); ++count) {
+                HashMap nativeItem = stmtRs.getColumnValues();
+                /* Use the RecordFactory to extract header/identifier pairs for each item */
+                Iterator setSpecs = getSetSpecs(nativeItem);
+                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+            }
+
+            /* decide if you're done. */
+            if (oldCount+count < numRows) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        oldCount));
+            } else {
+                stmtRs.close();
+                stmtRs = null;
+            }
+        } catch (SQLException e) {
+            if (stmtRs != null) {
+                try {
+                    stmtRs.close();
+                } catch (SQLException e1) {
+                    e1.printStackTrace();
+                }
+            }
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return the <record/> portion of the XML response.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix is not
+     * supported by the item.
+     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+    throws OAIInternalServerError, CannotDisseminateFormatException,
+    IdDoesNotExistException {
+        StatementResultSet stmtRs = null;
+        try {
+            stmtRs = new StatementResultSet(populateIdentifierQuery(oaiIdentifier));
+            if (!stmtRs.next()) {
+                throw new IdDoesNotExistException(oaiIdentifier);
+            }
+            HashMap nativeItem = stmtRs.getColumnValues();
+            return constructRecord(nativeItem, metadataPrefix);
+        } catch (SQLException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } finally {
+            try {
+                if (stmtRs != null)
+                    stmtRs.close();
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria. Note, though,
+     * that unlike the other OAI verb type methods implemented here, both of the
+     * listRecords methods are already implemented in AbstractCatalog rather than
+     * abstracted. This is because it is possible to implement ListRecords as a
+     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
+     * I suggest that you override both the AbstractCatalog.listRecords methods
+     * here since it will probably improve the performance if you create the
+     * response in one fell swoop rather than construct it one GetRecord at a time.
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for a "records" Iterator object
+     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception CannotDisseminateFormatException the metadataPrefix isn't
+     * supported by the item.
+     */
+    public Map listRecords(String from, String until, String set, String metadataPrefix)
+    throws CannotDisseminateFormatException, NoItemsMatchException,
+    OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        StatementResultSet stmtRs = null;
+
+        try {
+            stmtRs = new StatementResultSet(populateRangeQuery(from, until, set));
+            stmtRs.last();
+            int numRows = stmtRs.getRow();
+            if (numRows == 0) {
+                throw new NoItemsMatchException();
+            }
+            stmtRs.beforeFirst();
+            int count;
+
+//          if (debug) System.out.println("JDBCOAICatalog.listRecords: numRows=" + numRows);
+
+            /* load the records ArrayList */
+            for (count=0; count < maxListSize && stmtRs.next(); ++count) {
+                HashMap nativeItem = stmtRs.getColumnValues();
+                String record = constructRecord(nativeItem, metadataPrefix);
+                records.add(record);
+            }
+
+            /* decide if you're done */
+            if (count < numRows) {
+                String resumptionId = getResumptionId();
+                resumptionResults.put(resumptionId, stmtRs);
+
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        0));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
+            } else {
+                stmtRs.close();
+                stmtRs = null;
+            }
+        } catch (SQLException e) {
+            if (stmtRs != null) {
+                try {
+                    stmtRs.close();
+                } catch (SQLException e1) {
+                    e1.printStackTrace();
+                }
+            }
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     * @exception BadResumptionTokenException the value of the resumptionToken argument
+     * is invalid or expired.
+     */
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        purge(); // clean out old resumptionTokens
+
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+        String resumptionId;
+        int oldCount;
+        int numRows;
+        String metadataPrefix;
+        StatementResultSet stmtRs = null;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        try {
+            /* Get some more records from your database */
+            stmtRs = (StatementResultSet) resumptionResults.get(resumptionId);
+            if (stmtRs == null) {
+                throw new BadResumptionTokenException();
+            }
+
+            if (stmtRs.getRow() != oldCount) {
+//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                stmtRs.absolute(oldCount);
+            }
+
+            int count;
+
+            /* load the headers and identifiers ArrayLists. */
+            for (count = 0; count < maxListSize && stmtRs.next(); ++count) {
+                try {
+                    HashMap nativeItem = stmtRs.getColumnValues();
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                } catch (CannotDisseminateFormatException e) {
+                    /* the client hacked the resumptionToken beyond repair */
+                    throw new BadResumptionTokenException();
+                }
+            }
+
+            /* decide if you're done */
+            if (oldCount+count < numRows) {
+                /*****************************************************************
+                 * Construct the resumptionToken String however you see fit.
+                 *****************************************************************/
+                StringBuffer resumptionTokenSb = new StringBuffer();
+                resumptionTokenSb.append(resumptionId);
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(oldCount + count));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(Integer.toString(numRows));
+                resumptionTokenSb.append("!");
+                resumptionTokenSb.append(metadataPrefix);
+
+                /*****************************************************************
+                 * Use the following line if you wish to include the optional
+                 * resumptionToken attributes in the response. Otherwise, use the
+                 * line after it that I've commented out.
+                 *****************************************************************/
+                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                        numRows,
+                        oldCount));
+                //          listRecordsMap.put("resumptionMap",
+                //                                 getResumptionMap(resumptionTokenSb.toString()));
+            } else {
+                stmtRs.close();
+                stmtRs = null;
+            }
+        } catch (SQLException e) {
+            if (stmtRs != null) {
+                try {
+                    stmtRs.close();
+                } catch (SQLException e1) {
+                    e1.printStackTrace();
+                }
+            }
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(HashMap nativeItem, String metadataPrefix)
+    throws CannotDisseminateFormatException, OAIInternalServerError {
+        String schemaURL = null;
+        Iterator setSpecs = getSetSpecs(nativeItem);
+        Iterator abouts = getAbouts(nativeItem);
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * Retrieve a list of sets that satisfy the specified criteria
+     *
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception OAIBadRequestException signals an http status code 400 problem
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
+        StatementResultSet stmtRs = null;
+        if (setQuery == null) {
+            if (sets.size() == 0)
+                throw new NoSetHierarchyException();
+            Map listSetsMap = new HashMap();
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            try {
+                if (debug) System.out.println(setQuery);
+
+                /* Get some records from your database */
+                stmtRs = new StatementResultSet(setQuery);
+                stmtRs.last();
+                int numRows = stmtRs.getRow();
+                stmtRs.beforeFirst();
+                int count;
+
+//              if (debug) System.out.println("JDBCOAICatalog.listSets: numRows=" + numRows);
+
+                /* load the sets ArrayLists. */
+                for (count=0; count < maxListSize && stmtRs.next(); ++count) {
+                    /* Use the RecordFactory to extract header/set pairs for each item */
+                    HashMap nativeItem = stmtRs.getColumnValues();
+                    sets.add(getSetXML(nativeItem));
+                    if (debug) System.out.println("JDBCOAICatalog.listSets: adding an entry");
+                }
+
+                /* decide if you're done */
+                if (count < numRows) {
+                    String resumptionId = getResumptionId();
+                    /*****************************************************************
+                     * Note that storing the ResultSet in the resumptionResult
+                     * means the token can't be reused.
+                     *****************************************************************/
+                    resumptionResults.put(resumptionId, stmtRs);
+
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(count));
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            0));
+                } else {
+                    stmtRs.close();
+                    stmtRs = null;
+                }
+            } catch (SQLException e) {
+                if (stmtRs != null) {
+                    try {
+                        stmtRs.close();
+                    } catch (SQLException e1) {
+                        e1.printStackTrace();
+                    }
+                }
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * Retrieve the next set of sets associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listSets() Map result.
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     * @exception OAIInternalServerError signals an http status code 500 problem
+     */
+    public Map listSets(String resumptionToken)
+    throws OAIInternalServerError, BadResumptionTokenException {
+        StatementResultSet stmtRs = null;
+
+        if (setQuery == null) {
+            throw new BadResumptionTokenException();
+        } else {
+            purge(); // clean out old resumptionTokens
+            Map listSetsMap = new HashMap();
+            ArrayList sets = new ArrayList();
+
+            /**********************************************************************
+             * parse your resumptionToken and look it up in the resumptionResults,
+             * if necessary
+             **********************************************************************/
+            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
+            String resumptionId;
+            int oldCount;
+            int numRows;
+            try {
+                resumptionId = tokenizer.nextToken();
+                oldCount = Integer.parseInt(tokenizer.nextToken());
+                numRows = Integer.parseInt(tokenizer.nextToken());
+            } catch (NoSuchElementException e) {
+                throw new BadResumptionTokenException();
+            }
+
+            try {
+                /* Get some more records from your database */
+                stmtRs = (StatementResultSet)resumptionResults.get(resumptionId);
+                if (stmtRs == null) {
+                    throw new BadResumptionTokenException();
+                }
+
+                if (stmtRs.getRow() != oldCount) {
+//                  System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+                    stmtRs.absolute(oldCount);
+                }
+
+                int count;
+
+                /* load the sets ArrayLists. */
+                for (count = 0; count < maxListSize && stmtRs.next(); ++count) {
+                    HashMap nativeItem = stmtRs.getColumnValues();
+                    /* Use the RecordFactory to extract set for each item */
+                    sets.add(getSetXML(nativeItem));
+                }
+
+                /* decide if you're done. */
+                if (oldCount+count < numRows) {
+                    /*****************************************************************
+                     * Construct the resumptionToken String however you see fit.
+                     *****************************************************************/
+                    StringBuffer resumptionTokenSb = new StringBuffer();
+                    resumptionTokenSb.append(resumptionId);
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(oldCount + count));
+                    resumptionTokenSb.append("!");
+                    resumptionTokenSb.append(Integer.toString(numRows));
+
+                    /*****************************************************************
+                     * Use the following line if you wish to include the optional
+                     * resumptionToken attributes in the response. Otherwise, use the
+                     * line after it that I've commented out.
+                     *****************************************************************/
+                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                            numRows,
+                            oldCount));
+                } else {
+                    stmtRs.close();
+                    stmtRs = null;
+                }
+            } catch (SQLException e) {
+                if (stmtRs != null) {
+                    try {
+                        stmtRs.close();
+                    } catch (SQLException e1) {
+                        e1.printStackTrace();
+                    }
+                }
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+
+            listSetsMap.put("sets", sets.iterator());
+            return listSetsMap;
+        }
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(HashMap nativeItem)
+    throws OAIInternalServerError {
+        StatementResultSet stmtRs = null;
+        try {
+            ArrayList setSpecs = new ArrayList();
+            if (setSpecQuery != null) {
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                stmtRs = new StatementResultSet(populateSetSpecQuery(oaiIdentifier));
+                while (stmtRs.next()) {
+                    HashMap setMap = stmtRs.getColumnValues();
+                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
+                }
+            }
+            return setSpecs.iterator();
+        } catch (SQLException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } finally {
+            try {
+                stmtRs.close();
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(HashMap nativeItem)
+    throws OAIInternalServerError {
+        StatementResultSet stmtRs = null;
+        try {
+            ArrayList abouts = new ArrayList();
+            if (aboutQuery != null) {
+                RecordFactory rf = getRecordFactory();
+                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
+                stmtRs = new StatementResultSet(populateAboutQuery(oaiIdentifier));
+                while (stmtRs.next()) {
+                    HashMap aboutMap = stmtRs.getColumnValues();
+                    abouts.add(aboutMap.get(aboutValueLabel));
+                }
+            }
+            return abouts.iterator();
+        } catch (SQLException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } finally {
+            try {
+                stmtRs.close();
+            } catch (SQLException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Extract &lt;set&gt; XML string from setItem object
+     *
+     * @param setItem individual set instance in native format
+     * @return an XML String containing the XML &lt;set&gt; content
+     */
+    public String getSetXML(HashMap setItem)
+    throws IllegalArgumentException {
+        String setSpec = getSetSpec(setItem);
+        String setName = getSetName(setItem);
+        String setDescription = getSetDescription(setItem);
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("<set>");
+        sb.append("<setSpec>");
+        sb.append(OAIUtil.xmlEncode(setSpec));
+        sb.append("</setSpec>");
+        sb.append("<setName>");
+        sb.append(OAIUtil.xmlEncode(setName));
+        sb.append("</setName>");
+        if (setDescription != null) {
+            sb.append("<setDescription>");
+            sb.append(OAIUtil.xmlEncode(setDescription));
+            sb.append("</setDescription>");
+        }
+        sb.append("</set>");
+        return sb.toString();
+    }
+
+    /**
+     * get the setSpec XML string. Extend this class and override this method
+     * if the setSpec can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setSpec&gt; content
+     */
+    protected String getSetSpec(HashMap setItem) {
+        try {
+            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            return "UnsupportedEncodingException";
+        }
+    }
+
+    /**
+     * get the setName XML string. Extend this class and override this method
+     * if the setName can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setName&gt; content
+     */
+    protected String getSetName(HashMap setItem) {
+        return (String)setItem.get(setNameLabel);
+    }
+
+    /**
+     * get the setDescription XML string. Extend this class and override this method
+     * if the setDescription can't be directly taken from the result set as a String
+     *
+     * @param setItem
+     * @return an XML String containing the &lt;setDescription&gt; content
+     */
+    protected String getSetDescription(HashMap setItem) {
+        if (setDescriptionLabel == null)
+            return null;
+        return (String)setItem.get(setDescriptionLabel);
+    }
+
+    private Connection getNewConnection() throws SQLException {
+        // open the connection
+        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
+    }
+
+    private Connection getConnection() throws SQLException {
+        if (persistentConnection != null) {
+            if (persistentConnection.isClosed()) {
+                System.out.println("Persistent connection has expired.");
+                persistentConnection = getNewConnection();
+            }
+        } else {
+            persistentConnection = getNewConnection();
+        }
+        return persistentConnection;
+    }
+
+    /**
+     * close the repository
+     */
+    public void close() {
+    }
+
+    /**
+     * Purge tokens that are older than the configured time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date now = new Date();
+        Iterator keySet = resumptionResults.keySet().iterator();
+        while (keySet.hasNext()) {
+            String key = (String)keySet.next();
+            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            String key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a String version of the current time
+     */
+    private synchronized static String getResumptionId() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/NodeRecordFactory.java b/src/ORG/oclc/oai/server/catalog/NodeRecordFactory.java
new file mode 100644
index 0000000..e95818c
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/NodeRecordFactory.java
@@ -0,0 +1,357 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.TransformerException;
+
+import org.apache.xpath.XPathAPI;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXParseException;
+
+import ORG.oclc.oai.server.crosswalk.CrosswalkItem;
+import ORG.oclc.oai.server.crosswalk.NodePassThruCrosswalk;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * NodeRecordFactory converts native XML "items" to "record" Strings.
+ */
+public class NodeRecordFactory extends RecordFactory {
+//     private static Logger logger = Logger.getLogger(NodeRecordFactory.class);
+//     static {
+// 	BasicConfigurator.configure();
+//     }
+    private static Element xmlnsEl = null;
+    private static DocumentBuilderFactory factory = null;
+    static {
+	try {
+	    factory =
+		DocumentBuilderFactory.newInstance();
+	    factory.setNamespaceAware(true);
+	    DocumentBuilder builder = factory.newDocumentBuilder();
+	    DOMImplementation impl = builder.getDOMImplementation();
+	    Document xmlnsDoc =
+		impl.createDocument(
+				    "http://www.oclc.org/research/software/oai/harvester",
+				    "harvester:xmlnsDoc",
+				    null);
+	    xmlnsEl = xmlnsDoc.getDocumentElement();
+// 	    xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+// 				   "xmlns:mx",
+// 				   "http://www.loc.gov/MARC21/slim");
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                                   "xmlns:xsd",
+                                   "http://www.w3.org/2001/XMLSchema");
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                                   "xmlns:srw",
+                                   "http://www.loc.gov/zing/srw/");
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+				   "xmlns:oai",
+				   "http://www.openarchives.org/OAI/2.0/");
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                                   "xmlns:explain",
+                                   "http://explain.z3950.org/dtd/2.0/");
+	} catch (Exception e) {
+	    e.printStackTrace();
+	}
+    }
+    
+    public NodeRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	this(properties, getCrosswalkMap(properties.getProperty("SRUOAICatalog.sruURL"), false));
+    }
+    
+    /**
+     * Construct an NodeRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public NodeRecordFactory(Properties properties, HashMap crosswalkMap)
+	throws IllegalArgumentException {
+	super(crosswalkMap);
+    }
+
+    private static HashMap getCrosswalkMap(String sruURL, boolean enrich)
+	throws IllegalArgumentException {
+	try {
+	    DocumentBuilder parser = factory.newDocumentBuilder();
+	    System.out.println("sruURL=" + sruURL);
+	    Document explainDoc = parser.parse(sruURL);
+	    System.out.println("explainDoc=" + explainDoc);
+ 	    NodeList schemas = XPathAPI.selectNodeList(explainDoc, "/srw:explainResponse/srw:record/srw:recordData/explain:explain/explain:schemaInfo/explain:schema", xmlnsEl);
+	    
+	    // Load the formats the repository supports directly
+	    HashMap crosswalkMap = new HashMap();
+	    for (int i=0; i<schemas.getLength(); ++i) {
+		Object[] crosswalkItem = crosswalkItemFactory(schemas.item(i));
+		for (int j=0; j<crosswalkItem.length; ++j) {
+		    CrosswalkItem currentItem = (CrosswalkItem)crosswalkItem[j];
+		    // 		logger.debug(currentItem.toString());
+		    Object key = currentItem.getMetadataPrefix();
+		    CrosswalkItem storedItem = (CrosswalkItem)crosswalkMap.get(key);
+		    if (storedItem == null || (currentItem.getRank() < storedItem.getRank())) {
+			crosswalkMap.put(key, currentItem);
+		    }
+		}
+	    }
+
+	    HashMap moreCrosswalkMap = new HashMap();
+// 	    if (enrich) {
+// 		System.out.println("enriching...");
+// 		// Enrich the crosswalkMap with formats we can create w/crosswalks
+// 		Iterator oldCrosswalkItems = crosswalkMap.values().iterator();
+// 		while (oldCrosswalkItems.hasNext()) {
+// 		    CrosswalkItem oldCrosswalkItem = (CrosswalkItem)oldCrosswalkItems.next();
+// 		    String oldSchema = oldCrosswalkItem.getSchema();
+// 		    String nativeRecordSchema = oldCrosswalkItem.getNativeRecordSchema();
+// 		    moreCrosswalkMap.putAll(getCrosswalkMap(oldSchema,
+// 							    nativeRecordSchema,
+// 							    crosswalkMap,
+// 							    moreCrosswalkMap));
+// 		}
+// 	    }
+	    // Now, combine the lists with the originals taking precidence
+	    moreCrosswalkMap.putAll(crosswalkMap);
+	    return moreCrosswalkMap;
+	} catch (Exception e) {
+	    e.printStackTrace();
+	    throw new IllegalArgumentException(sruURL);
+	}
+    }
+
+//    private static HashMap getCrosswalkMap(String oldSchema,
+//				    String nativeRecordSchema,
+//				    HashMap crosswalkMap,
+//				    HashMap moreCrosswalkMap)
+//	throws OAIInternalServerError, SAXException, IOException,
+//	       TransformerException {
+//	HashMap hashMap = new HashMap();
+//	return hashMap;
+//    }
+
+//     public ArrayList querySourceSchema(String nativeRecordSchema,
+// 				       String sourceSchema)
+// 	throws OAIInternalServerError, SAXException, IOException,
+// 	       TransformerException {
+// 	try {
+// 	    ArrayList list = new ArrayList();
+// 	    StringBuffer sb = new StringBuffer(getCentralERRoLURL());
+// 	    sb.append("/schemaTrans.oclc.org.sru?query=schemaTrans.source+%3D+%22");
+// 	    sb.append(URLEncoder.encode(sourceSchema, "UTF-8"));
+// 	    sb.append("%22&version=1.1&operation=searchRetrieve&maximumRecords=10&startRecord=1&resultSetTTL=0&recordPacking=xml&recordXPath=&sortKeys=");
+// 	    Document sruDoc = parser.parse(sb.toString());
+// 	    NodeList nodeList = XPathAPI.selectNodeList(sruDoc, "/srw:searchRetrieveResponse/srw:records/srw:record/srw:recordData/mets:mets[mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:div[@LABEL='infoURI']]", xmlnsEl);
+
+// 	    for (int i=0; i<nodeList.getLength(); ++i) {
+// 		Node recordNode = nodeList.item(i);
+// 		String infoURIFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:div[@LABEL='infoURI']/mets:fptr/@FILEID", xmlnsEl).str();
+//  		String infoURI = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+infoURIFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();
+// 		String namespaceFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:div[@LABEL='namespace']/mets:fptr/@FILEID", xmlnsEl).str();
+// 		String namespace = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+namespaceFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();
+// 		String schemaFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:fptr/@FILEID", xmlnsEl).str();
+// 		String schema = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+schemaFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();
+// 		String crosswalkFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='crosswalk']/mets:div[@LABEL='Application']/mets:fptr/@FILEID", xmlnsEl).str();
+// 		String crosswalk = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+crosswalkFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();
+
+// 		String metadataPrefix = infoURI.substring("info:ofi/fmt:xml:xsd:".length());
+// 		list.add(new CrosswalkItem(nativeRecordSchema,
+// 					   metadataPrefix,
+// 					   schema,
+// 					   namespace,
+// 					   XSLCrosswalk.class,
+// 					   crosswalk
+// 					   ));
+// 	    }
+// 	    return list;
+// 	} catch (UnsupportedEncodingException e) {
+// 	    e.printStackTrace();
+// 	    throw new OAIInternalServerError(e.getMessage());
+// 	}
+//     }
+    
+    public static Object[] crosswalkItemFactory(Node explainSchemaNode)
+	throws TransformerException, OAIInternalServerError {
+	ArrayList crosswalkItemList = new ArrayList();
+ 	String nativeRecordSchema = XPathAPI.eval(explainSchemaNode, "@identifier", xmlnsEl).str();
+ 	String metadataPrefix = XPathAPI.eval(explainSchemaNode, "@name", xmlnsEl).str();
+	String schema = XPathAPI.eval(explainSchemaNode, "@location", xmlnsEl).str();
+	String metadataNamespace = null;
+	try {
+	    DocumentBuilder parser = factory.newDocumentBuilder();
+	    Document schemaDoc = parser.parse(schema);
+	    metadataNamespace = XPathAPI.eval(schemaDoc, "/xsd:schema/@targetNamespace", xmlnsEl).str();
+	} catch (SAXParseException e) {
+// 	    logger.debug("assume unreadable schema is for empty namespace", e);
+	    metadataNamespace = "";
+	} catch (Exception e) {
+// 	    logger.warn("Problem obtaining namespace", e);
+	    System.out.println("Failed to get schema: " + schema);
+	    e.printStackTrace();
+	    metadataNamespace = "";
+	}
+	CrosswalkItem crosswalkItem = new CrosswalkItem(nativeRecordSchema, metadataPrefix, schema, metadataNamespace, NodePassThruCrosswalk.class);
+	crosswalkItemList.add(crosswalkItem);
+// 	logger.debug("Service_srw2oai.crosswalkItemFactory: metadataPrefix=" + metadataPrefix);
+// 	logger.debug("Service_srw2oai.crosswalkItemFactory: nativeRecordSchema=" + crosswalkItem.getNativeRecordSchema());
+// 	if (nativeRecordSchema.equals("info:srw/schema/1/marcxml-v1.1")) {
+// 	}
+	return crosswalkItemList.toArray();
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+	return identifier;
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+	return getLocalIdentifier(nativeItem);
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+//  	logger.debug("getLocalIdentifier: nativeItem=" + nativeItem);
+	HashMap hashMap = (HashMap)nativeItem;
+	Node dataNode = (Node) hashMap.get("header");
+// 	MessageElement messageElement = dataNode.get_any()[0];
+//	StringBuffer sb = new StringBuffer();
+	try {
+// 	    Element recordEl = messageElement.getAsDOM();
+	    Element recordEl = (Element)dataNode;
+// 	    logger.debug("NodeRecordFactory.getLocalIdentifier: recordEl=" + recordEl);
+	    Node identifierNode =
+		XPathAPI.selectSingleNode(
+					  recordEl,
+					  "/oai:header/oai:identifier",
+					  xmlnsEl);
+// 	    logger.debug("NodeRecordFactory.getLocalIdentifier: identifierNode=" + identifierNode);
+	    if (identifierNode != null) {
+		return XPathAPI.eval(identifierNode, "string()").str();
+	    }
+	} catch (Exception e) {
+	    e.printStackTrace();
+	}
+	return null;
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException {
+	HashMap hashMap = (HashMap)nativeItem;
+	Node dataNode = (Node) hashMap.get("header");
+// 	MessageElement messageElement = dataNode.get_any()[0];
+	try {
+// 	    Element recordEl = messageElement.getAsDOM();
+	    Element recordEl = (Element)dataNode;
+	    Node datestampNode =
+		XPathAPI.selectSingleNode(
+					  recordEl,
+					  "/oai:header/oai:datestamp",
+					  xmlnsEl);
+	    if (datestampNode != null) {
+		return XPathAPI.eval(datestampNode, "string()").str();
+	    }
+	} catch (Exception e) {
+	    e.printStackTrace();
+	}
+	return null;
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException {
+	//         List setSpecs = (List)((HashMap)nativeItem).get("setSpecs");
+	//         if (setSpecs != null) 
+	//             return setSpecs.iterator();
+	//         else
+	return null;
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem)
+	throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(
+			      Object nativeItem,
+			      String schemaLocation,
+			      String metadataPrefix) {
+	return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/RecordFactory.java b/src/ORG/oclc/oai/server/catalog/RecordFactory.java
new file mode 100644
index 0000000..b412eda
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/RecordFactory.java
@@ -0,0 +1,509 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+// import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Vector;
+
+import ORG.oclc.oai.server.crosswalk.Crosswalk;
+import ORG.oclc.oai.server.crosswalk.CrosswalkItem;
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * RecordFactory is responsible for pulling various pieces of information from native records
+ * such as the identifier, datestamp, and sets. Note that the Crosswalk implementations
+ * are responsible for the creating the &lt;metadata&gt; part of the response from
+ * native records.
+ */
+public abstract class RecordFactory {
+    public static final boolean debug=false;
+//     private boolean encodeSetSpec = true;
+    
+    /**
+     * Container for the crosswalk(s) supported by this factory
+     */
+    private Crosswalks crosswalks;
+
+    /**
+     * Construct a RecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     *
+     * @param properties Contains information to configure the factory;
+     *                   specifically, the names of crosswalk(s) supported
+     */
+    public RecordFactory(Properties properties) {
+//         if ("false".equals(properties.getProperty("RecordFactory.encodeSetSpec"))) {
+//             encodeSetSpec = false;
+//         }
+	crosswalks = new Crosswalks(properties);
+    }
+
+    public RecordFactory(HashMap crosswalkMap) {
+	crosswalks = new Crosswalks(crosswalkMap);
+    }
+
+    /**
+     * Retrieve the crosswalk(s) container
+     *
+     * @return Crosswalk(s) container
+     */
+    public Crosswalks getCrosswalks() { return crosswalks; }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier
+     * @return local identifier
+     */
+    public abstract String fromOAIIdentifier(String identifier);
+
+    /**
+     * Get a list of supported schemaLocations for the specified native record.
+     * @param nativeItem native database record
+     * @return A Vector containing all the schemaLocations this record can support.
+     * @exception NoMetadatdaFormatsException This record doesn't support any of the
+     * available schemaLocations for this repository.
+     */
+    public Vector getSchemaLocations(Object nativeItem)
+	throws NoMetadataFormatsException {
+	if (isDeleted(nativeItem)) {
+	    throw new NoMetadataFormatsException();
+	}
+	Vector v = new Vector();
+	Iterator iterator = getCrosswalks().iterator();
+	while (iterator.hasNext()) {
+	    Map.Entry entry = (Map.Entry)iterator.next();
+	    CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
+	    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
+	    if (crosswalk.isAvailableFor(nativeItem)) {
+		v.add(crosswalk.getSchemaLocation());
+	    }
+	}
+	return v;
+    }
+
+    /**
+     * Convert a native "item" to a "record" String. Use this version of createHeader
+     * if the setSpecs are derived from the nativeItem itself.
+     *
+     * @param nativeItem the native record.
+     * @return String[0] = "header" XML string String[1] = oai-identifier.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     */
+    public String[] createHeader(Object nativeItem)
+	throws IllegalArgumentException {
+	    return createHeader(
+				getOAIIdentifier(nativeItem),
+				getDatestamp(nativeItem),
+				getSetSpecs(nativeItem),
+				isDeleted(nativeItem));
+	}
+    
+    /**
+     * Convert a native "item" to a "record" String. Use this version of createHeader
+     * if the setSpecs are supplied from a source other than the nativeItem itself.
+     *
+     * @param nativeItem the native record.
+     * @param setSpecs a setSpec iterator
+     * @return String[0] = "header" XML string String[1] = oai-identifier.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     */
+    public String[] createHeader(Object nativeItem, Iterator setSpecs)
+	throws IllegalArgumentException {
+	    if (setSpecs == null) {
+		return createHeader(nativeItem);
+	    } else {
+		return createHeader(
+				    getOAIIdentifier(nativeItem),
+				    getDatestamp(nativeItem),
+				    setSpecs,
+				    isDeleted(nativeItem));
+	    }
+	}
+    
+    /**
+     * Create a "record" string from the specified components.
+     *
+     * @param identifier the OAI identifier
+     * @param datestamp the datestamp
+     * @param setSpecs 
+     * @param isDeleted a flag indicating the status of the record.
+     * @return String[0] = "header" XML string String[1] = oai-identifier.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     */
+    public static String[] createHeader(String identifier,
+				 String datestamp, Iterator setSpecs, boolean isDeleted)
+	throws IllegalArgumentException {
+	    StringBuffer xmlHeader = new StringBuffer();
+	    xmlHeader.append("<header");
+	    if (isDeleted) {
+		xmlHeader.append(" status=\"deleted\"");
+	    }
+	    xmlHeader.append("><identifier>");
+	    xmlHeader.append(OAIUtil.xmlEncode(identifier));
+	    xmlHeader.append("</identifier><datestamp>");
+	    xmlHeader.append(datestamp);
+	    xmlHeader.append("</datestamp>");
+	    if (setSpecs != null) {
+		while (setSpecs.hasNext()) {
+		    xmlHeader.append("<setSpec>");
+//                     try {
+//                         if (encodeSetSpec) {
+//                             xmlHeader.append(URLEncoder.encode((String)setSpecs.next(), "UTF-8"));
+//                         } else {
+                            xmlHeader.append((String)setSpecs.next());
+//                         }
+                        
+//                     } catch (UnsupportedEncodingException e) {
+//                         e.printStackTrace();
+//                         throw new OAIInternalServerError(e.getMessage());
+//                     }
+		    xmlHeader.append("</setSpec>");
+		}
+	    }
+	    xmlHeader.append("</header>");
+	    return new String[] {
+		xmlHeader.toString(),
+		identifier
+	    };
+	}
+
+    /**
+     * Create the &lt;record&gt; for the given record and metadataFormat selection. Use this
+     * version of create if the setSpecs can be derived from the nativeItem itself.
+     *
+     * @param nativeItem the native record
+     * @param schemaURL the schemaURL desired for the response
+     * @param metadataPrefix 
+     * @return a String containing the OAI record response.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @throws CannotDisseminateFormatException 
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public String create(Object nativeItem, String schemaURL, String metadataPrefix)
+	throws IllegalArgumentException, CannotDisseminateFormatException {
+	return create(nativeItem, schemaURL, metadataPrefix, (Iterator)null, (Iterator)null);
+    }
+
+    /**
+     * Create the &lt;record&gt; for the given record and metadataFormat selection.
+     * Use this version of create if the setSpecs are derived from a source other
+     * other than the nativeItem itself.
+     *
+     * @param nativeItem the native record
+     * @param schemaURL the schemaURL desired for the response
+     * @param metadataPrefix 
+     * @param setSpecs 
+     * @param abouts 
+     * @return a String containing the OAI record response.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @throws CannotDisseminateFormatException 
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public String create(Object nativeItem, String schemaURL, String metadataPrefix,
+			 Iterator setSpecs, Iterator abouts)
+	throws IllegalArgumentException, CannotDisseminateFormatException {
+        if (isDeleted(nativeItem)) {
+            StringBuffer sb = new StringBuffer("<record>");
+            sb.append(createHeader(nativeItem)[0]);
+            sb.append("</record>");
+            return sb.toString();
+        }
+        String result = quickCreate(nativeItem, schemaURL, metadataPrefix);
+        if (result == null) {
+            if (setSpecs == null) {
+                setSpecs = getSetSpecs(nativeItem);
+            }
+            if (abouts == null) {
+                abouts = getAbouts(nativeItem);
+            }
+            result = create(nativeItem, schemaURL, metadataPrefix,
+                            getOAIIdentifier(nativeItem),
+                            getDatestamp(nativeItem),
+                            setSpecs,
+                            abouts,
+                            isDeleted(nativeItem));
+        }
+      
+        return result;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record.
+     * @param nativeItem the native record
+     * @param schemaURL the schemaURL desired for the response
+     * @param metadataPrefix 
+     * @return a String containing the OAI record response.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @throws CannotDisseminateFormatException 
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public abstract String quickCreate(Object nativeItem, String schemaURL, String metadataPrefix)
+	throws IllegalArgumentException, CannotDisseminateFormatException;
+
+    public String create(Object nativeItem, String schemaURL, String identifier,
+            String datestamp, Iterator setSpecs, Iterator abouts, boolean isDeleted)
+    throws IllegalArgumentException, CannotDisseminateFormatException {
+        return create(nativeItem, schemaURL, null, identifier, datestamp,
+                setSpecs, abouts, isDeleted);
+    }
+    
+    /**
+     * Convert a native "item" to a "record" object.
+     *
+     * @param nativeItem native "Item"
+     * @param schemaURL the schemaURL of the desired metadataFormat
+     *                  or NULL for a Record object containing all
+     *                  the supported Crosswalk(s)
+     * @return "record" String
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public String create(Object nativeItem, String schemaURL,
+            String metadataPrefix, String identifier,
+            String datestamp, Iterator setSpecs, Iterator abouts, boolean isDeleted)
+    throws IllegalArgumentException, CannotDisseminateFormatException {
+        if (debug) System.out.println("RecordFactory.create");
+        StringBuffer xmlRec = new StringBuffer();
+        xmlRec.append("<record><header");
+        if (isDeleted) {
+            xmlRec.append(" status=\"deleted\"");
+        }
+        xmlRec.append("><identifier>");
+        xmlRec.append(OAIUtil.xmlEncode(identifier));
+        xmlRec.append("</identifier><datestamp>");
+        xmlRec.append(datestamp);
+        xmlRec.append("</datestamp>");
+        if (setSpecs != null) {
+            while (setSpecs.hasNext()) {
+                xmlRec.append("<setSpec>");
+//              try {
+//              if (encodeSetSpec) {
+//              xmlRec.append(URLEncoder.encode((String)setSpecs.next(), "UTF-8"));
+//              } else {
+                xmlRec.append((String)setSpecs.next());
+//              }
+//              } catch (UnsupportedEncodingException e) {
+//              e.printStackTrace();
+//              throw new OAIInternalServerError(e.getMessage());
+//              }
+                xmlRec.append("</setSpec>");
+            }
+        }
+        xmlRec.append("</header>");
+        if (debug) System.out.println("RecordFactory.create: header finished");
+        if (!isDeleted) {
+            if (debug) System.out.println("RecordFactory.create: starting metadata");
+            xmlRec.append("<metadata>");
+            Iterator iterator = getCrosswalks().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry entry = (Map.Entry)iterator.next();
+                String itemPrefix = (String) entry.getKey();
+                CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
+                Crosswalk crosswalk = crosswalkItem.getCrosswalk();
+                if (debug) System.out.println("RecordFactory.create: crosswalk=" + crosswalk);
+                if (schemaURL == null
+                        || (metadataPrefix == null && crosswalk.getSchemaURL().equals(schemaURL))
+                        || (metadataPrefix != null && itemPrefix.equals(metadataPrefix))) {
+                    xmlRec.append(crosswalk.createMetadata(nativeItem));
+                    break;
+                }
+            }
+            xmlRec.append("</metadata>");
+            if (debug) System.out.println("RecordFactory.create: finished metadata");
+            if (abouts != null) {
+                while (abouts.hasNext()) {
+                    xmlRec.append("<about>");
+                    xmlRec.append((String)abouts.next());
+                    xmlRec.append("</about>");
+                }
+            }
+        }
+        xmlRec.append("</record>");
+        if (debug) System.out.println("RecordFactory.create: return=" + xmlRec.toString());
+        return xmlRec.toString();
+    }
+
+    /**
+     * Create the &lt;record&gt; for the given record and metadataFormat selection. Use this
+     * version of create if the setSpecs can be derived from the nativeItem itself.
+     *
+     * @param nativeItem the native record
+     * @param schemaURL the schemaURL desired for the response
+     * @param metadataPrefix 
+     * @return a String containing the OAI record response.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @throws CannotDisseminateFormatException 
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public String createMetadata(Object nativeItem, String schemaURL, String metadataPrefix)
+	throws IllegalArgumentException, CannotDisseminateFormatException {
+	return createMetadata(nativeItem, schemaURL, metadataPrefix, (Iterator)null, (Iterator)null);
+    }
+
+    /**
+     * Create the &lt;record&gt; for the given record and metadataFormat selection.
+     * Use this version of create if the setSpecs are derived from a source other
+     * other than the nativeItem itself.
+     *
+     * @param nativeItem the native record
+     * @param schemaURL the schemaURL desired for the response
+     * @param metadataPrefix 
+     * @param setSpecs 
+     * @param abouts 
+     * @return a String containing the OAI record response.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @throws CannotDisseminateFormatException 
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public String createMetadata(Object nativeItem, String schemaURL, String metadataPrefix,
+			 Iterator setSpecs, Iterator abouts)
+	throws IllegalArgumentException, CannotDisseminateFormatException {
+        if (isDeleted(nativeItem)) {
+            throw new CannotDisseminateFormatException("Record is deleted.");
+        }
+        String result = quickCreateMetadata(nativeItem, schemaURL, metadataPrefix);
+        if (result == null) {
+            if (setSpecs == null) {
+                setSpecs = getSetSpecs(nativeItem);
+            }
+            if (abouts == null) {
+                abouts = getAbouts(nativeItem);
+            }
+            result = createMetadata(nativeItem, schemaURL,
+                            isDeleted(nativeItem));
+        }
+      
+        return result;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default createMetadata() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record.
+     * @param nativeItem the native record
+     * @param schemaURL the schemaURL desired for the response
+     * @param metadataPrefix 
+     * @return a String containing the OAI record response.
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public String quickCreateMetadata(Object nativeItem, String schemaURL, String metadataPrefix)
+	throws IllegalArgumentException {
+        return null;
+    }
+
+    /**
+     * Convert a native "item" to a "record" object.
+     *
+     * @param nativeItem native "Item"
+     * @param schemaURL the schemaURL of the desired metadataFormat
+     *                  or NULL for a Record object containing all
+     *                  the supported Crosswalk(s)
+     * @return "record" String
+     * @exception IllegalArgumentException One of the header components for this record is bad.
+     * @exception This nativeItem doesn't support the specified metadataPrefix
+     */
+    public String createMetadata(Object nativeItem, String schemaURL, boolean isDeleted)
+	throws IllegalArgumentException, CannotDisseminateFormatException {
+	StringBuffer xmlRec = new StringBuffer();
+	if (isDeleted) {
+            throw new CannotDisseminateFormatException("Record is deleted");
+	}
+        Iterator iterator = getCrosswalks().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry entry = (Map.Entry)iterator.next();
+            CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
+	    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
+            if (schemaURL == null
+                || crosswalk.getSchemaURL().equals(schemaURL)) {
+                xmlRec.append(crosswalk.createMetadata(nativeItem));
+            }
+        }
+	return xmlRec.toString();
+    }
+
+    /**
+     * Construct an OAI identifier from the native record
+     *
+     * Caveat: This brazenly assumes the record's local identifier
+     * exists somewhere within the native, or can be
+     * generated on the fly.
+     *
+     * @param nativeItem the native record
+     * @return String OAI identifier
+     */
+    public abstract String getOAIIdentifier(Object nativeItem);
+
+    /**
+     * Extract the local identifier from the native record.
+     * Since I forgot to add this originally, it is a roundabout
+     * way to get this. It's a good idea to override this method
+     * to do it more directly.
+     *
+     * @param nativeItem the native record
+     * @return String local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+	return fromOAIIdentifier(getOAIIdentifier(nativeItem));
+    }
+    
+    /**
+     * Construct an OAI datestamp from the native record
+     *
+     * Caveat: This brazenly assumes the record's datestamp
+     * exists somewhere within the native record, or can be
+     * generated on the fly.
+     *
+     * @param nativeItem the native record
+     * @return String OAI identifier
+     */
+    public abstract String getDatestamp(Object nativeItem);
+
+    /**
+     * Get an Iterator containing all the setSpecs for this native record.
+     * Return null if the setSpecs are derived from
+     * a source other than the nativeItem itself. In this case, the AbstractCatalog
+     * is responsible for obtaining the setSpecs which can be passed to the
+     * create() and createHeader() methods along with the nativeItem.
+     *
+     * @param nativeItem the native record
+     * @return an Iterator containing setSpec String
+     */
+    public abstract Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException;
+    /**
+     * Get a boolean indicating if this native record is deleted
+     *
+     * @param nativeItem the native record
+     * @return true if record is deleted, false otherwise.
+     */
+    public abstract boolean isDeleted(Object nativeItem);
+    /**
+     * Get an Iterator containing all the &lt;about&gt; entries for this native record
+     * Return null if the abouts are derived from a source outside the native record itself.
+     * In this case, the AbstractCatalog is responsible for obtaining the abouts which
+     * can be passed to the create() method along with the nativeItem.
+     *
+     * @param nativeItem the native record
+     * @return an Iterator containing &lt;about&gt; XML String
+     */
+    public abstract Iterator getAbouts(Object nativeItem);
+}
diff --git a/src/ORG/oclc/oai/server/catalog/SRUOAICatalog.java b/src/ORG/oclc/oai/server/catalog/SRUOAICatalog.java
new file mode 100644
index 0000000..c518459
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/SRUOAICatalog.java
@@ -0,0 +1,857 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.xpath.XPathAPI;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import ORG.oclc.oai.server.verb.BadArgumentException;
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+public class SRUOAICatalog extends AbstractCatalog {
+    private static final boolean debug = false;
+    private String sruURL;
+    private String sortKeys = "";
+    protected int maxListSize;
+    private TreeMap sets = null;
+    private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
+    private static Transformer transformer = null;
+    private static HashMap builderMap = new HashMap();
+    private static Element xmlnsEl = null;
+    private static DocumentBuilderFactory factory =
+        DocumentBuilderFactory.newInstance();
+    
+    static {
+        try {
+            transformer = transformerFactory.newTransformer();
+            factory.setNamespaceAware(true);
+            DocumentBuilder builder = getBuilder();
+            DOMImplementation impl = builder.getDOMImplementation();
+            Document xmlnsDoc
+            = impl.createDocument("http://www.oclc.org/research/software/oai/harvester",
+                    "harvester:xmlnsDoc", null);
+            xmlnsEl = xmlnsDoc.getDocumentElement();
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                    "xmlns:srw",
+            "http://www.loc.gov/zing/srw/");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+    
+    public SRUOAICatalog(Properties properties) {
+        this(properties, properties.getProperty("SRUOAICatalog.sruURL"));
+    }
+    
+    public SRUOAICatalog(Properties properties, String sruURL) {
+        sortKeys = properties.getProperty("SRUOAICatalog.sortKeys");
+        if (sortKeys == null) sortKeys = "";
+        
+        String maxListSize =
+            properties.getProperty("SRUOAICatalog.maxListSize");
+        
+        if (maxListSize == null) {
+            throw new IllegalArgumentException("SRUOAICatalog.maxListSize is missing from the properties file");
+        } else {
+            this.maxListSize = Integer.parseInt(maxListSize);
+        }
+        
+        this.sruURL = sruURL;
+        sets = getSets(properties);
+    }
+    
+    private static TreeMap getSets(Properties properties) {
+        TreeMap treeMap = new TreeMap();
+        return treeMap;
+    }
+    
+    private static DocumentBuilder getBuilder() throws ParserConfigurationException {
+        Thread currentThread = Thread.currentThread();
+        DocumentBuilder builder = (DocumentBuilder) builderMap.get(currentThread);
+        if (builder == null) {
+            builder = factory.newDocumentBuilder();
+            builderMap.put(currentThread, builder);
+        }
+        return builder;
+    }
+    
+    private String normalizeTerm(String term) {
+        return term;
+    }
+    
+    public Map listSets() throws NoSetHierarchyException {
+        if (sets.size() == 0)
+            throw new NoSetHierarchyException();
+        Map listSetsMap = new LinkedHashMap();
+        try {
+            Object[] keys = sets.keySet().toArray();
+            Object[] values = sets.values().toArray();
+            ArrayList newList = new ArrayList();
+            for (int i = 0; i < keys.length; ++i) {
+                newList.add(values[i]);
+            }
+            listSetsMap.put("sets", newList.iterator());
+        } catch (Throwable e) {
+            System.err.println("SRUOAICatalog.listSets: browse failed");
+            e.printStackTrace();
+        }
+        
+        return listSetsMap;
+    }
+    
+    public Map listSets(String resumptionToken)
+    throws BadResumptionTokenException {
+        throw new BadResumptionTokenException();
+    }
+    
+    /**
+     * get a map containing a key="headers" value=Iterator and
+     * a key="resumptionToken" value=String. The "headers" Map
+     * contains Map.Entrys where key="header" value="deleted" or null.
+     */
+    public Map listIdentifiers(String from,
+            String until,
+            String set,
+            String metadataPrefix)
+    throws
+    BadArgumentException,
+    NoItemsMatchException,
+    OAIInternalServerError {
+        if (set != null
+                && set.length() > 0
+                && from.equals(toFinestFrom("0000-00-00"))
+                && until.equals(toFinestUntil("9999-99-99"))) {
+            from = null;
+            until = null;
+        }
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        Document srResponse;
+        try {
+            srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        from,
+                        until,
+                        set,
+                        "http://www.openarchives.org/OAI/2.0/#header",
+                        1,
+                        maxListSize,
+                "xml");
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+        try {
+            NodeList nodeList = getRecords(srResponse);
+            if (nodeList != null) {
+                RecordFactory recordFactory = getRecordFactory();
+                for (int i = 0; i < nodeList.getLength(); ++i) {
+                    Object rec = getRecordData(nodeList.item(i));
+                    HashMap hashMap = new HashMap();
+                    hashMap.put("header", rec);
+                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
+                    try {
+                        hashMap.put("metadata", getNativeMetadata(localIdentifier,
+                                metadataPrefix));
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (TransformerException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (ParserConfigurationException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (SAXException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    }
+                    String[] header = recordFactory.createHeader(hashMap);
+                    headers.add(header[0]);
+                    identifiers.add(header[1]);
+                }
+                try {
+                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
+                    String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
+                    if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                        StringBuffer resumptionToken = new StringBuffer();
+                        resumptionToken.append(resultSetId);
+                        resumptionToken.append(":");
+                        resumptionToken.append(nextRecordPosition);
+                        resumptionToken.append(":");
+                        resumptionToken.append(metadataPrefix);
+                        listIdentifiersMap.put(
+                                "resumptionMap",
+                                getResumptionMap(resumptionToken.toString()));
+                    }
+                } catch (TransformerException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            } else {
+                throw new NoItemsMatchException();
+            }
+            listIdentifiersMap.put("headers", headers.iterator());
+            listIdentifiersMap.put("identifiers", identifiers.iterator());
+            return listIdentifiersMap;
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * get a map containing a key="headers" value=Iterator and
+     * a key="resumptionToken" value=String. The "headers" Iterator
+     * contains Map.Entrys where key="header" value="deleted" or null.
+     */
+    public Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resultSetId;
+        String nextRecordPosition;
+        String metadataPrefix;
+        try {
+            resultSetId = tokenizer.nextToken();
+            nextRecordPosition = tokenizer.nextToken();
+            metadataPrefix = tokenizer.nextToken();
+            if (metadataPrefix.equals("null"))
+                metadataPrefix = null;
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        try {
+            Document srResponse;
+            try {
+                srResponse =
+                    getSearchRetrieveResponse(sruURL,
+                            resultSetId,
+                            nextRecordPosition,
+                            "http://www.openarchives.org/OAI/2.0/#header",
+                            maxListSize,
+                    "xml");
+            } catch (IOException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            } catch (SAXException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            } catch (ParserConfigurationException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+            NodeList nodeList = getRecords(srResponse);
+            if (nodeList != null) {
+                RecordFactory recordFactory = getRecordFactory();
+                for (int i = 0; i < nodeList.getLength(); ++i) {
+                    
+                    Object rec = getRecordData(nodeList.item(i));
+                    HashMap hashMap = new HashMap();
+                    hashMap.put("header", rec);
+                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
+                    hashMap.put("metadata", getNativeMetadata(localIdentifier,
+                            metadataPrefix));
+                    String[] header = recordFactory.createHeader(hashMap);
+                    headers.add(header[0]);
+                    identifiers.add(header[1]);
+                }
+                nextRecordPosition = XPathAPI.eval(srResponse,
+                        "/srw:searchRetrieveResponse/srw:nextRecordPosition",
+                        xmlnsEl).str();
+                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                    StringBuffer newResumptionToken = new StringBuffer();
+                    newResumptionToken.append(resultSetId);
+                    newResumptionToken.append(":");
+                    newResumptionToken.append(nextRecordPosition);
+                    newResumptionToken.append(":");
+                    newResumptionToken.append(metadataPrefix);
+                    listIdentifiersMap.put(
+                            "resumptionMap",
+                            getResumptionMap(newResumptionToken.toString()));
+                }
+            }
+            listIdentifiersMap.put("headers", headers.iterator());
+            listIdentifiersMap.put("identifiers", identifiers.iterator());
+        } catch (Throwable e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+        return listIdentifiersMap;
+    }
+    
+    /**
+     * get an Iterator containing Map.Entrys where key=metadataPrefix
+     * and value=schema.
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+    throws
+    IdDoesNotExistException,
+    NoMetadataFormatsException,
+    OAIInternalServerError {
+        Vector result = new Vector();
+        NodeList nodeList = null;
+        try {
+            nodeList = getIdentifierRecords(sruURL, oaiIdentifier, (String) null);
+        } catch (Throwable e) {
+            throw new OAIInternalServerError("Database failure");
+        }
+        if (nodeList != null) {
+            for (int i = 0; i < nodeList.getLength(); ++i) {
+                try {
+                    Object rec = getRecordData(nodeList.item(i));
+                    if (rec != null) {
+                        Vector schemaLocations =
+                            getRecordFactory().getSchemaLocations(rec);
+                        for (int j = 0; j < schemaLocations.size(); ++j) {
+                            result.add(schemaLocations.get(j));
+                        }
+                    } else {
+                        throw new OAIInternalServerError("Null Record");
+                    }
+                } catch (TransformerException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                } catch (SAXException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                } catch (ParserConfigurationException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            }
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+        return result;
+    }
+    
+    /**
+     * get a DocumentFragment containing the specified record
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+    throws
+    IdDoesNotExistException,
+    IdDoesNotExistException,
+    CannotDisseminateFormatException,
+    OAIInternalServerError {
+        Object nativeObject;
+        try {
+            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+        
+        
+        if (nativeObject != null) {
+            String schemaURL = null;
+            
+            if (metadataPrefix != null) {
+                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                        == null) {
+                    throw new CannotDisseminateFormatException(metadataPrefix);
+                }
+            }
+            try {
+                String s =
+                    getRecordFactory().create(
+                            nativeObject,
+                            schemaURL,
+                            metadataPrefix);
+                return s;
+            } catch (CannotDisseminateFormatException e) {
+                e.printStackTrace();
+                throw e;
+            }
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+    
+    /**
+     * get a DocumentFragment containing the specified record
+     */
+    public String getMetadata(String oaiIdentifier, String metadataPrefix)
+    throws
+    IdDoesNotExistException,
+    IdDoesNotExistException,
+    CannotDisseminateFormatException,
+    OAIInternalServerError {
+        Object nativeObject;
+        try {
+            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+        
+        if (debug) {
+            System.out.println(nativeObject);
+        }
+        if (nativeObject != null) {
+            String schemaURL = null;
+            
+            if (metadataPrefix != null) {
+                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                        == null) {
+                    if (debug) {
+                        System.out.println(
+                        "SRUOAICatalog.getRecord: metadataPrefix not found");
+                    }
+                    throw new CannotDisseminateFormatException(metadataPrefix);
+                }
+            }
+            try {
+                return getRecordFactory().createMetadata(
+                        nativeObject,
+                        schemaURL,
+                        metadataPrefix);
+            } catch (CannotDisseminateFormatException e) {
+                e.printStackTrace();
+                throw e;
+            }
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+    
+    private NodeList getIdentifierRecords(String sruURL,
+            String oaiIdentifier,
+            String metadataPrefix)
+    throws TransformerException, SAXException, IOException,
+    ParserConfigurationException {
+        Document srResponse;
+        String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+        srResponse =
+            getSearchRetrieveResponse(sruURL,
+                    localIdentifier,
+                    "http://www.openarchives.org/OAI/2.0/#header",
+            "xml");
+        return getRecords(srResponse);
+    }
+    
+    private Object getFullRecord(String sruURL,
+            String oaiIdentifier,
+            String metadataPrefix)
+    throws SAXException, TransformerException, IOException,
+    ParserConfigurationException {
+        NodeList nodeList = getIdentifierRecords(sruURL, oaiIdentifier, metadataPrefix);
+        if (nodeList != null) {
+            HashMap hashMap = new HashMap();
+            hashMap.put("header", getRecordData(nodeList.item(0)));
+            String nativeRecordSchema = getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
+            String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+            Document srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        localIdentifier,
+                        nativeRecordSchema,
+                "xml");
+            nodeList = getRecords(srResponse);
+            hashMap.put("metadata", getRecordData(nodeList.item(0)));
+            return hashMap;
+        }
+        return null;
+    }
+    
+    private Object getNativeMetadata(String localIdentifier, String metadataPrefix)
+    throws TransformerException, SAXException, IOException,
+    ParserConfigurationException {
+        String nativeRecordSchema = getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
+        Document srResponse;
+        srResponse =
+            getSearchRetrieveResponse(sruURL,
+                    localIdentifier,
+                    nativeRecordSchema,
+            "xml");
+        NodeList nodeList = getRecords(srResponse);
+        return getRecordData(nodeList.item(0));
+    }
+    
+    public Map listRecords(String from,
+            String until,
+            String set,
+            String metadataPrefix)
+    throws
+    BadArgumentException,
+    CannotDisseminateFormatException,
+    NoItemsMatchException,
+    OAIInternalServerError {
+        if (set != null
+                && set.length() > 0
+                && from.equals(toFinestFrom("0000-00-00"))
+                && until.equals(toFinestUntil("9999-99-99"))) {
+            from = null;
+            until = null;
+        }
+        if (toFinestFrom("0000-00-00").equals(from)) {
+            from = null;
+        }
+        if (toFinestUntil("9999-12-31").compareTo(until) <= 0) {
+            until = null;
+        }
+        Map listRecordsMap = new HashMap();
+        ArrayList recordsList = new ArrayList();
+        Document srResponse;
+        try {
+            srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        from,
+                        until,
+                        set,
+                        "http://www.openarchives.org/OAI/2.0/#header",
+                        1,
+                        maxListSize,
+                "xml");
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+        try {
+            NodeList nodeList = getRecords(srResponse);
+            if (nodeList != null) {
+                RecordFactory recordFactory = getRecordFactory();
+                String schemaURL = null;
+                if (metadataPrefix != null) {
+                    if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                            == null)
+                        throw new CannotDisseminateFormatException(metadataPrefix);
+                }
+                for (int i = 0; i < nodeList.getLength(); ++i) {
+                    Object rec = getRecordData(nodeList.item(i));
+                    HashMap hashMap = new HashMap();
+                    hashMap.put("header", rec);
+                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
+                    try {
+                        hashMap.put("metadata", getNativeMetadata(localIdentifier,
+                                metadataPrefix));
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (ParserConfigurationException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (SAXException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    }
+                    recordsList.add(
+                            recordFactory.create(hashMap,
+                                    schemaURL,
+                                    metadataPrefix));
+                }
+                String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
+                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
+                    StringBuffer resumptionToken = new StringBuffer();
+                    resumptionToken.append(resultSetId);
+                    resumptionToken.append(":");
+                    resumptionToken.append(nextRecordPosition);
+                    resumptionToken.append(":");
+                    resumptionToken.append(metadataPrefix);
+                    listRecordsMap.put(
+                            "resumptionMap",
+                            getResumptionMap(resumptionToken.toString()));
+                }
+            } else {
+                throw new NoItemsMatchException();
+            }
+            listRecordsMap.put("records", recordsList.iterator());
+            return listRecordsMap;
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resultSetId;
+        String nextRecordPosition;
+        String metadataPrefix;
+        
+        try {
+            resultSetId = tokenizer.nextToken();
+            nextRecordPosition = tokenizer.nextToken();
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            e.printStackTrace();
+            throw new BadResumptionTokenException();
+        }
+        Map listRecordsMap = new HashMap();
+        ArrayList recordsList = new ArrayList();
+        try {
+            Document srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        resultSetId,
+                        nextRecordPosition,
+                        "http://www.openarchives.org/OAI/2.0/#header",
+                        maxListSize,
+                "xml");
+            NodeList nodeList = getRecords(srResponse);
+            RecordFactory recordFactory = getRecordFactory();
+            String schemaURL = null;
+            if (metadataPrefix != null) {
+                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                        == null)
+                    throw new CannotDisseminateFormatException(metadataPrefix);
+            }
+            for (int i = 0; i < nodeList.getLength(); ++i) {
+                Object rec = getRecordData(nodeList.item(i));
+                HashMap hashMap = new HashMap();
+                hashMap.put("header", rec);
+                String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
+                hashMap.put("metadata", getNativeMetadata(localIdentifier,
+                        metadataPrefix));
+                recordsList.add(
+                        recordFactory.create(hashMap,
+                                schemaURL,
+                                metadataPrefix));
+            }
+            nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
+            if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
+                StringBuffer newResumptionToken = new StringBuffer();
+                newResumptionToken.append(resultSetId);
+                newResumptionToken.append(":");
+                newResumptionToken.append(nextRecordPosition);
+                newResumptionToken.append(":");
+                newResumptionToken.append(metadataPrefix);
+                listRecordsMap.put(
+                        "resumptionMap",
+                        getResumptionMap(newResumptionToken.toString()));
+            }
+            listRecordsMap.put("records", recordsList.iterator());
+        } catch (Throwable e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+        return listRecordsMap;
+    }
+    
+    private Document
+    getSearchRetrieveResponse(String sruURL,
+            String from,
+            String until,
+            String set,
+            String recordSchema,
+            int startRecord,
+            int maximumRecords,
+            String recordPacking)
+    throws SAXException, ParserConfigurationException,
+    UnsupportedEncodingException, IOException {
+        StringBuffer query = new StringBuffer();
+        if ((from != null && from.length() > 0)
+                || (until != null && until.length() > 0)) {
+            query.append("(");
+            if (from != null && from.length() > 0) {
+                query.append("oai.datestamp>=\"");
+                query.append(normalizeTerm(from));
+                query.append("\"");
+            }
+            if (until != null && from.length() > 0) {
+                if (query.length() > 0)
+                    query.append(" and ");
+                query.append("oai.datestamp<=\"");
+                query.append(normalizeTerm(until));
+                query.append("\"");
+            }
+            query.append(")");
+        }
+        if (set != null && set.length() > 0) {
+            if (query.length() > 0)
+                query.append(" and ");
+            query.append("oai.set=\"");
+            query.append(normalizeTerm(set));
+            query.append("\"");
+        }
+        StringBuffer request = new StringBuffer(sruURL);
+        request.append("?operation=searchRetrieve&version=1.1&resultSetTTL=600&query=");
+        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
+        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
+        request.append("&startRecord=").append(Integer.toString(startRecord));
+        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
+        request.append("&recordPacking=").append(recordPacking);
+        request.append("&sortKeys=").append(URLEncoder.encode(sortKeys, "UTF-8"));
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(request.toString());
+    }
+    
+    private Document
+    getSearchRetrieveResponse(String sruURL,
+            String resultSetId,
+            String nextRecordPosition,
+            String recordSchema,
+            int maximumRecords,
+            String recordPacking)
+    throws SAXException, ParserConfigurationException,
+    UnsupportedEncodingException, IOException {
+        StringBuffer query = new StringBuffer();
+        query.append("cql.resultSetId=");
+        query.append(resultSetId);
+        StringBuffer request = new StringBuffer(sruURL);
+        request.append("?operation=searchRetrieve&resultSetTTL=600&version=1.1&query=");
+        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
+        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
+        request.append("&startRecord=").append(nextRecordPosition);
+        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
+        request.append("&recordPacking=").append(recordPacking);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(request.toString());
+    }
+    
+    private Document
+    getSearchRetrieveResponse(String sruURL,
+            String localIdentifier,
+            String recordSchema,
+            String recordPacking)
+    throws SAXException, IOException, ParserConfigurationException,
+    UnsupportedEncodingException {
+        StringBuffer query = new StringBuffer();
+        query.append("oai.identifier exact \"");
+        query.append(localIdentifier);
+        query.append("\"");
+        
+        StringBuffer request = new StringBuffer(sruURL);
+        request.append("?operation=searchRetrieve&resultSetTTL=0&version=1.1&query=");
+        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
+        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
+        request.append("&startRecord=1&maximumRecords=1");
+        request.append("&recordPacking=").append(recordPacking);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(request.toString());
+    }
+    
+    private Element getRecordData(Node record)
+    throws TransformerException, SAXException, IOException, ParserConfigurationException {
+        Node result = XPathAPI.selectSingleNode(record,
+                "srw:recordData/*[1]",
+                xmlnsEl);
+        return toDocument((Element)result).getDocumentElement();
+    }
+    
+    private Document toDocument(Element el)
+    throws TransformerException, SAXException, IOException, ParserConfigurationException {
+        DOMSource source = new DOMSource(el);
+        StringWriter sw = new StringWriter();
+        StreamResult result = new StreamResult(sw);
+        synchronized (transformer) {
+            transformer.transform(source, result);
+        }
+        DocumentBuilder builder = getBuilder();
+        Document doc = builder.parse(new InputSource(new StringReader(sw.toString())));
+        Element docEl = doc.getDocumentElement();
+        if (docEl.getNamespaceURI() == null) {
+            docEl.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "");
+        }
+        return doc;
+    }
+    
+    private NodeList getRecords(Document srResponse)
+    throws TransformerException {
+        return XPathAPI.selectNodeList(srResponse, "/srw:searchRetrieveResponse/srw:records/srw:record", xmlnsEl);
+    }
+
+    public void close() {
+    }
+}
+
diff --git a/src/ORG/oclc/oai/server/catalog/XMLFileOAICatalog.java b/src/ORG/oclc/oai/server/catalog/XMLFileOAICatalog.java
new file mode 100644
index 0000000..2a92376
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/XMLFileOAICatalog.java
@@ -0,0 +1,729 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.SortedMap;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.xml.sax.SAXException;
+
+import ORG.oclc.oai.server.catalog.helpers.RecordStringHandler;
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+
+/**
+ * XMLFileOAICatalog is an implementation of AbstractCatalog interface
+ * with the data sitting in a directory on a filesystem.
+ *
+ * @author Jeff Young, OCLC Online Computer Library Center
+ */
+
+public class XMLFileOAICatalog extends AbstractCatalog {
+    private static boolean debug=false;
+
+    private SortedMap nativeMap = null;
+    private HashMap          resumptionResults=new HashMap();
+    private int              maxListSize;
+    private ArrayList sets = null;
+    private Transformer getMetadataTransformer = null;
+    private boolean schemaLocationIndexed = false;
+
+    public XMLFileOAICatalog(Properties properties) throws IOException {
+        try {
+            String          temp;
+
+            temp=properties.getProperty("XMLFileOAICatalog.debug");
+            if ("true".equals(temp)) debug = true;
+            temp=properties.getProperty("XMLFileOAICatalog.schemaLocationIndexed");
+            if ("true".equals(temp)) schemaLocationIndexed = true;
+            temp=properties.getProperty("XMLFileOAICatalog.maxListSize");
+            if (temp==null)
+                throw new IllegalArgumentException("XMLFileOAICatalog."+
+                                                   "maxListSize is missing from the properties file");
+            maxListSize = Integer.parseInt(temp);
+            if(debug)
+                System.out.println("in XMLFileOAICatalog(): maxListSize="+
+                                   maxListSize);
+            
+            String sourceFile=properties.getProperty("XMLFileOAICatalog.sourceFile");
+            if (sourceFile==null)
+                throw new IllegalArgumentException("XMLFileOAICatalog."+
+                                                   "sourceFile is missing from the properties file");
+            if(debug)
+                System.out.println("in XMLFileOAICatalog(): sourceFile="+
+                                   sourceFile);
+            String getMetadataXSLTName = properties.getProperty("XMLFileOAICatalog.getMetadataXSLTName");
+            if (getMetadataXSLTName != null) {
+                try {
+                    InputStream is;
+                    try {
+                        is = new FileInputStream(getMetadataXSLTName);
+                    } catch (FileNotFoundException e) {
+                        is = Thread.currentThread().getContextClassLoader().getResourceAsStream(getMetadataXSLTName);
+                    }
+                    StreamSource xslSource = new StreamSource(is);
+                    TransformerFactory tFactory = TransformerFactory.newInstance();
+                    getMetadataTransformer = tFactory.newTransformer(xslSource);
+                } catch (TransformerConfigurationException e) {
+                    e.printStackTrace();
+                    throw new IOException(e.getMessage());
+                }
+            }
+            
+            RecordStringHandler rsh = new RecordStringHandler();
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            factory.setNamespaceAware(true);
+            factory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
+            SAXParser saxParser = factory.newSAXParser();
+            InputStream in;
+            try {
+                in = new FileInputStream(sourceFile);
+            } catch (FileNotFoundException e) {
+                in = Thread.currentThread().getContextClassLoader().getResourceAsStream(sourceFile);
+            }
+            saxParser.parse(in, rsh);
+//            saxParser.parse(new File(sourceFile), rsh);
+            
+            // build the indexes
+            nativeMap = rsh.getNativeRecords();
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new IOException(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new IOException(e.getMessage());
+        }
+	sets = getSets(properties);
+    }
+
+    private static ArrayList getSets(Properties properties) {
+        TreeMap treeMap = new TreeMap();
+	String propertyPrefix = "Sets.";
+	Enumeration propNames = properties.propertyNames();
+	while (propNames.hasMoreElements()) {
+	    String propertyName = (String)propNames.nextElement();
+	    if (propertyName.startsWith(propertyPrefix)) {
+                treeMap.put(propertyName, properties.get(propertyName));
+	    }
+	}
+ 	return new ArrayList(treeMap.values());
+    }
+    
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param     oaiIdentifier the OAI identifier
+     * @param     metadataPrefix the OAI metadataPrefix
+     * @return    the Record object containing the result.
+     * @exception CannotDisseminateFormatException signals an http status
+     *                code 400 problem
+     * @exception IdDoesNotExistException signals an http status code 404
+     *                problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *                problem
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+        throws IdDoesNotExistException, CannotDisseminateFormatException,
+               OAIInternalServerError {
+        String localIdentifier
+            = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+        String recordid = localIdentifier;
+        if (schemaLocationIndexed) {
+            recordid=recordid + "/" + metadataPrefix;
+        }
+        if (debug) System.out.println("XMLFileOAICatalog.getRecord: recordid=" + recordid);
+        Object nativeRecord = nativeMap.get(recordid.toLowerCase());
+        if (nativeRecord == null)
+            throw new IdDoesNotExistException(oaiIdentifier);
+        return constructRecord(nativeRecord, metadataPrefix);
+    }
+
+
+    /**
+     * get a DocumentFragment containing the specified record
+     */
+    public String getMetadata(String oaiIdentifier, String metadataPrefix)
+        throws IdDoesNotExistException, IdDoesNotExistException, OAIInternalServerError {
+        String localIdentifier
+            = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+        String recordid = localIdentifier;
+        if (schemaLocationIndexed) {
+            recordid=recordid + "/" + metadataPrefix;
+        }
+        if (debug) System.out.println("XMLFileOAICatalog.getRecord: recordid=" + recordid);
+        HashMap nativeRecord = (HashMap)nativeMap.get(recordid.toLowerCase());
+        if (nativeRecord == null)
+            throw new IdDoesNotExistException(oaiIdentifier);
+        if (debug) {
+            Iterator keys = nativeRecord.keySet().iterator();
+            while (keys.hasNext())
+                System.out.println(keys.next());
+        }
+        String result = (String)nativeRecord.get("recordString");
+        if (getMetadataTransformer != null) {
+            StringReader stringReader = new StringReader(result);
+            StreamSource streamSource = new StreamSource(stringReader);
+            StringWriter stringWriter = new StringWriter();
+            try {
+                synchronized (getMetadataTransformer) {
+                    getMetadataTransformer.transform(streamSource, new StreamResult(stringWriter));
+                }
+            } catch (TransformerException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+            result = stringWriter.toString();
+        }
+        return result;
+    }
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * We get passed the ID for a record and are supposed to return a list
+     * of the formats that we can deliver the record in.  Since we are assuming
+     * that all the records in the directory have the same format, the
+     * response to this is static;
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAINotFoundException signals an http status code 404 problem
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+      throws IdDoesNotExistException, NoMetadataFormatsException {
+        Vector v = new Vector();
+//        String localIdentifier
+//            = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+        Iterator iterator = nativeMap.entrySet().iterator();
+        int numRows = nativeMap.entrySet().size();
+        for (int i=0; i<numRows; ++i) {
+            Map.Entry entryNativeMap = (Map.Entry)iterator.next();
+            HashMap nativeRecord = (HashMap)entryNativeMap.getValue();
+	    if (getRecordFactory().getOAIIdentifier(nativeRecord).equals(oaiIdentifier)) {
+		Vector schemaLocations = getRecordFactory().getSchemaLocations(nativeRecord);
+		Iterator itemIterator = schemaLocations.iterator();
+		while (itemIterator.hasNext())
+		    v.add(itemIterator.next());
+	    }
+        }
+        if (v.size() > 0) {
+            return v;
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+
+
+    /**
+     * Retrieve a list of Identifiers that satisfy the criteria parameters
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+        throws NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+	Iterator iterator = nativeMap.entrySet().iterator();
+	int numRows = nativeMap.entrySet().size();
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
+            HashMap nativeRecord = (HashMap)entryNativeMap.getValue();
+            String recordDate = getRecordFactory().getDatestamp(nativeRecord);
+            String schemaLocation = (String)nativeRecord.get("schemaLocation");
+	    List setSpecs = (List)nativeRecord.get("setSpecs");
+            if (recordDate.compareTo(from) >= 0
+                && recordDate.compareTo(until) <= 0
+                && (!schemaLocationIndexed || schemaLocation.equals(getCrosswalks().getSchemaLocation(metadataPrefix)))
+		&& (set==null || setSpecs.contains(set))) {
+                String[] header = getRecordFactory().createHeader(nativeRecord);
+                headers.add(header[0]);
+                identifiers.add(header[1]);
+                count++;
+            }
+	}
+
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listIdentifiersMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listIdentifiersMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of Identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+      throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("XMLFileOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+	    iterator = nativeMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the headers and identifiers ArrayLists. */
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
+            String key = (String)entryNativeMap.getKey();
+            Object nativeRecord = nativeMap.get(key);
+            String[] header = getRecordFactory().createHeader(nativeRecord);
+            headers.add(header[0]);
+            identifiers.add(header[1]);
+            count++;
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listIdentifiersMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeRecord native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(Object nativeRecord, String metadataPrefix)
+        throws CannotDisseminateFormatException, OAIInternalServerError {
+        String schemaURL = null;
+	Iterator setSpecs = getSetSpecs(nativeRecord);
+	Iterator abouts = getAbouts(nativeRecord);
+
+        if (metadataPrefix != null) {
+            if (debug) {
+                System.out.println(getCrosswalks());
+            }
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeRecord, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeRecord
+     *
+     * @param rs ResultSet containing the nativeRecord
+     * @return an Iterator containing the list of setSpec values for this nativeRecord
+     */
+    private Iterator getSetSpecs(Object nativeRecord)
+	throws OAIInternalServerError {
+	try {
+	    return getRecordFactory().getSetSpecs(nativeRecord);
+	} catch (Exception e) {
+	    e.printStackTrace();
+	    throw new OAIInternalServerError(e.getMessage());
+	}
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeRecord
+     *
+     * @param rs ResultSet containing the nativeRecord
+     * @return an Iterator containing the list of about values for this nativeRecord
+     */
+    private Iterator getAbouts(Object nativeRecord) {
+        return null;
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listRecords(String from, String until, String set,
+                                    String metadataPrefix)
+      throws CannotDisseminateFormatException,
+      OAIInternalServerError, NoItemsMatchException {
+        String requestedSchemaLocation = getCrosswalks().getSchemaLocation(metadataPrefix);
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        LinkedList records = new LinkedList();
+	Iterator iterator = nativeMap.entrySet().iterator();
+	int numRows = nativeMap.entrySet().size();
+        if (debug) {
+            System.out.println("XMLFileOAICatalog.listRecords: numRows=" + numRows);
+        }
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
+            HashMap nativeRecord = (HashMap)entryNativeMap.getValue();
+            String recordDate = getRecordFactory().getDatestamp(nativeRecord);
+            String schemaLocation = (String)nativeRecord.get("schemaLocation");
+	    List setSpecs = (List)nativeRecord.get("setSpecs");
+            if (debug) {
+                System.out.println("XMLFileOAICatalog.listRecord: recordDate=" + recordDate);
+                System.out.println("XMLFileOAICatalog.listRecord: requestedSchemaLocation=" + requestedSchemaLocation);
+                System.out.println("XMLFileOAICatalog.listRecord: schemaLocation=" + schemaLocation);
+            }
+            if (recordDate.compareTo(from) >= 0
+                && recordDate.compareTo(until) <= 0
+                && (!schemaLocationIndexed || requestedSchemaLocation.equals(schemaLocation))
+		&& (set==null || setSpecs.contains(set))) {
+                String record = constructRecord(nativeRecord, metadataPrefix);
+                if (debug) {
+                    System.out.println("XMLFileOAICatalog.listRecords: record=" + record);
+                }
+                records.add(record);
+                count++;
+            }
+	}
+        
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listRecordsMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listRecordsMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listRecords(String resumptionToken)
+      throws BadResumptionTokenException,
+      OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        LinkedList records = new LinkedList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("XMLFileOAICatalog.listRecords: reuse of old resumptionToken?");
+	    iterator = nativeMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the records ArrayLists. */
+	int count = 0;
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
+	    try {
+                String key = (String)entryNativeMap.getKey();
+                Object nativeRecord = nativeMap.get(key);
+                String record = constructRecord(nativeRecord, metadataPrefix);
+                records.add(record);
+                count++;
+            } catch (CannotDisseminateFormatException e) {
+                /* the client hacked the resumptionToken beyond repair */
+                throw new BadResumptionTokenException();
+	    }
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listRecordsMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    public Map listSets() throws NoSetHierarchyException {
+        if (sets.size() == 0)
+            throw new NoSetHierarchyException();
+         Map listSetsMap = new LinkedHashMap();
+         listSetsMap.put("sets", sets.iterator());
+         return listSetsMap;
+    }
+
+
+    public Map listSets(String resumptionToken)
+      throws BadResumptionTokenException {
+	throw new BadResumptionTokenException();
+    }
+
+
+    /**
+     * close the repository
+     */
+    public void close() { }
+
+
+    /**
+     * Purge tokens that are older than the time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date      then, now = new Date();
+        Iterator  keySet = resumptionResults.keySet().iterator();
+        String    key;
+
+        while (keySet.hasNext()) {
+            key=(String)keySet.next();
+            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a long integer version of the current time
+     */
+    private synchronized static String getRSName() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/XMLFileRecordFactory.java b/src/ORG/oclc/oai/server/catalog/XMLFileRecordFactory.java
new file mode 100644
index 0000000..7b118eb
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/XMLFileRecordFactory.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * XMLFileRecordFactory converts native XML "items" to "record" Strings.
+ * This factory assumes the native XML item looks exactly like the <record>
+ * element of an OAI GetRecord response, with the possible exception that the
+ * <metadata> element contains multiple metadataFormats from which to choose.
+ */
+public class XMLFileRecordFactory extends RecordFactory {
+//     private String repositoryIdentifier = null;
+    
+    /**
+     * Construct an XMLFileRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public XMLFileRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+// 	repositoryIdentifier = properties.getProperty("XMLFileRecordFactory.repositoryIdentifier");
+// 	if (repositoryIdentifier == null) {
+// 	    throw new IllegalArgumentException("XMLFileRecordFactory.repositoryIdentifier is missing from the properties file");
+// 	}
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+        try {
+//             StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
+//             tokenizer.nextToken();
+//             tokenizer.nextToken();
+//             return tokenizer.nextToken();
+            return identifier;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+	StringBuffer sb = new StringBuffer();
+// 	sb.append("oai:");
+// 	sb.append(repositoryIdentifier);
+// 	sb.append(":");
+	sb.append(getLocalIdentifier(nativeItem));
+	return sb.toString();
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+        return (String)((HashMap)nativeItem).get("localIdentifier");
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException  {
+        return (String)((HashMap)nativeItem).get("datestamp");
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+        List setSpecs = (List)((HashMap)nativeItem).get("setSpecs");
+        if (setSpecs != null) 
+            return setSpecs.iterator();
+        else
+            return null;
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+	// Don't perform quick creates
+	return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/XMLRecordFactory.java b/src/ORG/oclc/oai/server/catalog/XMLRecordFactory.java
new file mode 100644
index 0000000..a1fc5f0
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/XMLRecordFactory.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Properties;
+
+/**
+ * XMLRecordFactory converts native XML "items" to "record" Strings.
+ * This factory assumes the native XML item looks exactly like the <record>
+ * element of an OAI GetRecord response, with the possible exception that the
+ * <metadata> element contains multiple metadataFormats from which to choose.
+ */
+public class XMLRecordFactory extends RecordFactory {
+    private static final String identifierStart = "<identifier>";
+    private static final String identifierEnd = "</identifier>";
+    private static final String datestampStart = "<datestamp>";
+    private static final String datestampEnd = "</datestamp>";
+    private static final String setSpecStart = "<setSpec>";
+    private static final String setSpecEnd = "</setSpec>";
+    private static final String aboutStart = "<about>";
+    private static final String aboutEnd = "</about>";
+    
+    /**
+     * Construct an XMLRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public XMLRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+	return identifier;
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+	String xmlRec = (String)nativeItem;
+	int startOffset = xmlRec.indexOf(identifierStart);
+	int endOffset = xmlRec.indexOf(identifierEnd);
+	return xmlRec.substring(startOffset + identifierStart.length(), endOffset);
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException  {
+	String xmlRec = (String)nativeItem;
+	int startOffset = xmlRec.indexOf(datestampStart);
+	int endOffset = xmlRec.indexOf(datestampEnd);
+	return xmlRec.substring(startOffset + datestampStart.length(), endOffset);
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+	ArrayList list = new ArrayList();
+	String xmlRec = (String)nativeItem;
+	for (int startOffset = xmlRec.indexOf(setSpecStart);
+	     startOffset >= 0;
+	     startOffset = xmlRec.indexOf(setSpecStart, startOffset + 1)) {
+	    int endOffset = xmlRec.indexOf(setSpecEnd, startOffset + 1);
+	    list.add(xmlRec.substring(startOffset + setSpecStart.length(), endOffset));
+	}
+	return list.iterator();
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	ArrayList list = new ArrayList();
+	String xmlRec = (String)nativeItem;
+	for (int startOffset = xmlRec.indexOf(aboutStart);
+	     startOffset >= 0;
+	     startOffset = xmlRec.indexOf(aboutStart, startOffset + 1)) {
+	    int endOffset = xmlRec.indexOf(aboutEnd, startOffset + 1);
+	    list.add(xmlRec.substring(startOffset + aboutStart.length(), endOffset));
+	}
+	return list.iterator();
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	String xmlRec = (String)nativeItem;
+	return xmlRec.indexOf("<header status=\"deleted\"") != -1;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+	// Don't perform quick creates
+	return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/XerSRUOAICatalog.java b/src/ORG/oclc/oai/server/catalog/XerSRUOAICatalog.java
new file mode 100644
index 0000000..e75c17b
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/XerSRUOAICatalog.java
@@ -0,0 +1,842 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.xpath.XPathAPI;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import ORG.oclc.oai.server.verb.BadArgumentException;
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+public class XerSRUOAICatalog extends AbstractCatalog {
+    private static final boolean debug = false;
+    private String sruURL;
+    private String sortKeys = "";
+    protected int maxListSize;
+    private TreeMap sets = null;
+    private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
+    private static Transformer transformer = null;
+    private static HashMap builderMap = new HashMap();
+    private static Element xmlnsEl = null;
+    private static DocumentBuilderFactory factory =
+        DocumentBuilderFactory.newInstance();
+
+    static {
+        try {
+            transformer = transformerFactory.newTransformer();
+            factory.setNamespaceAware(true);
+            DocumentBuilder builder = getBuilder();
+            DOMImplementation impl = builder.getDOMImplementation();
+            Document xmlnsDoc
+            = impl.createDocument("http://www.oclc.org/research/software/oai/harvester",
+                    "harvester:xmlnsDoc", null);
+            xmlnsEl = xmlnsDoc.getDocumentElement();
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                    "xmlns:srw",
+            "http://www.loc.gov/zing/srw/");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public XerSRUOAICatalog(Properties properties) {
+        this(properties, properties.getProperty("SRUOAICatalog.sruURL"));
+    }
+
+    public XerSRUOAICatalog(Properties properties, String sruURL) {
+        sortKeys = properties.getProperty("SRUOAICatalog.sortKeys");
+        if (sortKeys == null) sortKeys = "";
+
+        String maxListSize =
+            properties.getProperty("SRUOAICatalog.maxListSize");
+
+        if (maxListSize == null) {
+            throw new IllegalArgumentException("SRUOAICatalog.maxListSize is missing from the properties file");
+        } else {
+            this.maxListSize = Integer.parseInt(maxListSize);
+        }
+
+        this.sruURL = sruURL;
+        sets = getSets(properties);
+    }
+
+    private static TreeMap getSets(Properties properties) {
+        TreeMap treeMap = new TreeMap();
+        return treeMap;
+    }
+
+    private static DocumentBuilder getBuilder() throws ParserConfigurationException {
+        Thread currentThread = Thread.currentThread();
+        DocumentBuilder builder = (DocumentBuilder) builderMap.get(currentThread);
+        if (builder == null) {
+            builder = factory.newDocumentBuilder();
+            builderMap.put(currentThread, builder);
+        }
+        return builder;
+    }
+
+    private String normalizeTerm(String term) {
+        return term;
+    }
+
+    public Map listSets() throws NoSetHierarchyException {
+        if (sets.size() == 0)
+            throw new NoSetHierarchyException();
+        Map listSetsMap = new LinkedHashMap();
+        try {
+            Object[] keys = sets.keySet().toArray();
+            Object[] values = sets.values().toArray();
+            ArrayList newList = new ArrayList();
+            for (int i = 0; i < keys.length; ++i) {
+                newList.add(values[i]);
+            }
+            listSetsMap.put("sets", newList.iterator());
+        } catch (Throwable e) {
+            System.err.println("SRUOAICatalog.listSets: browse failed");
+            e.printStackTrace();
+        }
+
+        return listSetsMap;
+    }
+
+    public Map listSets(String resumptionToken)
+    throws BadResumptionTokenException {
+        throw new BadResumptionTokenException();
+    }
+
+    /**
+     * get a map containing a key="headers" value=Iterator and
+     * a key="resumptionToken" value=String. The "headers" Map
+     * contains Map.Entrys where key="header" value="deleted" or null.
+     */
+    public Map listIdentifiers(String from,
+            String until,
+            String set,
+            String metadataPrefix)
+    throws
+    BadArgumentException,
+    NoItemsMatchException,
+    OAIInternalServerError {
+        if (set != null
+                && set.length() > 0
+                && from.equals(toFinestFrom("0000-00-00"))
+                && until.equals(toFinestUntil("9999-99-99"))) {
+            from = null;
+            until = null;
+        }
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        Document srResponse;
+        try {
+            srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        from,
+                        until,
+                        set,
+                        "http://www.openarchives.org/OAI/2.0/#header",
+                        1,
+                        maxListSize,
+                "xml");
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+        try {
+            NodeList nodeList = getRecords(srResponse);
+            if (nodeList != null) {
+                RecordFactory recordFactory = getRecordFactory();
+                for (int i = 0; i < nodeList.getLength(); ++i) {
+                    Object rec = getRecordData(nodeList.item(i));
+                    HashMap hashMap = new HashMap();
+                    hashMap.put("header", rec);
+                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
+                    try {
+                        hashMap.put("metadata", getNativeMetadata(localIdentifier,
+                                metadataPrefix));
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (TransformerException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (ParserConfigurationException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    } catch (SAXException e) {
+                        e.printStackTrace();
+                        throw new OAIInternalServerError(e.getMessage());
+                    }
+                    String[] header = recordFactory.createHeader(hashMap);
+                    headers.add(header[0]);
+                    identifiers.add(header[1]);
+                }
+                try {
+                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
+                    String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
+                    if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                        StringBuffer resumptionToken = new StringBuffer();
+                        resumptionToken.append(resultSetId);
+                        resumptionToken.append(":");
+                        resumptionToken.append(nextRecordPosition);
+                        resumptionToken.append(":");
+                        resumptionToken.append(metadataPrefix);
+                        listIdentifiersMap.put(
+                                "resumptionMap",
+                                getResumptionMap(resumptionToken.toString()));
+                    }
+                } catch (TransformerException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            } else {
+                throw new NoItemsMatchException();
+            }
+            listIdentifiersMap.put("headers", headers.iterator());
+            listIdentifiersMap.put("identifiers", identifiers.iterator());
+            return listIdentifiersMap;
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * get a map containing a key="headers" value=Iterator and
+     * a key="resumptionToken" value=String. The "headers" Iterator
+     * contains Map.Entrys where key="header" value="deleted" or null.
+     */
+    public Map listIdentifiers(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resultSetId;
+        String nextRecordPosition;
+        String metadataPrefix;
+        try {
+            resultSetId = tokenizer.nextToken();
+            nextRecordPosition = tokenizer.nextToken();
+            metadataPrefix = tokenizer.nextToken();
+            if (metadataPrefix.equals("null"))
+                metadataPrefix = null;
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        try {
+            Document srResponse;
+            try {
+                srResponse =
+                    getSearchRetrieveResponse(sruURL,
+                            resultSetId,
+                            nextRecordPosition,
+                            "http://www.openarchives.org/OAI/2.0/#header",
+                            maxListSize,
+                    "xml");
+            } catch (IOException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            } catch (SAXException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            } catch (ParserConfigurationException e) {
+                e.printStackTrace();
+                throw new OAIInternalServerError(e.getMessage());
+            }
+            NodeList nodeList = getRecords(srResponse);
+            if (nodeList != null) {
+                RecordFactory recordFactory = getRecordFactory();
+                for (int i = 0; i < nodeList.getLength(); ++i) {
+
+                    Object rec = getRecordData(nodeList.item(i));
+                    HashMap hashMap = new HashMap();
+                    hashMap.put("header", rec);
+                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
+                    hashMap.put("metadata", getNativeMetadata(localIdentifier,
+                            metadataPrefix));
+                    String[] header = recordFactory.createHeader(hashMap);
+                    headers.add(header[0]);
+                    identifiers.add(header[1]);
+                }
+                nextRecordPosition = XPathAPI.eval(srResponse,
+                        "/srw:searchRetrieveResponse/srw:nextRecordPosition",
+                        xmlnsEl).str();
+                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                    StringBuffer newResumptionToken = new StringBuffer();
+                    newResumptionToken.append(resultSetId);
+                    newResumptionToken.append(":");
+                    newResumptionToken.append(nextRecordPosition);
+                    newResumptionToken.append(":");
+                    newResumptionToken.append(metadataPrefix);
+                    listIdentifiersMap.put(
+                            "resumptionMap",
+                            getResumptionMap(newResumptionToken.toString()));
+                }
+            }
+            listIdentifiersMap.put("headers", headers.iterator());
+            listIdentifiersMap.put("identifiers", identifiers.iterator());
+        } catch (Throwable e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+        return listIdentifiersMap;
+    }
+
+    /**
+     * get an Iterator containing Map.Entrys where key=metadataPrefix
+     * and value=schema.
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+    throws
+    IdDoesNotExistException,
+    NoMetadataFormatsException,
+    OAIInternalServerError {
+        Vector result = new Vector();
+        NodeList nodeList = null;
+        try {
+            nodeList = getIdentifierRecords(sruURL, oaiIdentifier, (String) null);
+        } catch (Throwable e) {
+            throw new OAIInternalServerError("Database failure");
+        }
+        if (nodeList != null) {
+            for (int i = 0; i < nodeList.getLength(); ++i) {
+                try {
+                    Object rec = getRecordData(nodeList.item(i));
+                    if (rec != null) {
+                        Vector schemaLocations =
+                            getRecordFactory().getSchemaLocations(rec);
+                        for (int j = 0; j < schemaLocations.size(); ++j) {
+                            result.add(schemaLocations.get(j));
+                        }
+                    } else {
+                        throw new OAIInternalServerError("Null Record");
+                    }
+                } catch (TransformerException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                } catch (SAXException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                } catch (ParserConfigurationException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            }
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+        return result;
+    }
+
+    /**
+     * get a DocumentFragment containing the specified record
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+    throws
+    IdDoesNotExistException,
+    IdDoesNotExistException,
+    CannotDisseminateFormatException,
+    OAIInternalServerError {
+        Object nativeObject;
+        try {
+            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+
+        if (nativeObject != null) {
+            String schemaURL = null;
+
+            if (metadataPrefix != null) {
+                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                        == null) {
+                    throw new CannotDisseminateFormatException(metadataPrefix);
+                }
+            }
+            try {
+                String s =
+                    getRecordFactory().create(
+                            nativeObject,
+                            schemaURL,
+                            metadataPrefix);
+                return s;
+            } catch (CannotDisseminateFormatException e) {
+                e.printStackTrace();
+                throw e;
+            }
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+
+    /**
+     * get a DocumentFragment containing the specified record
+     */
+    public String getMetadata(String oaiIdentifier, String metadataPrefix)
+    throws
+    IdDoesNotExistException,
+    IdDoesNotExistException,
+    CannotDisseminateFormatException,
+    OAIInternalServerError {
+        Object nativeObject;
+        try {
+            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+
+        if (debug) {
+            System.out.println(nativeObject);
+        }
+        if (nativeObject != null) {
+            String schemaURL = null;
+
+            if (metadataPrefix != null) {
+                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                        == null) {
+                    if (debug) {
+                        System.out.println(
+                                "SRUOAICatalog.getRecord: metadataPrefix not found");
+                    }
+                    throw new CannotDisseminateFormatException(metadataPrefix);
+                }
+            }
+            try {
+                return getRecordFactory().createMetadata(
+                        nativeObject,
+                        schemaURL,
+                        metadataPrefix);
+            } catch (CannotDisseminateFormatException e) {
+                e.printStackTrace();
+                throw e;
+            }
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+
+    private NodeList getIdentifierRecords(String sruURL,
+            String oaiIdentifier,
+            String metadataPrefix)
+    throws TransformerException, SAXException, IOException,
+    ParserConfigurationException {
+        Document srResponse;
+        String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+        srResponse =
+            getSearchRetrieveResponse(sruURL,
+                    localIdentifier,
+                    "http://www.openarchives.org/OAI/2.0/#header",
+            "xml");
+        return getRecords(srResponse);
+    }
+
+    private Object getFullRecord(String sruURL,
+            String oaiIdentifier,
+            String metadataPrefix)
+    throws SAXException, TransformerException, IOException,
+    ParserConfigurationException {
+        NodeList nodeList = getIdentifierRecords(sruURL, oaiIdentifier, metadataPrefix);
+        if (nodeList != null) {
+            HashMap hashMap = new HashMap();
+            hashMap.put("header", getRecordData(nodeList.item(0)));
+            String nativeRecordSchema = getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
+            String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+            Document srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        localIdentifier,
+                        nativeRecordSchema,
+                "xml");
+            nodeList = getRecords(srResponse);
+            hashMap.put("metadata", getRecordData(nodeList.item(0)));
+            return hashMap;
+        }
+        return null;
+    }
+
+    private Object getNativeMetadata(String localIdentifier, String metadataPrefix)
+    throws TransformerException, SAXException, IOException,
+    ParserConfigurationException {
+        String nativeRecordSchema = "info:srw/schema/1/xer";
+//            getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
+        Document srResponse;
+        srResponse =
+            getSearchRetrieveResponse(sruURL,
+                    localIdentifier,
+                    nativeRecordSchema,
+            "xml");
+        NodeList nodeList = getRecords(srResponse);
+        return getRecordData(nodeList.item(0));
+    }
+
+    public Map listRecords(String from,
+            String until,
+            String set,
+            String metadataPrefix)
+    throws
+    BadArgumentException,
+    CannotDisseminateFormatException,
+    NoItemsMatchException,
+    OAIInternalServerError {
+        if (set != null
+                && set.length() > 0
+                && from.equals(toFinestFrom("0000-00-00"))
+                && until.equals(toFinestUntil("9999-99-99"))) {
+            from = null;
+            until = null;
+        }
+        if (toFinestFrom("0000-00-00").equals(from)) {
+            from = null;
+        }
+        if (toFinestUntil("9999-12-31").compareTo(until) <= 0) {
+            until = null;
+        }
+        Map listRecordsMap = new HashMap();
+        ArrayList recordsList = new ArrayList();
+        Document srResponse;
+        try {
+            srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        from,
+                        until,
+                        set,
+                        "info:srw/schema/1/xer",
+                        1,
+                        maxListSize,
+                "xml");
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+        try {
+            NodeList nodeList = getRecords(srResponse);
+            if (nodeList != null) {
+                RecordFactory recordFactory = getRecordFactory();
+                String schemaURL = null;
+                if (metadataPrefix != null) {
+                    if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                            == null)
+                        throw new CannotDisseminateFormatException(metadataPrefix);
+                }
+                for (int i = 0; i < nodeList.getLength(); ++i) {
+                    Element recEl = getRecordData(nodeList.item(i));
+                    recordsList.add(
+                            recordFactory.create(recEl,
+                                    schemaURL,
+                                    metadataPrefix));
+                }
+                String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
+                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
+                    StringBuffer resumptionToken = new StringBuffer();
+                    resumptionToken.append(resultSetId);
+                    resumptionToken.append(":");
+                    resumptionToken.append(nextRecordPosition);
+                    resumptionToken.append(":");
+                    resumptionToken.append(metadataPrefix);
+                    listRecordsMap.put(
+                            "resumptionMap",
+                            getResumptionMap(resumptionToken.toString()));
+                }
+            } else {
+                throw new NoItemsMatchException();
+            }
+            listRecordsMap.put("records", recordsList.iterator());
+            return listRecordsMap;
+        } catch (TransformerException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (SAXException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    public Map listRecords(String resumptionToken)
+    throws BadResumptionTokenException, OAIInternalServerError {
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resultSetId;
+        String nextRecordPosition;
+        String metadataPrefix;
+
+        try {
+            resultSetId = tokenizer.nextToken();
+            nextRecordPosition = tokenizer.nextToken();
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            e.printStackTrace();
+            throw new BadResumptionTokenException();
+        }
+        Map listRecordsMap = new HashMap();
+        ArrayList recordsList = new ArrayList();
+        try {
+            Document srResponse =
+                getSearchRetrieveResponse(sruURL,
+                        resultSetId,
+                        nextRecordPosition,
+                        "http://www.openarchives.org/OAI/2.0/#header",
+                        maxListSize,
+                "xml");
+            NodeList nodeList = getRecords(srResponse);
+            RecordFactory recordFactory = getRecordFactory();
+            String schemaURL = null;
+            if (metadataPrefix != null) {
+                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
+                        == null)
+                    throw new CannotDisseminateFormatException(metadataPrefix);
+            }
+            for (int i = 0; i < nodeList.getLength(); ++i) {
+                Object rec = getRecordData(nodeList.item(i));
+                HashMap hashMap = new HashMap();
+                hashMap.put("header", rec);
+                String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
+                hashMap.put("metadata", getNativeMetadata(localIdentifier,
+                        metadataPrefix));
+                recordsList.add(
+                        recordFactory.create(hashMap,
+                                schemaURL,
+                                metadataPrefix));
+            }
+            nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
+            if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
+                resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
+                StringBuffer newResumptionToken = new StringBuffer();
+                newResumptionToken.append(resultSetId);
+                newResumptionToken.append(":");
+                newResumptionToken.append(nextRecordPosition);
+                newResumptionToken.append(":");
+                newResumptionToken.append(metadataPrefix);
+                listRecordsMap.put(
+                        "resumptionMap",
+                        getResumptionMap(newResumptionToken.toString()));
+            }
+            listRecordsMap.put("records", recordsList.iterator());
+        } catch (Throwable e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+        return listRecordsMap;
+    }
+
+    private Document
+    getSearchRetrieveResponse(String sruURL,
+            String from,
+            String until,
+            String set,
+            String recordSchema,
+            int startRecord,
+            int maximumRecords,
+            String recordPacking)
+    throws SAXException, ParserConfigurationException,
+    UnsupportedEncodingException, IOException {
+        StringBuffer query = new StringBuffer();
+        if ((from != null && from.length() > 0)
+                || (until != null && until.length() > 0)) {
+            query.append("(");
+            if (from != null && from.length() > 0) {
+                query.append("oai.datestamp>=\"");
+                query.append(normalizeTerm(from));
+                query.append("\"");
+            }
+            if (until != null && from.length() > 0) {
+                if (query.length() > 0)
+                    query.append(" and ");
+                query.append("oai.datestamp<=\"");
+                query.append(normalizeTerm(until));
+                query.append("\"");
+            }
+            query.append(")");
+        }
+        if (set != null && set.length() > 0) {
+            if (query.length() > 0)
+                query.append(" and ");
+            query.append("oai.set=\"");
+            query.append(normalizeTerm(set));
+            query.append("\"");
+        }
+        StringBuffer request = new StringBuffer(sruURL);
+        request.append("?operation=searchRetrieve&version=1.1&resultSetTTL=600&query=");
+        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
+        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
+        request.append("&startRecord=").append(Integer.toString(startRecord));
+        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
+        request.append("&recordPacking=").append(recordPacking);
+        request.append("&sortKeys=").append(URLEncoder.encode(sortKeys, "UTF-8"));
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(request.toString());
+    }
+
+    private Document
+    getSearchRetrieveResponse(String sruURL,
+            String resultSetId,
+            String nextRecordPosition,
+            String recordSchema,
+            int maximumRecords,
+            String recordPacking)
+    throws SAXException, ParserConfigurationException,
+    UnsupportedEncodingException, IOException {
+        StringBuffer query = new StringBuffer();
+        query.append("cql.resultSetId=");
+        query.append(resultSetId);
+        StringBuffer request = new StringBuffer(sruURL);
+        request.append("?operation=searchRetrieve&resultSetTTL=600&version=1.1&query=");
+        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
+        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
+        request.append("&startRecord=").append(nextRecordPosition);
+        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
+        request.append("&recordPacking=").append(recordPacking);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(request.toString());
+    }
+
+    private Document
+    getSearchRetrieveResponse(String sruURL,
+            String localIdentifier,
+            String recordSchema,
+            String recordPacking)
+    throws SAXException, IOException, ParserConfigurationException,
+    UnsupportedEncodingException {
+        StringBuffer query = new StringBuffer();
+        query.append("oai.identifier exact \"");
+        query.append(localIdentifier);
+        query.append("\"");
+
+        StringBuffer request = new StringBuffer(sruURL);
+        request.append("?operation=searchRetrieve&resultSetTTL=0&version=1.1&query=");
+        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
+        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
+        request.append("&startRecord=1&maximumRecords=1");
+        request.append("&recordPacking=").append(recordPacking);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(request.toString());
+    }
+
+    private Element getRecordData(Node record)
+    throws TransformerException, SAXException, IOException, ParserConfigurationException {
+        Node result = XPathAPI.selectSingleNode(record,
+                "srw:recordData/*[1]",
+                xmlnsEl);
+        return toDocument((Element)result).getDocumentElement();
+    }
+
+    private Document toDocument(Element el)
+    throws TransformerException, SAXException, IOException, ParserConfigurationException {
+        DOMSource source = new DOMSource(el);
+        StringWriter sw = new StringWriter();
+        StreamResult result = new StreamResult(sw);
+        synchronized (transformer) {
+            transformer.transform(source, result);
+        }
+        DocumentBuilder builder = getBuilder();
+        Document doc = builder.parse(new InputSource(new StringReader(sw.toString())));
+        Element docEl = doc.getDocumentElement();
+        if (docEl.getNamespaceURI() == null) {
+            docEl.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "");
+        }
+        return doc;
+    }
+
+    private NodeList getRecords(Document srResponse)
+    throws TransformerException {
+        return XPathAPI.selectNodeList(srResponse, "/srw:searchRetrieveResponse/srw:records/srw:record", xmlnsEl);
+    }
+
+    public void close() {
+    }
+}
+
diff --git a/src/ORG/oclc/oai/server/catalog/XerSRURecordFactory.java b/src/ORG/oclc/oai/server/catalog/XerSRURecordFactory.java
new file mode 100644
index 0000000..c945bbc
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/XerSRURecordFactory.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.TransformerException;
+
+import org.apache.xpath.XPathAPI;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/**
+ * NodeRecordFactory converts native XML "items" to "record" Strings.
+ */
+public class XerSRURecordFactory extends RecordFactory {
+
+    private static Element xmlnsEl = null;
+    private static DocumentBuilderFactory factory = null;
+    static {
+        try {
+            factory =
+                DocumentBuilderFactory.newInstance();
+            factory.setNamespaceAware(true);
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            DOMImplementation impl = builder.getDOMImplementation();
+            Document xmlnsDoc =
+                impl.createDocument(
+                        "http://www.oclc.org/research/software/oai/harvester",
+                        "harvester:xmlnsDoc",
+                        null);
+            xmlnsEl = xmlnsDoc.getDocumentElement();
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                    "xmlns:xsd",
+            "http://www.w3.org/2001/XMLSchema");
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                    "xmlns:srw",
+            "http://www.loc.gov/zing/srw/");
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                    "xmlns:oai",
+            "http://www.openarchives.org/OAI/2.0/");
+            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                    "xmlns:explain",
+            "http://explain.z3950.org/dtd/2.0/");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public XerSRURecordFactory(Properties properties)
+    throws IllegalArgumentException {
+        super(properties);
+//        this(properties, getCrosswalkMap(properties.getProperty("SRUOAICatalog.sruURL")));
+    }
+
+    /**
+     * Construct an NodeRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public XerSRURecordFactory(Properties properties, HashMap crosswalkMap)
+    throws IllegalArgumentException {
+        super(crosswalkMap);
+    }
+
+//    private static HashMap getCrosswalkMap(String sruURL)
+//    throws IllegalArgumentException {
+//        try {
+//            DocumentBuilder parser = factory.newDocumentBuilder();
+//            Document explainDoc = parser.parse(sruURL);
+//            NodeList schemas = XPathAPI.selectNodeList(explainDoc, "/srw:explainResponse/srw:record/srw:recordData/explain:explain/explain:schemaInfo/explain:schema", xmlnsEl);
+//
+//            // Load the formats the repository supports directly
+//            HashMap crosswalkMap = new HashMap();
+//            for (int i=0; i<schemas.getLength(); ++i) {
+//                Object[] crosswalkItem = crosswalkItemFactory(schemas.item(i));
+//                for (int j=0; j<crosswalkItem.length; ++j) {
+//                    CrosswalkItem currentItem = (CrosswalkItem)crosswalkItem[j];
+//                    // 		logger.debug(currentItem.toString());
+//                    Object key = currentItem.getMetadataPrefix();
+//                    CrosswalkItem storedItem = (CrosswalkItem)crosswalkMap.get(key);
+//                    if (storedItem == null || (currentItem.getRank() < storedItem.getRank())) {
+//                        crosswalkMap.put(key, currentItem);
+//                    }
+//                }
+//            }
+//
+//            HashMap moreCrosswalkMap = new HashMap();
+//
+//            // Now, combine the lists with the originals taking precidence
+//            moreCrosswalkMap.putAll(crosswalkMap);
+//            return moreCrosswalkMap;
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//            throw new IllegalArgumentException(sruURL);
+//        }
+//    }
+
+//    public static Object[] crosswalkItemFactory(Node explainSchemaNode)
+//    throws TransformerException, OAIInternalServerError {
+//        ArrayList crosswalkItemList = new ArrayList();
+//        String nativeRecordSchema = XPathAPI.eval(explainSchemaNode, "@identifier", xmlnsEl).str();
+//        String metadataPrefix = XPathAPI.eval(explainSchemaNode, "@name", xmlnsEl).str();
+//        String schema = XPathAPI.eval(explainSchemaNode, "@location", xmlnsEl).str();
+//        String metadataNamespace = null;
+//        try {
+//            DocumentBuilder parser = factory.newDocumentBuilder();
+//            Document schemaDoc = parser.parse(schema);
+//            metadataNamespace = XPathAPI.eval(schemaDoc, "/xsd:schema/@targetNamespace", xmlnsEl).str();
+//        } catch (SAXParseException e) {
+//            metadataNamespace = "";
+//        } catch (Exception e) {
+//            System.out.println("Failed to get schema: " + schema);
+//            e.printStackTrace();
+//            metadataNamespace = "";
+//        }
+//        CrosswalkItem crosswalkItem = new CrosswalkItem(nativeRecordSchema, metadataPrefix, schema, metadataNamespace, NodePassThruCrosswalk.class);
+//        crosswalkItemList.add(crosswalkItem);
+//        return crosswalkItemList.toArray();
+//    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+        return identifier;
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+        return getLocalIdentifier(nativeItem);
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+        try {
+            Element recordEl = (Element)nativeItem;
+//            Node identifierNode =
+//                XPathAPI.selectSingleNode(
+//                        recordEl,
+//                        "/oai:header/oai:identifier",
+//                        xmlnsEl);
+//            if (identifierNode != null) {
+//                return XPathAPI.eval(identifierNode, "string()").str();
+//            }
+            return "foo";
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+    throws IllegalArgumentException {
+        Element recordData = (Element) nativeItem;
+        try {
+            String datetime = XPathAPI.eval(recordData,
+                    "/ber/tag0/tag0[@class='private'/tag0]/",
+                    xmlnsEl)
+                    .str();
+            return datetime;
+        } catch (TransformerException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+    throws IllegalArgumentException {
+        //         List setSpecs = (List)((HashMap)nativeItem).get("setSpecs");
+        //         if (setSpecs != null) 
+        //             return setSpecs.iterator();
+        //         else
+        return null;
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem)
+    throws IllegalArgumentException {
+        return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+    throws IllegalArgumentException {
+        return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(
+            Object nativeItem,
+            String schemaLocation,
+            String metadataPrefix) {
+        return null;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/catalog/helpers/RecordStringHandler.java b/src/ORG/oclc/oai/server/catalog/helpers/RecordStringHandler.java
new file mode 100644
index 0000000..5297762
--- /dev/null
+++ b/src/ORG/oclc/oai/server/catalog/helpers/RecordStringHandler.java
@@ -0,0 +1,227 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.catalog.helpers;
+
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.helpers.DefaultHandler;
+
+import ORG.oclc.oai.util.OAIUtil;
+
+public class RecordStringHandler extends DefaultHandler {
+    private static final boolean debug = false;
+    private static final String OAI_NS = "http://www.openarchives.org/OAI/2.0/";
+    private static final String DATABASE_NS = "http://www.oclc.org/pears/";
+//     private static final String OAI_DC_NS = "http://www.openarchives.org/OAI/2.0/oai_dc/";
+//     private static final String MARC21_NS = "http://www.loc.gov/MARC21/slim";
+//     private static final String REG_NS = "http://info-uri.info/registry";
+//     private static final String MTX_NS = "http://www.w3.org/1999/xhtml";
+//     private static final String PRO_NS = "info:ofi/pro";
+//     private static final String XSD_NS = "http://www.w3.org/2001/XMLSchema";
+//     private static final String XSL_NS = "http://www.w3.org/1999/XSL/Transform";
+    private static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance";
+    private SortedMap nativeRecords = new TreeMap();
+    private int recordFlag = 0;
+    private int metadataFlag = 0;
+    private StringWriter metadata = null;
+    private int recordidFlag = 0;
+    private StringBuffer recordid = null;
+    private String schemaLocation = null;
+    private int identifierFlag = 0;
+    private StringBuffer identifier = null;
+    private int datestampFlag = 0;
+    private StringBuffer datestamp = null;
+    private ArrayList setSpecs = null;
+    private int setSpecFlag = 0;
+    private StringBuffer setSpec = null;
+//     private static Logger logger = Logger.getLogger(RecordStringHandler.class);
+//     static {
+//         BasicConfigurator.configure();
+//     }
+
+    public SortedMap getNativeRecords() { return nativeRecords; }
+    
+    public void startElement(String uri, String localName, String qName,
+			     Attributes attrs) {
+        if (debug) {
+            System.out.println("startElement: " + uri + ", " + localName + ", "
+                               + qName + ", ");
+        }
+	if (OAI_NS.equals(uri) && "record".equals(localName)) {
+	    setSpecs = new ArrayList();
+	    recordFlag++;
+	}
+	if (metadataFlag > 0) {
+	    metadata.write("<" + getName(localName, qName));
+	    if (attrs != null) {
+		for (int i=0; i<attrs.getLength(); ++i) {
+		    String attributeName = getName(attrs.getLocalName(i),
+						   attrs.getQName(i));
+		    
+		    // modified by Colin DOig, 6 September 2006
+		    // xmlEncode ",&,< etc within attributes
+		    // previously invalid XML was being produced.
+		    metadata.write(" " + attributeName + "=\"" +
+				  OAIUtil.xmlEncode(attrs.getValue(i)) + "\"");
+		}
+	    }
+	    metadata.write(">");
+	}
+	if (schemaLocation == null
+            && metadataFlag == 1) {
+// 	    && ((OAI_DC_NS.equals(uri) && "dc".equals(localName))
+//             || (XSD_NS.equals(uri) && "schema".equals(localName))
+//             || (XSL_NS.equals(uri) && "stylesheet".equals(localName))
+//             || (MARC21_NS.equals(uri) && "record".equals(localName))
+//             || (REG_NS.equals(uri) && "info-registry-entry".equals(localName))
+//             || (MTX_NS.equals(uri) && "html".equals(localName))
+//             || (PRO_NS.equals(uri) && "profile".equals(localName)))) {
+            schemaLocation = attrs.getValue(XSI_NS, "schemaLocation");
+	}
+	if (OAI_NS.equals(uri) && "metadata".equals(localName)) {
+	    if (metadata == null) {
+		metadata = new StringWriter();
+	    }
+	    metadataFlag++;
+	}
+	if (OAI_NS.equals(uri) && "identifier".equals(localName)) {
+	    if (identifier == null) {
+		identifier = new StringBuffer();
+	    }
+	    identifierFlag++;
+	}
+	if (DATABASE_NS.equals(uri) && "recordid".equals(localName)) {
+	    if (recordid == null) {
+		recordid = new StringBuffer();
+	    }
+	    recordidFlag++;
+	}
+	if (OAI_NS.equals(uri) && "datestamp".equals(localName)) {
+	    if (datestamp == null) {
+		datestamp = new StringBuffer();
+	    }
+	    datestampFlag++;
+	}
+	if (OAI_NS.equals(uri) && "setSpec".equals(localName)) {
+	    if (setSpec == null) {
+		setSpec = new StringBuffer();
+	    }
+	    setSpecFlag++;
+	}
+    }
+
+    public void endElement(String uri, String localName, String qName) {
+	if (OAI_NS.equals(uri) && "identifier".equals(localName)) {
+	    identifierFlag--;
+	}
+	if (DATABASE_NS.equals(uri) && "recordid".equals(localName)) {
+	    recordidFlag--;
+	}
+	if (OAI_NS.equals(uri) && "datestamp".equals(localName)) {
+	    datestampFlag--;
+	}
+	if (OAI_NS.equals(uri) && "setSpec".equals(localName)) {
+	    setSpecs.add(setSpec.toString());
+	    setSpec = null;
+	    setSpecFlag--;
+	}
+	if (OAI_NS.equals(uri) && "record".equals(localName)) {
+	    recordFlag--;
+	    if (recordFlag == 0) {
+		HashMap nativeRecord = new HashMap();
+		nativeRecord.put("recordString", metadata.toString());
+//                 logger.debug(metadata.toString());
+                if (debug) {
+                    System.out.println("metadata: " + metadata.toString());
+                }
+                nativeRecord.put("localIdentifier", identifier.toString());
+                if (debug) {
+                    System.out.println("localIdentifier=" + identifier.toString());
+                }
+                nativeRecord.put("recordid", recordid.toString());
+                if (debug) {
+                    System.out.println("recordid=" + recordid.toString());
+                }
+                nativeRecord.put("schemaLocation", schemaLocation);
+                if (debug) {
+                    System.out.println("schemaLocation=" + schemaLocation);
+                }
+                nativeRecord.put("datestamp", datestamp.toString());
+                if (debug) {
+                    System.out.println("datestamp=" + datestamp.toString());
+                }
+ 		nativeRecord.put("setSpecs", setSpecs);
+                nativeRecords.put(recordid.toString().toLowerCase(), nativeRecord);
+		setSpecs = null;
+                identifier = null;
+                metadata = null;
+                recordid = null;
+                schemaLocation = null;
+                datestamp = null;
+	    }
+	}
+	if (OAI_NS.equals(uri) && "metadata".equals(localName)) {
+	    metadataFlag--;
+	}
+	if (metadataFlag > 0) {
+	    metadata.write("</" + getName(localName, qName) + ">");
+	}
+    }
+
+    public void characters(char[] ch, int start, int length) {
+        String s = new String(ch, start, length);
+	if (metadataFlag > 0) {
+	    metadata.write(OAIUtil.xmlEncode(s));
+	}
+	if (identifierFlag > 0) {
+	    identifier.append(s);
+	}
+	if (recordidFlag > 0) {
+	    recordid.append(s);
+	}
+	if (datestampFlag > 0) {
+	    datestamp.append(s);
+	}
+	if (setSpecFlag > 0) {
+	    setSpec.append(s);
+	}
+    }
+
+    private String getName(String s1, String s2) {
+        if (s2==null || "".equals(s2))
+            return s1;
+        else
+            return s2;
+    }
+
+//     public InputSource resolveEntity(String publicId, String systemId) {
+//         System.out.println("RecordStringHandler.resolveEntity:publicId=" + publicId);
+//         System.out.println("RecordStringHandler.resolveEntity:systemId=" + systemId);
+//         return null;
+//     }
+
+//     public void skippedEntity(String name) {
+//         System.out.println("RecordStringHandler.skippedEntity:name=" + name);
+//     }
+
+//     public void unparsedEntityDecl(String name, String publicId, String systemId,
+//                                    String notationName) {
+//         System.out.println("RecordStringHandler.unparsedEntityDecl: name=" + name);
+//         System.out.println("RecordStringHandler.unparsedEntityDecl: publicId=" + publicId);
+//         System.out.println("RecordStringHandler.unparsedEntityDecl: systemId=" + systemId);
+//         System.out.println("RecordStringHandler.unparsedEntityDecl: notationName=" + notationName);
+//     }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copycdwalite.java b/src/ORG/oclc/oai/server/crosswalk/Copycdwalite.java
new file mode 100644
index 0000000..0c7fc65
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copycdwalite.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to xsd. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copycdwalite extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copycdwalite(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.getty.edu/CDWA/CDWALite http://www.getty.edu/CDWA/CDWALite/CDWALite-xsd-public-v1-1.xsd", null);
+        try {
+            String xsltName = properties.getProperty("Copycdwalite.xsltName");
+            String classpathXSL = properties.getProperty("Copycdwalite.classpathXSL");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            } else if (classpathXSL != null) {
+                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classpathXSL);
+                StreamSource xslSource = new StreamSource(is);
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("cdwalite");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copydesc.java b/src/ORG/oclc/oai/server/crosswalk/Copydesc.java
new file mode 100644
index 0000000..2642f12
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copydesc.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to desc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copydesc extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copydesc(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://errol.oclc.org/oai:xmlregistry.oclc.org:sts/desc http://errol.oclc.org/oai:xmlregistry.oclc.org:sts/desc.xsd", null);
+        try {
+            String xsltName = properties.getProperty("Copydesc.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("desc");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copymets.java b/src/ORG/oclc/oai/server/crosswalk/Copymets.java
new file mode 100644
index 0000000..1762eb7
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copymets.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to mets. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copymets extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copymets(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.loc.gov/METS/ http://www.loc.gov/standards/mets/mets.xsd", null);
+        try {
+            String xsltName = properties.getProperty("Copymets.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("mets");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copyoai_dc.java b/src/ORG/oclc/oai/server/crosswalk/Copyoai_dc.java
new file mode 100644
index 0000000..08f1519
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copyoai_dc.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copyoai_dc extends XSLTCrosswalk {
+    private boolean debug=false;
+//     private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copyoai_dc(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd", (String)null);
+        try {
+            String xsltName = properties.getProperty("Copyoai_dc.xsltName");
+            String classpathXSL = properties.getProperty("Copyoai_dc.classpathXSL");
+            if (debug) System.out.println("Copyoai_dc.Copyoai_dc: xsltName="
+                                          + xsltName);
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            } else if (classpathXSL != null) {
+                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classpathXSL);
+                StreamSource xslSource = new StreamSource(is);
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("oai_dc");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copyxhtml.java b/src/ORG/oclc/oai/server/crosswalk/Copyxhtml.java
new file mode 100644
index 0000000..204ed67
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copyxhtml.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to mtx. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copyxhtml extends XSLTCrosswalk {
+//     private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copyxhtml(Properties properties)
+        throws OAIInternalServerError {
+  	super(properties, "http://www.w3.org/1999/xhtml http://www.w3.org/2002/08/xhtml/xhtml1-transitional.xsd", "text/html; charset=UTF-8", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
+        try {
+            String xsltName = properties.getProperty("Copyxhtml.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+                System.out.println("Copyxhtml.Copyxhtml: transformer="
+                                   + this.transformer);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("xhtml");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copyxoaiconfig.java b/src/ORG/oclc/oai/server/crosswalk/Copyxoaiconfig.java
new file mode 100644
index 0000000..85fec7d
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copyxoaiconfig.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to xoaiconfig. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copyxoaiconfig extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copyxoaiconfig(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://errol.oclc.org/oai:xmlregistry.oclc.org:xoai/config http://errol.oclc.org/oai:xmlregistry.oclc.org:xoai/config.xsd", null);
+        try {
+            String xsltName = properties.getProperty("Copyxoaiconfig.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("xoaiconfig");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copyxsd.java b/src/ORG/oclc/oai/server/crosswalk/Copyxsd.java
new file mode 100644
index 0000000..e06d71f
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copyxsd.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to xsd. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copyxsd extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copyxsd(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd", null);
+        try {
+            String xsltName = properties.getProperty("Copyxsd.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("xsd");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Copyxsl.java b/src/ORG/oclc/oai/server/crosswalk/Copyxsl.java
new file mode 100644
index 0000000..29167c3
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Copyxsl.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to xsl. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class Copyxsl extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public Copyxsl(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.w3.org/1999/XSL/Transform http://www.w3.org/1999/XSL/Transform.xsd", null);
+        try {
+            String xsltName = properties.getProperty("Copyxsl.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        ArrayList list = (ArrayList)nativeItem;
+        return list.contains("xsl");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/Crosswalk.java b/src/ORG/oclc/oai/server/crosswalk/Crosswalk.java
new file mode 100644
index 0000000..4b7324b
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Crosswalk.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+
+/**
+ * Converts a native "item" to an OAI metadataFormat.
+ */
+public abstract class Crosswalk {
+    /**
+     * The schemaLocation supported by this crosswalk
+     */
+    private String schemaLocation;
+    private String contentType;
+    private String docType;
+    private String encoding;
+
+    /**
+     * Constructor
+     *
+     * @param schemaLocation the schemaLocation supported by this crosswalk
+     */
+    public Crosswalk(String schemaLocation) {
+        this(schemaLocation, (String)null);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param schemaLocation the schemaLocation supported by this crosswalk
+     */
+    public Crosswalk(String schemaLocation, String contentType) {
+        this(schemaLocation, contentType, (String)null);
+    }
+
+    public Crosswalk(String schemaLocation, String contentType, String docType) {
+        this(schemaLocation, contentType, docType, (String)null);
+    }
+    /**
+     * Constructor
+     *
+     * @param schemaLocation the schemaLocation supported by this crosswalk
+     */
+    public Crosswalk(String schemaLocation, String contentType, String docType, String encoding) {
+        this.schemaLocation = schemaLocation;
+        if (contentType == null)
+            contentType = "text/xml; charset=UTF-8";
+        this.contentType = contentType;
+        this.docType = docType;
+        this.encoding = encoding;
+    }
+
+    /**
+     * returns the schemaLocation
+     *
+     * @return the schemaLocation
+     */
+    public String getSchemaLocation() { return schemaLocation; }
+
+    public String getContentType() { return contentType; }
+
+    public String getDocType() { return docType; }
+
+    public String getEncoding() { return encoding; }
+
+    /**
+     * parse the schemaURL from the schemaLocation
+     *
+     * @return the schemaURL portion of the schemaLocation
+     */
+    public String getSchemaURL() {
+	StringTokenizer tokenizer = new StringTokenizer(schemaLocation, " ");
+	String temp = tokenizer.nextToken();
+	try {
+	    return tokenizer.nextToken();
+	} catch (NoSuchElementException e) {
+	    // no namespace for the format
+	    return temp;
+	}
+    }
+
+    /**
+     * parse the namespaceURL from the schemaLocation
+     *
+     * @return the namespaceURL portion of the schemaLocation
+     */
+    public String getNamespaceURL() {
+	StringTokenizer tokenizer = new StringTokenizer(schemaLocation, " ");
+	return tokenizer.nextToken();
+    }
+
+    /**
+     * Can this nativeItem be represented in ETDMS format?
+     * @param nativeItem a record in native format
+     * @return true if ETDMS format is possible, false otherwise.
+     */
+    public abstract boolean isAvailableFor(Object nativeItem);
+    
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the XML to be stored within the <metadata> element.
+     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
+     */
+    public abstract String createMetadata(Object nativeItem)
+	throws CannotDisseminateFormatException;
+
+    /**
+     * returns the schemaLocation for this crosswalk.
+     * @return a String containing the schemaLocation.
+     */
+    public String toString() {
+	return schemaLocation;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/CrosswalkItem.java b/src/ORG/oclc/oai/server/crosswalk/CrosswalkItem.java
new file mode 100644
index 0000000..d5e6dc5
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/CrosswalkItem.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+public class CrosswalkItem {
+    private String contentType = null;
+    private String docType = null;
+    private String encoding = null;
+    private String nativeRecordSchema = null;
+    private String metadataPrefix = null;
+    private String schema = null;
+    private String metadataNamespace = null;
+    private String xsltName = null;
+    private Crosswalk crosswalk = null;
+    private int rank = -1;
+    
+    public static final int RANK_DIRECTLY_AVAILABLE = 0;
+    public static final int RANK_DERIVED = 1;
+
+    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
+			 String schema, String metadataNamespace,
+			 Crosswalk crosswalk, int rank) {
+	this(nativeRecordSchema, metadataPrefix, schema, metadataNamespace,
+	     rank);
+	this.crosswalk = crosswalk;
+    }
+    
+    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
+			 String schema, String metadataNamespace,
+			 int rank) {
+	this.nativeRecordSchema = nativeRecordSchema;
+	this.metadataPrefix = metadataPrefix;
+	this.schema = schema;
+	this.metadataNamespace = metadataNamespace;
+	this.rank = rank;
+    }
+    
+    public CrosswalkItem(String metadataPrefix, String schema,
+			 String metadataNamespace, Crosswalk crosswalk) {
+	this(metadataPrefix, metadataPrefix, schema, metadataNamespace,
+	     RANK_DIRECTLY_AVAILABLE);
+	this.crosswalk = crosswalk;
+    }
+    
+    public CrosswalkItem(String metadataPrefix, String schema,
+			 String metadataNamespace, Class crosswalkClass)
+	throws OAIInternalServerError {
+	this(metadataPrefix, metadataPrefix, schema, metadataNamespace,
+	     RANK_DIRECTLY_AVAILABLE);
+	try {
+	    this.crosswalk = getCrosswalk(crosswalkClass);
+	} catch (Exception e) {
+	    e.printStackTrace();
+	    throw new OAIInternalServerError(e.getMessage());
+	}
+    }
+    
+    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
+			 String schema, String metadataNamespace,
+			 Class crosswalkClass, String xsltName)
+	throws OAIInternalServerError {
+	this(nativeRecordSchema, metadataPrefix, schema, metadataNamespace,
+	     RANK_DERIVED);
+	this.xsltName = xsltName;
+	try {
+	    this.crosswalk = getCrosswalk(crosswalkClass);
+	} catch (Exception e) {
+	    e.printStackTrace();
+	    throw new OAIInternalServerError(e.getMessage());
+	}
+    }
+    
+    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
+			 String schema, String metadataNamespace,
+			 Class crosswalkClass)
+	throws OAIInternalServerError {
+	this(nativeRecordSchema, metadataPrefix, schema, metadataNamespace,
+	     RANK_DIRECTLY_AVAILABLE);
+	try {
+	    this.crosswalk = getCrosswalk(crosswalkClass);
+	} catch (Exception e) {
+	    e.printStackTrace();
+	    throw new OAIInternalServerError(e.getMessage());
+	}
+    }
+
+    private Crosswalk getCrosswalk(Class crosswalkClass)
+	throws NoSuchMethodException, InstantiationException,
+	       IllegalAccessException, InvocationTargetException {
+	Constructor constructor = crosswalkClass.getConstructor(new Class[] {CrosswalkItem.class});
+	return (Crosswalk)constructor.newInstance(new Object[] {this});
+    }
+	
+    public String getNativeRecordSchema() { return nativeRecordSchema; }
+    public String getMetadataPrefix() { return metadataPrefix; }
+    public String getMetadataNamespace() { return metadataNamespace; }
+    public String getSchema() { return schema; }
+    public Crosswalk getCrosswalk() { return crosswalk; }
+    public String getContentType() { return contentType; }
+    public String getDocType() { return docType; }
+    public String getEncoding() { return encoding; }
+    public String getXSLTName() { return xsltName; }
+    
+    public int getRank() { return rank; }
+
+    public String toString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append("CrosswalkItem: " );
+	sb.append(getNativeRecordSchema());
+	sb.append(":");
+ 	sb.append(getMetadataPrefix());
+	sb.append(":");
+	sb.append(getMetadataNamespace());
+	sb.append(":");
+	sb.append(getSchema());
+	sb.append(":");
+	sb.append(getCrosswalk());
+	sb.append(":");
+	sb.append(getRank());
+	return sb.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/ORG/oclc/oai/server/crosswalk/Crosswalks.java b/src/ORG/oclc/oai/server/crosswalk/Crosswalks.java
new file mode 100644
index 0000000..c357346
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/Crosswalks.java
@@ -0,0 +1,232 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.lang.reflect.Constructor;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+/**
+ * Crosswalks manages all the Crosswalk objects this
+ * repository supports.
+ *
+ * @author Jeffrey A. Young
+ */
+public class Crosswalks {
+    private static final boolean debug = false;
+
+    // map of metadataPrefix/CrosswalkItem
+    private Map crosswalksMap = new HashMap();
+
+    /**
+     * Find out which metadataFormats this repository supports and create
+     * the corresponding Crosswalk objects for each.
+     *
+     * @param properties a properties object containing Crosswalks entries
+     */
+    public Crosswalks(Properties properties) {
+        String propertyPrefix = "Crosswalks.";
+        Enumeration propNames = properties.propertyNames();
+        while (propNames.hasMoreElements()) {
+            String propertyName = (String)propNames.nextElement();
+            if (propertyName.startsWith(propertyPrefix)) {
+                String schemaLabel = propertyName.substring(propertyPrefix.length());
+		String formatClassName = (String)properties.get(propertyName);
+		try {
+		    Class crosswalkClass = Class.forName(formatClassName);
+		    Crosswalk crosswalk = null;
+		    try {
+			Constructor crosswalkConstructor = crosswalkClass.getConstructor(new Class[] {String.class, Properties.class});
+			crosswalk = (Crosswalk)crosswalkConstructor.newInstance(new Object[] {schemaLabel, properties});
+		    } catch (NoSuchMethodException e) {
+			Constructor crosswalkConstructor = crosswalkClass.getConstructor(new Class[] {Properties.class});
+			crosswalk = (Crosswalk)crosswalkConstructor.newInstance(new Object[] {properties});
+		    }
+		    CrosswalkItem crosswalkItem = new CrosswalkItem(schemaLabel, crosswalk.getSchemaURL(), crosswalk.getNamespaceURL(), crosswalk);
+		    crosswalksMap.put(schemaLabel, crosswalkItem);
+                    if (debug) {
+                        System.out.println("Crosswalks.Crosswalks: " + schemaLabel + "=" + crosswalk);
+                    }
+		} catch (Exception e) {
+		    System.err.println("Crosswalks: couldn't construct: " + formatClassName);
+		    e.printStackTrace();
+		}
+            }
+        }
+        if (crosswalksMap.size() == 0) {
+            System.err.println("Crosswalks entries are missing from properties file");
+        }
+    }
+
+    public Crosswalks(Map crosswalkItemMap) {
+	Iterator iter = crosswalkItemMap.values().iterator();
+	while (iter.hasNext()) {
+	    CrosswalkItem crosswalkItem = (CrosswalkItem)iter.next();
+	    String schemaLabel = crosswalkItem.getMetadataPrefix();
+// 	    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
+	    crosswalksMap.put(schemaLabel, crosswalkItem);
+	}
+	
+	if (crosswalksMap.size() == 0) {
+            System.err.println("Crosswalks entries are missing from properties file");
+	}
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        Iterator iterator = iterator();
+        while (iterator.hasNext()) {
+            Map.Entry entry = (Map.Entry)iterator.next();
+            sb.append(entry.getKey());
+	    sb.append("=");
+	    CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
+	    sb.append(crosswalkItem.getCrosswalk().toString());
+	    sb.append("\n");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Get the metadataPrefix associated with the specified namespace/schema
+     *
+     * @param namespaceURI the namespaceURI portion of the format specifier
+     * @param schemaURL the schemaURL portion of the format specifier
+     * @return a String containing the metadataPrefix value associated with this pair
+     */
+    public String getMetadataPrefix(String namespaceURI, String schemaURL) {
+        StringBuffer sb = new StringBuffer();
+        if (namespaceURI != null) {
+        	sb.append(namespaceURI);
+        }
+        sb.append(" ")
+        .append(schemaURL);
+	Iterator iterator = crosswalksMap.entrySet().iterator();
+	while (iterator.hasNext()) {
+	    Map.Entry entry = (Map.Entry)iterator.next();
+	    if (((CrosswalkItem)entry.getValue()).getCrosswalk().getSchemaLocation().equals(sb.toString())) {
+		return (String)entry.getKey();
+	    }
+	}
+        return null;
+    }
+
+    /**
+     * Get the schemaURL associated with the specified metadataPrefix
+     *
+     * @param metadataPrefix the prefix desired
+     * @return a String containing the schemaURL associated with the metadataPrefix
+     */
+    public String getSchemaURL(String metadataPrefix) {
+        String schemaLocation = getSchemaLocation(metadataPrefix);
+        StringTokenizer tokenizer = new StringTokenizer(schemaLocation);
+        String temp = tokenizer.nextToken();
+	try {
+	    return tokenizer.nextToken();
+	} catch (NoSuchElementException e) {
+	    return temp;
+	}
+    }
+    
+    /**
+     * Get the namespaceURI associated with the specified metadataPrefix
+     *
+     * @param metadataPrefix the prefix desired
+     * @return a String containing the namespaceURI associated with the metadataPrefix
+     */
+    public String getNamespaceURI(String metadataPrefix) {
+        String schemaLocation = getSchemaLocation(metadataPrefix);
+        StringTokenizer tokenizer = new StringTokenizer(schemaLocation);
+        return tokenizer.nextToken();
+    }
+
+    public String getNativeRecordSchema(String metadataPrefix) {
+	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
+	if (crosswalkItem == null) {
+	    return null;
+	} else {
+	    return crosswalkItem.getNativeRecordSchema();
+	}
+    }
+    
+    /**
+     * Get the namespaceURI/schemaURL associated with the specified metadataPrefix
+     *
+     * @param metadataPrefix the prefix desired
+     * @return a String containing the namespaceURI/schemaURL associated with the metadataPrefix
+     */
+    public String getSchemaLocation(String metadataPrefix) {
+	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
+	if (crosswalkItem != null)
+	    return crosswalkItem.getCrosswalk().getSchemaLocation();
+	else
+	    return null;
+    }
+
+    /**
+     * Get the namespaceURI/schemaURL associated with the specified metadataPrefix
+     *
+     * @param metadataPrefix the prefix desired
+     * @return a String containing the namespaceURI/schemaURL associated with the metadataPrefix
+     */
+    public String getContentType(String metadataPrefix) {
+	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
+	if (crosswalkItem != null)
+	    return crosswalkItem.getCrosswalk().getContentType();
+	else
+	    return null;
+    }
+
+    /**
+     * Get the DOCTYPE associated with the specified metadataPrefix
+     *
+     * @param metadataPrefix the prefix desired
+     * @return a String containing the DOCTYPE associated with the metadataPrefix
+     */
+    public String getDocType(String metadataPrefix) {
+	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
+	if (crosswalkItem != null)
+	    return crosswalkItem.getCrosswalk().getDocType();
+	else
+	    return null;
+    }
+
+    public String getEncoding(String metadataPrefix) {
+	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
+	if (crosswalkItem != null)
+	    return crosswalkItem.getCrosswalk().getEncoding();
+	else
+	    return null;
+    }
+
+    /**
+     * Does the specified metadataPrefix appears in the list of supportedFormats?
+     *
+     * @param metadataPrefix the prefix desired
+     * @return true if prefix is supported, false otherwise.
+     */
+    public boolean containsValue(String metadataPrefix) {
+        return (crosswalksMap.get(metadataPrefix) != null);
+    }
+
+    /**
+     * Get an iterator containing Map.Entry's for the supported formats.
+     *
+     * @return an Iterator containing Map.Entry's for each supported format.
+     */
+    public Iterator iterator() {
+ 	return crosswalksMap.entrySet().iterator();
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/ExtendedJDBC2oai_dc.java b/src/ORG/oclc/oai/server/crosswalk/ExtendedJDBC2oai_dc.java
new file mode 100644
index 0000000..29d9c57
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/ExtendedJDBC2oai_dc.java
@@ -0,0 +1,216 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.util.HashMap;
+import java.util.Properties;
+
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class ExtendedJDBC2oai_dc extends Crosswalk {
+    private String dcTitleLabel = null;
+    private String dcCreatorLabel = null;
+    private String dcSubjectLabel = null;
+    private String dcDescriptionLabel = null;
+    private String dcPublisherLabel = null;
+    private String dcContributorLabel = null;
+    private String dcDateLabel = null;
+    private String dcTypeLabel = null;
+    private String dcFormatLabel = null;
+    private String dcIdentifierLabel = null;
+    private String dcSourceLabel = null;
+    private String dcLanguageLabel = null;
+    private String dcRelationLabel = null;
+    private String dcCoverageLabel = null;
+    private String dcRightsLabel = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public ExtendedJDBC2oai_dc(Properties properties) {
+	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
+	dcTitleLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcTitleLabel");
+	dcCreatorLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcCreatorLabel");
+	dcSubjectLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcSubjectLabel");
+	dcDescriptionLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcDescriptionLabel");
+	dcPublisherLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcPublisherLabel");
+	dcContributorLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcContributorLabel");
+	dcDateLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcDateLabel");
+	dcTypeLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcTypeLabel");
+	dcFormatLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcFormatLabel");
+	dcIdentifierLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcIdentifierLabel");
+	dcSourceLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcSourceLabel");
+	dcLanguageLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcLanguageLabel");
+	dcRelationLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcRelationLabel");
+	dcCoverageLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcCoverageLabel");
+	dcRightsLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcRightsLabel");
+    }
+
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+	return true; // all records must support oai_dc according to the OAI spec.
+    }
+
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the XML to be stored within the <metadata> element.
+     */
+    public String createMetadata(Object nativeItem) {
+	Object dcTitle = null;
+	Object dcCreator = null;
+	Object dcSubject = null;
+	Object dcDescription = null;
+	Object dcPublisher = null;
+	Object dcContributor = null;
+	Object dcDate = null;
+	Object dcType = null;
+	Object dcFormat = null;
+	Object dcIdentifier = null;
+	Object dcSource = null;
+	Object dcLanguage = null;
+	Object dcRelation = null;
+	Object dcCoverage = null;
+	Object dcRights = null;
+	HashMap coreTable = (HashMap)((HashMap)nativeItem).get("coreResult");
+	StringBuffer sb = new StringBuffer();
+	sb.append("<oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\""
+		  + getSchemaLocation()
+		  + "\">");
+
+
+	if (dcTitleLabel != null
+	    && (dcTitle = coreTable.get(dcTitleLabel)) != null) {
+	    sb.append("<dc:title>");
+	    sb.append(OAIUtil.xmlEncode(dcTitle.toString()));
+	    sb.append("</dc:title>");
+	}
+	
+	if (dcCreatorLabel != null
+	    && (dcCreator = coreTable.get(dcCreatorLabel)) != null) {
+	    sb.append("<dc:creator>");
+	    sb.append(OAIUtil.xmlEncode(dcCreator.toString()));
+	    sb.append("</dc:creator>");
+	}
+	
+	if (dcSubjectLabel != null
+	    && (dcSubject = coreTable.get(dcSubjectLabel)) != null) {
+	    sb.append("<dc:subject>");
+	    sb.append(OAIUtil.xmlEncode(dcSubject.toString()));
+	    sb.append("</dc:subject>");
+	}
+	
+	if (dcDescriptionLabel != null
+	    && (dcDescription = coreTable.get(dcDescriptionLabel)) != null) {
+	    sb.append("<dc:description>");
+	    sb.append(OAIUtil.xmlEncode(dcDescription.toString()));
+	    sb.append("</dc:description>");
+	}
+	
+	if (dcPublisherLabel != null
+	    && (dcPublisher = coreTable.get(dcPublisherLabel)) != null) {
+	    sb.append("<dc:publisher>");
+	    sb.append(OAIUtil.xmlEncode(dcPublisher.toString()));
+	    sb.append("</dc:publisher>");
+	}
+	
+	if (dcContributorLabel != null
+	    && (dcContributor = coreTable.get(dcContributorLabel)) != null) {
+	    sb.append("<dc:contributor>");
+	    sb.append(OAIUtil.xmlEncode(dcContributor.toString()));
+	    sb.append("</dc:contributor>");
+	}
+	
+	if (dcDateLabel != null
+	    && (dcDate = coreTable.get(dcDateLabel)) != null) {
+	    sb.append("<dc:date>");
+	    sb.append(OAIUtil.xmlEncode(dcDate.toString()));
+	    sb.append("</dc:date>");
+	}
+	
+	if (dcTypeLabel != null
+	    && (dcType = coreTable.get(dcTypeLabel)) != null) {
+	    sb.append("<dc:type>");
+	    sb.append(OAIUtil.xmlEncode(dcType.toString()));
+	    sb.append("</dc:type>");
+	}
+	
+	if (dcFormatLabel != null
+	    && (dcFormat = coreTable.get(dcFormatLabel)) != null) {
+	    sb.append("<dc:format>");
+	    sb.append(OAIUtil.xmlEncode(dcFormat.toString()));
+	    sb.append("</dc:format>");
+	}
+	
+	if (dcIdentifierLabel != null
+	    && (dcIdentifier = coreTable.get(dcIdentifierLabel)) != null) {
+	    sb.append("<dc:identifier>");
+	    sb.append(OAIUtil.xmlEncode(dcIdentifier.toString()));
+	    sb.append("</dc:identifier>");
+	}
+	
+	if (dcSourceLabel != null
+	    && (dcSource = coreTable.get(dcSourceLabel)) != null) {
+	    sb.append("<dc:source>");
+	    sb.append(OAIUtil.xmlEncode(dcSource.toString()));
+	    sb.append("</dc:source>");
+	}
+	
+	if (dcLanguageLabel != null
+	    && (dcLanguage = coreTable.get(dcLanguageLabel)) != null) {
+	    sb.append("<dc:language>");
+	    sb.append(OAIUtil.xmlEncode(dcLanguage.toString()));
+	    sb.append("</dc:language>");
+	}
+	
+	if (dcRelationLabel != null
+	    && (dcRelation = coreTable.get(dcRelationLabel)) != null) {
+	    sb.append("<dc:relation>");
+	    sb.append(OAIUtil.xmlEncode(dcRelation.toString()));
+	    sb.append("</dc:relation>");
+	}
+	
+	if (dcCoverageLabel != null
+	    && (dcCoverage = coreTable.get(dcCoverageLabel)) != null) {
+	    sb.append("<dc:coverage>");
+	    sb.append(OAIUtil.xmlEncode(dcCoverage.toString()));
+	    sb.append("</dc:coverage>");
+	}
+	
+	if (dcRightsLabel != null
+	    && (dcRights = coreTable.get(dcRightsLabel)) != null) {
+	    sb.append("<dc:rights>");
+	    sb.append(OAIUtil.xmlEncode(dcRights.toString()));
+	    sb.append("</dc:rights>");
+	}
+	
+	sb.append("</oai_dc:dc>");
+	return sb.toString();
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/FileMap2oai_dc.java b/src/ORG/oclc/oai/server/crosswalk/FileMap2oai_dc.java
new file mode 100644
index 0000000..b86bd3a
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/FileMap2oai_dc.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Properties;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class FileMap2oai_dc extends Crosswalk {
+    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public FileMap2oai_dc(Properties properties)
+        throws OAIInternalServerError {
+	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
+        try {
+            String xsltName = properties.getProperty("FileMap2oai_dc.xsltName");
+	    TransformerFactory tFactory = TransformerFactory.newInstance();
+	    if (xsltName != null) {
+		StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+		this.transformer = tFactory.newTransformer(xslSource);
+	    } else {
+		this.transformer = tFactory.newTransformer();
+                this.transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+	    }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        return true;
+    }
+
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the FileMap to be stored within the <metadata> element.
+     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
+     */
+    public String createMetadata(Object nativeItem)
+	throws CannotDisseminateFormatException {
+	HashMap recordMap = (HashMap)nativeItem;
+        try {
+            String xmlRec = (new String((byte[])recordMap.get("recordBytes"), "UTF-8")).trim();
+            if (xmlRec.startsWith("<?")) {
+                int offset = xmlRec.indexOf("?>");
+                xmlRec = xmlRec.substring(offset+2);
+            }
+            StringReader stringReader = new StringReader(xmlRec);
+            StreamSource streamSource = new StreamSource(stringReader);
+            StringWriter stringWriter = new StringWriter();
+            synchronized (transformer) {
+                transformer.transform(streamSource, new StreamResult(stringWriter));
+            }
+            return stringWriter.toString();
+        } catch (Exception e) {
+            throw new CannotDisseminateFormatException(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/FileMap2oai_etdms.java b/src/ORG/oclc/oai/server/crosswalk/FileMap2oai_etdms.java
new file mode 100644
index 0000000..d5f5b91
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/FileMap2oai_etdms.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Properties;
+
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+
+/**
+ * Convert native "item" to oai_etdms. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class FileMap2oai_etdms extends Crosswalk {
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public FileMap2oai_etdms(Properties properties) {
+	super("http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd");
+    }
+
+    /**
+     * Can this nativeItem be represented in ETDMS format?
+     * @param nativeItem a record in native format
+     * @return true if ETDMS format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        return true;
+    }
+    
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the FileMap to be stored within the <metadata> element.
+     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
+     */
+    public String createMetadata(Object nativeItem)
+	throws CannotDisseminateFormatException {
+	HashMap recordMap = (HashMap)nativeItem;
+        try {
+            String xmlRec = (new String((byte[])recordMap.get("recordBytes"), "UTF-8")).trim();
+            if (xmlRec.startsWith("<?")) {
+                int offset = xmlRec.indexOf("?>");
+                xmlRec = xmlRec.substring(offset+2);
+            }
+            return xmlRec;
+        } catch (UnsupportedEncodingException e) {
+            throw new CannotDisseminateFormatException("An error occurred");
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/JDBC2oai_dc.java b/src/ORG/oclc/oai/server/crosswalk/JDBC2oai_dc.java
new file mode 100644
index 0000000..ab75217
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/JDBC2oai_dc.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.util.HashMap;
+import java.util.Properties;
+
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class JDBC2oai_dc extends Crosswalk {
+    private String separator = null;
+    private String dcTitleLabel = null;
+    private String dcCreatorLabel = null;
+    private String dcSubjectLabel = null;
+    private String dcDescriptionLabel = null;
+    private String dcPublisherLabel = null;
+    private String dcContributorLabel = null;
+    private String dcDateLabel = null;
+    private String dcTypeLabel = null;
+    private String dcFormatLabel = null;
+    private String dcIdentifierLabel = null;
+    private String dcSourceLabel = null;
+    private String dcLanguageLabel = null;
+    private String dcRelationLabel = null;
+    private String dcCoverageLabel = null;
+    private String dcRightsLabel = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public JDBC2oai_dc(Properties properties) {
+        super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
+        dcTitleLabel = (String)properties.get("JDBC2oai_dc.dcTitleLabel");
+        dcCreatorLabel = (String)properties.get("JDBC2oai_dc.dcCreatorLabel");
+        dcSubjectLabel = (String)properties.get("JDBC2oai_dc.dcSubjectLabel");
+        dcDescriptionLabel = (String)properties.get("JDBC2oai_dc.dcDescriptionLabel");
+        dcPublisherLabel = (String)properties.get("JDBC2oai_dc.dcPublisherLabel");
+        dcContributorLabel = (String)properties.get("JDBC2oai_dc.dcContributorLabel");
+        dcDateLabel = (String)properties.get("JDBC2oai_dc.dcDateLabel");
+        dcTypeLabel = (String)properties.get("JDBC2oai_dc.dcTypeLabel");
+        dcFormatLabel = (String)properties.get("JDBC2oai_dc.dcFormatLabel");
+        dcIdentifierLabel = (String)properties.get("JDBC2oai_dc.dcIdentifierLabel");
+        dcSourceLabel = (String)properties.get("JDBC2oai_dc.dcSourceLabel");
+        dcLanguageLabel = (String)properties.get("JDBC2oai_dc.dcLanguageLabel");
+        dcRelationLabel = (String)properties.get("JDBC2oai_dc.dcRelationLabel");
+        dcCoverageLabel = (String)properties.get("JDBC2oai_dc.dcCoverageLabel");
+        dcRightsLabel = (String)properties.get("JDBC2oai_dc.dcRightsLabel");
+        separator = (String)properties.get("JDBC2oai_dc.separator");
+    }
+    
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        return true; // all records must support oai_dc according to the OAI spec.
+    }
+    
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the XML to be stored within the <metadata> element.
+     */
+    public String createMetadata(Object nativeItem) {
+        HashMap table = (HashMap)nativeItem;
+        StringBuffer sb = new StringBuffer();
+        sb.append("<oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\""
+                + getSchemaLocation()
+                + "\">\n");
+        
+        sb.append(getElements(table, dcTitleLabel, "dc:title"));
+        sb.append(getElements(table, dcCreatorLabel, "dc:creator"));
+        sb.append(getElements(table, dcSubjectLabel, "dc:subject"));
+        sb.append(getElements(table, dcDescriptionLabel, "dc:description"));
+        sb.append(getElements(table, dcPublisherLabel, "dc:publisher"));
+        sb.append(getElements(table, dcContributorLabel, "dc:contributor"));
+        sb.append(getElements(table, dcDateLabel, "dc:date"));
+        sb.append(getElements(table, dcTypeLabel, "dc:type"));
+        sb.append(getElements(table, dcFormatLabel, "dc:format"));
+        sb.append(getElements(table, dcIdentifierLabel, "dc:identifier"));
+        sb.append(getElements(table, dcSourceLabel, "dc:source"));
+        sb.append(getElements(table, dcLanguageLabel, "dc:language"));
+        sb.append(getElements(table, dcRelationLabel, "dc:relation"));
+        sb.append(getElements(table, dcCoverageLabel, "dc:coverage"));
+        sb.append(getElements(table, dcRightsLabel, "dc:rights"));
+        
+        sb.append("</oai_dc:dc>\n");
+        return sb.toString();
+    }
+    
+    private String getElements(HashMap table, String jdbcLabel, String elementLabel) {
+        StringBuffer sb = new StringBuffer();
+        Object jdbcObject;
+        if (jdbcLabel != null
+                && (jdbcObject = table.get(jdbcLabel)) != null
+                && jdbcObject.toString().length() > 0) {
+            if (separator != null && separator.length() > 0) {
+                String[] values = jdbcObject.toString().split(separator);
+                for (int i=0; i<values.length; ++i) {
+                    sb.append("<").append(elementLabel).append(">");
+                    sb.append(OAIUtil.xmlEncode(values[i]));
+                    sb.append("</").append(elementLabel).append(">\n");
+                }
+            } else {
+                sb.append("<").append(elementLabel).append(">");
+                sb.append(OAIUtil.xmlEncode(jdbcObject.toString()));
+                sb.append("</").append(elementLabel).append(">\n");
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/NodePassThruCrosswalk.java b/src/ORG/oclc/oai/server/crosswalk/NodePassThruCrosswalk.java
new file mode 100644
index 0000000..ccbbed0
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/NodePassThruCrosswalk.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.StringWriter;
+import java.util.HashMap;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.w3c.dom.Element;
+
+// import gov.loc.www.zing.srw.StringOrXmlFragment;
+// import org.apache.axis.message.MessageElement;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class NodePassThruCrosswalk extends Crosswalk {
+//     private static Logger logger = Logger.getLogger(NodePassThruCrosswalk.class);
+    private static Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     * @param crosswalkItem 
+     */
+    public NodePassThruCrosswalk(CrosswalkItem crosswalkItem) {
+	super(crosswalkItem.getMetadataNamespace() + " " + crosswalkItem.getSchema());
+// 	BasicConfigurator.configure();
+	TransformerFactory transformerFactory = TransformerFactory.newInstance();
+	try {
+	    transformer = transformerFactory.newTransformer();
+	    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+	    transformer.setOutputProperty(OutputKeys.STANDALONE, "no");
+	    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+	} catch (Exception e) {
+	    e.printStackTrace();
+// 	    logger.fatal("failed to create transformer", e);
+	}
+    }
+
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+	return true;
+    }
+
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the XML to be stored within the <metadata> element.
+     */
+    public String createMetadata(Object nativeItem) {
+	try {
+	    HashMap hashMap = (HashMap)nativeItem;
+	    Element dataNode = (Element)hashMap.get("metadata");
+	    DOMSource source = new DOMSource(dataNode);
+	    StringWriter sw = new StringWriter();
+	    StreamResult result = new StreamResult(sw);
+	    synchronized (transformer) {
+		transformer.transform(source, result);
+	    }
+	    return sw.toString();
+	} catch (Exception e) {
+// 	    logger.warn("NodePassThruCrosswalk.createMetadata failed", e);
+	    e.printStackTrace();
+	    return e.getMessage();
+	}
+// 	MessageElement[] messageElement = stringOrXmlFragment.get_any();
+// 	StringBuffer sb = new StringBuffer();
+// 	for (int i=0; i<messageElement.length; ++i) {
+// 	    sb.append(messageElement[i].toString());
+// 	}
+// 	return sb.toString();
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XML2oai_dc.java b/src/ORG/oclc/oai/server/crosswalk/XML2oai_dc.java
new file mode 100644
index 0000000..6c9d284
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XML2oai_dc.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.util.Properties;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XML2oai_dc extends Crosswalk {
+    private static final String elementName = "oai_dc:dc";
+    private static final String elementStart = "<" + elementName;
+    private static final String elementEnd = elementName + ">";
+	
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XML2oai_dc(Properties properties) {
+	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
+    }
+
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+	String fullItem = (String)nativeItem;
+	if ((fullItem.indexOf(elementStart)) >= 0)
+	    return true;
+	return false;
+    }
+
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the XML to be stored within the <metadata> element.
+     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
+     */
+    public String createMetadata(Object nativeItem)
+	throws CannotDisseminateFormatException {
+	String fullItem = (String)nativeItem;
+
+	int startOffset = fullItem.indexOf(elementStart);
+	if (startOffset == -1) {
+	    throw new CannotDisseminateFormatException(getSchemaLocation());
+	}
+	int endOffset = fullItem.indexOf(elementEnd) + elementEnd.length();
+	return fullItem.substring(startOffset, endOffset);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XML2oai_etdms.java b/src/ORG/oclc/oai/server/crosswalk/XML2oai_etdms.java
new file mode 100644
index 0000000..4d787e3
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XML2oai_etdms.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.util.Properties;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+
+/**
+ * Convert native "item" to oai_etdms. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XML2oai_etdms extends Crosswalk {
+    private static final String elementName = "oai_etdms:thesis";
+    private static final String elementStart = "<" + elementName;
+    private static final String elementEnd = elementName + ">";
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XML2oai_etdms(Properties properties) {
+	super("http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd");
+    }
+
+    /**
+     * Can this nativeItem be represented in ETDMS format?
+     * @param nativeItem a record in native format
+     * @return true if ETDMS format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+	String fullItem = (String)nativeItem;
+	if (fullItem.indexOf(elementStart) > 0) {
+	    return true;
+	}
+	return false;
+    }
+    
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the XML to be stored within the <metadata> element.
+     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
+     */
+    public String createMetadata(Object nativeItem)
+	throws CannotDisseminateFormatException {
+	String fullItem = (String)nativeItem;
+	int startOffset = fullItem.indexOf(elementStart);
+	if (startOffset == -1) {
+	    throw new CannotDisseminateFormatException(getSchemaLocation());
+	}
+	int endOffset = fullItem.indexOf(elementEnd) + elementEnd.length();
+	return fullItem.substring(startOffset, endOffset);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTCrosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTCrosswalk.java
new file mode 100644
index 0000000..33fa5fe
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTCrosswalk.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Properties;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.w3c.dom.Document;
+
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.util.OAIUtil;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTCrosswalk extends Crosswalk {
+    private boolean debug = false;
+    protected Transformer transformer = null;
+
+    public XSLTCrosswalk(Properties properties)
+        throws OAIInternalServerError {
+        this(properties, properties.getProperty("XSLTCrosswalk.schemaLocation"), (String)null);
+    }
+
+    public XSLTCrosswalk(Properties properties, String schemaLocation, String contentType)
+        throws OAIInternalServerError {
+        this(properties, schemaLocation, contentType, (String)null);
+    }
+    public XSLTCrosswalk(Properties properties, String schemaLocation, String contentType, String docType)
+        throws OAIInternalServerError {
+        this(properties, schemaLocation, contentType, docType, (String)null);
+    }
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTCrosswalk(Properties properties, String schemaLocation, String contentType, String docType, String encoding)
+        throws OAIInternalServerError {
+// 	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
+        super(schemaLocation, contentType, docType, encoding);
+        if ("true".equals(properties.getProperty("XSLTCrosswalk.debug"))) debug = true;
+        try {
+            String xsltName = properties.getProperty("XSLTCrosswalk.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+		this.transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+		this.transformer.setOutputProperty(OutputKeys.STANDALONE, "no");
+		this.transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+
+    /**
+     * Can this nativeItem be represented in DC format?
+     * @param nativeItem a record in native format
+     * @return true if DC format is possible, false otherwise.
+     */
+    public boolean isAvailableFor(Object nativeItem) {
+        return true;
+    }
+
+    /**
+     * Perform the actual crosswalk.
+     *
+     * @param nativeItem the native "item". In this case, it is
+     * already formatted as an OAI <record> element, with the
+     * possible exception that multiple metadataFormats are
+     * present in the <metadata> element.
+     * @return a String containing the FileMap to be stored within the <metadata> element.
+     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
+     */
+    public String createMetadata(Object nativeItem)
+	throws CannotDisseminateFormatException {
+        try {
+            String xmlRec = null;
+            if (nativeItem instanceof HashMap) {
+                HashMap recordMap = (HashMap)nativeItem;
+                xmlRec = (String)recordMap.get("recordString");
+                if (xmlRec == null) {
+                    xmlRec = new String((byte[])recordMap.get("recordBytes"), "UTF-8");
+                }
+                xmlRec = xmlRec.trim();
+            } else if (nativeItem instanceof String) {
+                xmlRec = (String)nativeItem;
+            } else if (nativeItem instanceof Document) {
+                xmlRec = OAIUtil.toString((Document)nativeItem);
+            } else {
+                throw new Exception("Unrecognized nativeItem");
+            }
+        
+            if (debug) {
+                System.out.println("XSLTCrosswalk.createMetadata: xmlRec=" + xmlRec);
+            }
+            if (xmlRec.startsWith("<?")) {
+                int offset = xmlRec.indexOf("?>");
+                xmlRec = xmlRec.substring(offset+2);
+            }
+            if (debug) {
+                System.out.println("XSLTCrosswalk.createMetadata: transformer=" + transformer);
+            }
+            if (transformer != null) {
+                StringReader stringReader = new StringReader(xmlRec);
+                StreamSource streamSource = new StreamSource(stringReader);
+                StringWriter stringWriter = new StringWriter();
+                synchronized (this) {
+                    transformer.transform(streamSource, new StreamResult(stringWriter));
+                }
+                if (debug) {
+                    System.out.println("XSLTCrosswalk.createMetadata: return=" + stringWriter.toString());
+                }
+                return stringWriter.toString();
+            } else {
+                return xmlRec;
+            }
+        } catch (Exception e) {
+            if (debug) {
+                e.printStackTrace();
+            }
+            throw new CannotDisseminateFormatException(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTmarc21Crosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTmarc21Crosswalk.java
new file mode 100644
index 0000000..1aca2a0
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTmarc21Crosswalk.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to marc21. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTmarc21Crosswalk extends XSLTCrosswalk {
+    private boolean debug = false;
+//     private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTmarc21Crosswalk(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd", null);
+        String temp = properties.getProperty("XSLTmarc21Crosswalk.debug");
+        if ("true".equals(temp)) debug = true;
+        try {
+            String xsltName = properties.getProperty("XSLTmarc21Crosswalk.xsltName");
+            if (debug) System.out.println("XSLTmarc21Crosswalk.xsltName=" + xsltName);
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTmtxCrosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTmtxCrosswalk.java
new file mode 100644
index 0000000..2231b33
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTmtxCrosswalk.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to mtx. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTmtxCrosswalk extends XSLTCrosswalk {
+//     private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTmtxCrosswalk(Properties properties)
+        throws OAIInternalServerError {
+  	super(properties, "http://www.w3.org/1999/xhtml http://www.w3.org/2002/08/xhtml/xhtml1-transitional.xsd", "text/html; charset=UTF-8", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
+        try {
+            String xsltName = properties.getProperty("XSLTmtxCrosswalk.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+                System.out.println("XSLTmtxCrosswalk.XSLTmtxCrosswalk: transformer="
+                                   + this.transformer);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLToai_dcCrosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLToai_dcCrosswalk.java
new file mode 100644
index 0000000..59e475b
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLToai_dcCrosswalk.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to oai_dc. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLToai_dcCrosswalk extends XSLTCrosswalk {
+    private boolean debug=false;
+//     private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLToai_dcCrosswalk(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd", (String)null);
+        try {
+            String xsltName = properties.getProperty("XSLToai_dcCrosswalk.xsltName");
+            if (debug) System.out.println("XSLToai_dcCrosswalk.XSLToai_dcCrosswalk: xsltName="
+                                          + xsltName);
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTpro2004Crosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTpro2004Crosswalk.java
new file mode 100644
index 0000000..b4d2470
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTpro2004Crosswalk.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to pro. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTpro2004Crosswalk extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTpro2004Crosswalk(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "info:ofi/pro-2004 http://www.openurl.info/registry/docs/xsd/info:ofi/fmt:xml:xsd:pro-2004", null);
+        try {
+            String xsltName = properties.getProperty("XSLTproCrosswalk.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTproCrosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTproCrosswalk.java
new file mode 100644
index 0000000..a5f707f
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTproCrosswalk.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to pro. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTproCrosswalk extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTproCrosswalk(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "info:ofi/pro http://www.openurl.info/registry/docs/xsd/info:ofi/fmt:xml:xsd:pro", null);
+        try {
+            String xsltName = properties.getProperty("XSLTproCrosswalk.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTxsdCrosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTxsdCrosswalk.java
new file mode 100644
index 0000000..49bad22
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTxsdCrosswalk.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to xsd. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTxsdCrosswalk extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTxsdCrosswalk(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd", null);
+        try {
+            String xsltName = properties.getProperty("XSLTxsdCrosswalk.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTxslCrosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTxslCrosswalk.java
new file mode 100644
index 0000000..bcb67b5
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTxslCrosswalk.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to xsl. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTxslCrosswalk extends XSLTCrosswalk {
+//    private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTxslCrosswalk(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.w3.org/1999/XSL/Transform http://www.w3.org/1999/XSL/Transform.xsd", null);
+        try {
+            String xsltName = properties.getProperty("XSLTxslCrosswalk.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/crosswalk/XSLTz39_19Crosswalk.java b/src/ORG/oclc/oai/server/crosswalk/XSLTz39_19Crosswalk.java
new file mode 100644
index 0000000..db39b69
--- /dev/null
+++ b/src/ORG/oclc/oai/server/crosswalk/XSLTz39_19Crosswalk.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to z39_19. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLTz39_19Crosswalk extends XSLTCrosswalk {
+//     private Transformer transformer = null;
+    
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLTz39_19Crosswalk(Properties properties)
+        throws OAIInternalServerError {
+ 	super(properties, "http://www.oclc.org/z39_19 http://www.oclc.org/z39_19.xsd", null);
+        try {
+            String xsltName = properties.getProperty("XSLTz39_19Crosswalk.xsltName");
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                TransformerFactory tFactory = TransformerFactory.newInstance();
+                this.transformer = tFactory.newTransformer(xslSource);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/BadArgumentException.java b/src/ORG/oclc/oai/server/verb/BadArgumentException.java
new file mode 100644
index 0000000..d6f8772
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/BadArgumentException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI badArgumentException
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class BadArgumentException extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public BadArgumentException() {
+	super("<error code=\"badArgument\">The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax.</error>");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/BadResumptionTokenException.java b/src/ORG/oclc/oai/server/verb/BadResumptionTokenException.java
new file mode 100644
index 0000000..5a2ad6c
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/BadResumptionTokenException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI badResumptionTokenException
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class BadResumptionTokenException extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public BadResumptionTokenException() {
+	super("<error code=\"badResumptionToken\">The value of the resumptionToken argument is invalid or expired</error>");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/BadVerb.java b/src/ORG/oclc/oai/server/verb/BadVerb.java
new file mode 100644
index 0000000..73b6c53
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/BadVerb.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+/**
+ * This class represents an BadVerb response on either the server or
+ * on the client
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class BadVerb extends ServerVerb {
+    /**
+     * Construct the xml response on the server side.
+     *
+     * @param context the servlet context
+     * @param request the servlet request
+     * @param response 
+     * @param serverTransformer 
+     * @return a String containing the xml response
+     * @throws TransformerException 
+     */
+    public static String construct(HashMap context,
+                                   HttpServletRequest request, HttpServletResponse response,
+                                   Transformer serverTransformer)
+        throws TransformerException {
+        Properties properties =
+	    (Properties)context.get("OAIHandler.properties");
+        StringBuffer sb = new StringBuffer();
+        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
+	if (styleSheet != null) {
+	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
+	    sb.append(styleSheet);
+	    sb.append("\"?>");
+	}
+        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
+        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
+        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
+	sb.append("<responseDate>");
+	sb.append(createResponseDate(new Date()));
+	sb.append("</responseDate>");
+// 	sb.append("<requestURL>");
+//         sb.append(getRequestURL(request));
+// 	sb.append("</requestURL>");
+	sb.append("<request>");
+	try {
+	    sb.append(request.getRequestURL().toString());
+	} catch (java.lang.NoSuchMethodError e) {
+	    sb.append(request.getRequestURL().toString());
+//	    sb.append(HttpUtils.getRequestURL(request).toString());
+	}
+	sb.append("</request>");
+	sb.append("<error code=\"badVerb\">Illegal verb</error>");
+        sb.append("</OAI-PMH>");
+	return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/CannotDisseminateFormatException.java b/src/ORG/oclc/oai/server/verb/CannotDisseminateFormatException.java
new file mode 100644
index 0000000..a5743e9
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/CannotDisseminateFormatException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI cannotDisseminateFormatException
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class CannotDisseminateFormatException extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public CannotDisseminateFormatException(String metadataPrefix) {
+	super("<error code=\"cannotDisseminateFormat\">\"" + metadataPrefix + "\" is not supported by the item or by the repository</error>");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/GetRecord.java b/src/ORG/oclc/oai/server/verb/GetRecord.java
new file mode 100644
index 0000000..e0e5b4c
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/GetRecord.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+
+/**
+ * This class represents a GetRecord response on either the server or
+ * the client.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class GetRecord extends ServerVerb {
+    private static final boolean debug = false;
+    private static ArrayList validParamNames = new ArrayList();
+    static {
+	validParamNames.add("verb");
+	validParamNames.add("identifier");
+	validParamNames.add("metadataPrefix");
+    }
+
+    /**
+     * Construct the xml response on the server-side.
+     *
+     * @param context the servlet context
+     * @param request the servlet request
+     * @return a String containing the XML response
+     * @exception OAIBadRequestException an http 400 status error occurred
+     * @exception OAINotFoundException an http 404 status error occurred
+     * @exception OAIInternalServerError an http 500 status error occurred
+     */
+    public static String construct(HashMap context,
+                                   HttpServletRequest request,
+                                   HttpServletResponse response,
+                                   Transformer serverTransformer)
+        throws OAIInternalServerError, TransformerException {
+        Properties properties = (Properties)context.get("OAIHandler.properties");
+	AbstractCatalog abstractCatalog =
+	    (AbstractCatalog)context.get("OAIHandler.catalog");
+	String baseURL = properties.getProperty("OAIHandler.baseURL");
+	if (baseURL == null) {
+	    try {
+		baseURL = request.getRequestURL().toString();
+	    } catch (java.lang.NoSuchMethodError f) {
+		baseURL = request.getRequestURL().toString();
+	    }
+	}
+        StringBuffer sb = new StringBuffer();
+        String identifier = request.getParameter("identifier");
+        String metadataPrefix = request.getParameter("metadataPrefix");
+
+        if (debug) {
+            System.out.println("GetRecord.constructGetRecord: identifier=" +
+                               identifier);
+            System.out.println("GetRecord.constructGetRecord: metadataPrefix="
+                               + metadataPrefix);
+        }
+        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
+	if (styleSheet != null) {
+	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
+	    sb.append(styleSheet);
+	    sb.append("\"?>");
+	}
+        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
+        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+	String extraXmlns = properties.getProperty("OAIHandler.extraXmlns");
+	if (extraXmlns != null)
+	    sb.append(" ").append(extraXmlns);
+        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
+        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
+        sb.append("<responseDate>");
+        sb.append(createResponseDate(new Date()));
+        sb.append("</responseDate>");
+//         sb.append("<requestURL>");
+//         sb.append(getRequestURL(request));
+//         sb.append("</requestURL>");
+        Crosswalks crosswalks = abstractCatalog.getCrosswalks();
+	try {
+	    if (metadataPrefix == null || metadataPrefix.length() == 0
+		|| identifier == null || identifier.length() == 0
+		|| hasBadArguments(request, validParamNames.iterator(), validParamNames, abstractCatalog)) {
+		throw new BadArgumentException();
+	    }
+	    else if (!crosswalks.containsValue(metadataPrefix)) {
+		throw new CannotDisseminateFormatException(metadataPrefix);
+	    } else {
+		String record = abstractCatalog.getRecord(identifier, metadataPrefix);
+		if (record != null) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL));
+		    sb.append("<GetRecord>");
+		    sb.append(record);
+		    sb.append("</GetRecord>");
+		} else {
+		    throw new IdDoesNotExistException(identifier);
+		}
+	    }
+	} catch (BadArgumentException e) {
+	    sb.append("<request verb=\"GetRecord\">");
+	    sb.append(baseURL);
+	    sb.append("</request>");
+	    sb.append(e.getMessage());
+	} catch (CannotDisseminateFormatException e) {
+	    sb.append(getRequestElement(request, validParamNames, baseURL));
+	    sb.append(e.getMessage());
+	} catch (IdDoesNotExistException e) {
+	    sb.append(getRequestElement(request, validParamNames, baseURL));
+	    sb.append(e.getMessage());
+	}
+        sb.append("</OAI-PMH>");
+        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/IdDoesNotExistException.java b/src/ORG/oclc/oai/server/verb/IdDoesNotExistException.java
new file mode 100644
index 0000000..cfc8b19
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/IdDoesNotExistException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI idDoesNotExist Exception
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class IdDoesNotExistException extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public IdDoesNotExistException(String identifier) {
+	super("<error code=\"idDoesNotExist\">\"" + identifier + "\" is unknown or illegal in this repository</error>");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/Identify.java b/src/ORG/oclc/oai/server/verb/Identify.java
new file mode 100644
index 0000000..b77b4d3
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/Identify.java
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+//import org.xml.sax.SAXException;
+
+/**
+ * This class represents an Identify response on either the server or
+ * on the client
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class Identify extends ServerVerb {
+    private static ArrayList validParamNames = new ArrayList();
+    static {
+        validParamNames.add("verb");
+    }
+
+    /**
+     * Construct the xml response on the server side.
+     *
+     * @param context the servlet context
+     * @param request the servlet request
+     * @return a String containing the xml response
+     */
+    public static String construct(HashMap context,
+            HttpServletRequest request,
+            HttpServletResponse response,
+            Transformer serverTransformer)
+    throws TransformerException {
+        String version = (String)context.get("OAIHandler.version");
+        AbstractCatalog abstractCatalog =
+            (AbstractCatalog)context.get("OAIHandler.catalog");
+        Properties properties =
+            (Properties)context.get("OAIHandler.properties");
+        String baseURL = properties.getProperty("OAIHandler.baseURL");
+        if (baseURL == null) {
+            try {
+                baseURL = request.getRequestURL().toString();
+            } catch (java.lang.NoSuchMethodError f) {
+                baseURL = request.getRequestURL().toString();
+            }
+        }
+        StringBuffer sb = new StringBuffer();
+        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+        String styleSheet = properties.getProperty("OAIHandler.styleSheet");
+        if (styleSheet != null) {
+            sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
+            sb.append(styleSheet);
+            sb.append("\"?>");
+        }
+        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
+        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
+        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
+        sb.append("<responseDate>");
+        sb.append(createResponseDate(new Date()));
+        sb.append("</responseDate>");
+//      sb.append("<requestURL>");
+//      sb.append(getRequestURL(request));
+//      sb.append("</requestURL>");
+        sb.append(getRequestElement(request, validParamNames, baseURL));
+        if (hasBadArguments(request, validParamNames.iterator(), validParamNames, abstractCatalog)) {
+            sb.append(new BadArgumentException().getMessage());
+        } else {
+            sb.append("<Identify>");
+            sb.append("<repositoryName>");
+            sb.append(properties.getProperty("Identify.repositoryName",
+            "undefined"));
+            sb.append("</repositoryName>");
+            sb.append("<baseURL>");
+            sb.append(baseURL);
+            sb.append("</baseURL>");
+            sb.append("<protocolVersion>2.0</protocolVersion>");
+            sb.append("<adminEmail>");
+            sb.append(properties.getProperty("Identify.adminEmail", "undefined"));
+            sb.append("</adminEmail>");
+            sb.append("<earliestDatestamp>");
+            sb.append(properties.getProperty("Identify.earliestDatestamp", "undefined"));
+            sb.append("</earliestDatestamp>");
+            sb.append("<deletedRecord>");
+            sb.append(properties.getProperty("Identify.deletedRecord", "undefined"));
+            sb.append("</deletedRecord>");
+            String granularity = properties.getProperty("AbstractCatalog.granularity");
+            if (granularity != null) {
+                sb.append("<granularity>");
+                sb.append(granularity);
+                sb.append("</granularity>");
+            }
+            // 	String compression = properties.getProperty("Identify.compression");
+            // 	if (compression != null) {
+            sb.append("<compression>gzip</compression>");
+//          sb.append("<compression>compress</compression>");
+            sb.append("<compression>deflate</compression>");
+            // 	}
+            String repositoryIdentifier = properties.getProperty("Identify.repositoryIdentifier");
+            String sampleIdentifier = properties.getProperty("Identify.sampleIdentifier");
+            if (repositoryIdentifier != null && sampleIdentifier != null) {
+                sb.append("<description>");
+                sb.append("<oai-identifier xmlns=\"http://www.openarchives.org/OAI/2.0/oai-identifier\"");
+                sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+                sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai-identifier http://www.openarchives.org/OAI/2.0/oai-identifier.xsd\">");
+                sb.append("<scheme>oai</scheme>");
+                sb.append("<repositoryIdentifier>");
+                sb.append(repositoryIdentifier);
+                sb.append("</repositoryIdentifier>");
+                sb.append("<delimiter>:</delimiter>");
+                sb.append("<sampleIdentifier>");
+                sb.append(sampleIdentifier);
+                sb.append("</sampleIdentifier>");
+                sb.append("</oai-identifier>");
+                sb.append("</description>");
+            }
+            String propertyPrefix = "Identify.description";
+            Enumeration propNames = properties.propertyNames();
+            while (propNames.hasMoreElements()) {
+                String propertyName = (String)propNames.nextElement();
+                if (propertyName.startsWith(propertyPrefix)) {
+                    sb.append((String)properties.get(propertyName));
+                    sb.append("\n");
+                }
+            }
+            sb.append("<description><toolkit xsi:schemaLocation=\"http://oai.dlib.vt.edu/OAI/metadata/toolkit http://alcme.oclc.org/oaicat/toolkit.xsd\" xmlns=\"http://oai.dlib.vt.edu/OAI/metadata/toolkit\"><title>OCLC's OAICat Repository Framework</title><author><name>Jeffrey A. Young</name><email>jyoung@oclc.org</email><institution>OCLC</institution></author><version>");
+            sb.append(version);
+            sb.append("</version><toolkitIcon>http://alcme.oclc.org/oaicat/oaicat_icon.gif</toolkitIcon><URL>http://www.oclc.org/research/software/oai/cat.shtm</URL></toolkit></description>");
+            String descriptions = abstractCatalog.getDescriptions();
+            if (descriptions != null) {
+                sb.append(descriptions);
+            }
+            sb.append("</Identify>");
+        }
+        sb.append("</OAI-PMH>");
+        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/ListIdentifiers.java b/src/ORG/oclc/oai/server/verb/ListIdentifiers.java
new file mode 100644
index 0000000..8a3d2c4
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/ListIdentifiers.java
@@ -0,0 +1,240 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+
+/**
+ * This class represents a ListIdentifiers verb on either the server or
+ * on the client side.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class ListIdentifiers extends ServerVerb {
+    private static final boolean debug = false;
+    private static ArrayList validParamNames1 = new ArrayList();
+    static {
+	validParamNames1.add("verb");
+	validParamNames1.add("from");
+	validParamNames1.add("until");
+	validParamNames1.add("set");
+	validParamNames1.add("metadataPrefix");
+    }
+    private static ArrayList validParamNames2 = new ArrayList();
+    static {
+	validParamNames2.add("verb");
+	validParamNames2.add("resumptionToken");
+    }
+    private static ArrayList requiredParamNames1 = new ArrayList();
+    static {
+	requiredParamNames1.add("verb");
+	requiredParamNames1.add("metadataPrefix");
+    }
+    private static ArrayList requiredParamNames2 = new ArrayList();
+    static {
+	requiredParamNames2.add("verb");
+	requiredParamNames2.add("resumptionToken");
+    }
+
+    public static String construct(HashMap context,
+                                   HttpServletRequest request,
+                                   HttpServletResponse response,
+                                   Transformer serverTransformer)
+	throws OAIInternalServerError, TransformerException {
+        Properties properties =
+	    (Properties)context.get("OAIHandler.properties");
+	AbstractCatalog abstractCatalog =
+	    (AbstractCatalog)context.get("OAIHandler.catalog");
+	boolean xmlEncodeSetSpec = "true".equalsIgnoreCase(properties.getProperty("OAIHandler.xmlEncodeSetSpec"));
+	boolean urlEncodeSetSpec = !"false".equalsIgnoreCase(properties.getProperty("OAIHandler.urlEncodeSetSpec"));
+	String baseURL = properties.getProperty("OAIHandler.baseURL");
+	if (baseURL == null) {
+	    try {
+		baseURL = request.getRequestURL().toString();
+	    } catch (java.lang.NoSuchMethodError f) {
+		baseURL = request.getRequestURL().toString();
+	    }
+	}
+	StringBuffer sb = new StringBuffer();
+	String oldResumptionToken = request.getParameter("resumptionToken");
+	String metadataPrefix = request.getParameter("metadataPrefix");
+
+	if (metadataPrefix != null && metadataPrefix.length() == 0)
+	    metadataPrefix = null;
+
+	sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
+	if (styleSheet != null) {
+	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
+	    sb.append(styleSheet);
+	    sb.append("\"?>");
+	}
+	sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
+	sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+	sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
+	sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
+	sb.append("<responseDate>");
+	sb.append(createResponseDate(new Date()));
+	sb.append("</responseDate>");
+// 	sb.append("<requestURL>");
+// 	sb.append(getRequestURL(request));
+// 	sb.append("</requestURL>");
+
+	if (!abstractCatalog.isHarvestable()) {
+	    sb.append("<request verb=\"ListIdentifiers\">");
+	    sb.append(baseURL);
+	    sb.append("</request>");
+	    sb.append("<error code=\"badArgument\">Database is unavailable for harvesting</error>");
+	} else {
+	    ArrayList validParamNames = null;
+	    ArrayList requiredParamNames = null;
+	    Map listIdentifiersMap = null;
+	    if (oldResumptionToken == null) {
+		validParamNames = validParamNames1;
+		requiredParamNames = requiredParamNames1;
+		String from = request.getParameter("from");
+		String until = request.getParameter("until");
+		try {
+		    if (from != null && from.length() > 0 && from.length() < 10) {
+			throw new BadArgumentException();
+		    }
+		    if (until != null && until.length() > 0 && until.length() < 10) {
+			throw new BadArgumentException();
+		    }
+		    if (from != null && until != null && from.length() != until.length()) {
+			throw new BadArgumentException();
+		    }
+		    if (from == null || from.length() == 0) {
+			from = "0001-01-01";
+		    }
+		    if (until == null || until.length() == 0) {
+			until = "9999-12-31";
+		    }
+		    from = abstractCatalog.toFinestFrom(from);
+		    until = abstractCatalog.toFinestUntil(until);
+		    if (from.compareTo(until) > 0)
+			throw new BadArgumentException();
+		    String set = request.getParameter("set");
+                    if (set != null) {
+                        if (set.length() == 0) set = null;
+                        else if (urlEncodeSetSpec) set = set.replace(' ', '+');
+                    }
+		    Crosswalks crosswalks = abstractCatalog.getCrosswalks();
+		    if (metadataPrefix == null) {
+			throw new BadArgumentException();
+		    }
+
+		    if (!crosswalks.containsValue(metadataPrefix)) {
+			throw new CannotDisseminateFormatException(metadataPrefix);
+		    } else {
+			listIdentifiersMap = abstractCatalog.listIdentifiers(from, until, set,
+									     metadataPrefix);
+		    }
+		} catch (NoItemsMatchException e) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(e.getMessage());
+		} catch (BadArgumentException e) {
+		    sb.append("<request verb=\"ListIdentifiers\">");
+// 		    sb.append(HttpUtils.getRequestURL(request));
+		    sb.append(baseURL);
+		    sb.append("</request>");
+		    sb.append(e.getMessage());
+// 		} catch (BadGranularityException e) {
+// 		    sb.append(getRequestElement(request));
+// 		    sb.append(e.getMessage());
+		} catch (CannotDisseminateFormatException e) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(e.getMessage());
+		} catch (NoSetHierarchyException e) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(e.getMessage());
+		}
+	    } else {
+		validParamNames = validParamNames2;
+		requiredParamNames = requiredParamNames2;
+		if (hasBadArguments(request, requiredParamNames.iterator(), validParamNames, abstractCatalog)) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(new BadArgumentException().getMessage());
+		} else {
+		    try {
+			listIdentifiersMap = abstractCatalog.listIdentifiers(oldResumptionToken);
+		    } catch (BadResumptionTokenException e) {
+			sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+			sb.append(e.getMessage());
+		    }
+		}
+	    }
+
+	    if (listIdentifiersMap != null) {
+		sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		if (hasBadArguments(request, requiredParamNames.iterator(),
+				    validParamNames, abstractCatalog)) {
+		    sb.append(new BadArgumentException().getMessage());
+		} else {
+		    sb.append("<ListIdentifiers>");
+		    Iterator identifiers = (Iterator)listIdentifiersMap.get("headers");
+		    while (identifiers.hasNext()) {
+			sb.append((String)identifiers.next());
+		    }
+
+		    Map newResumptionMap = (Map)listIdentifiersMap.get("resumptionMap");
+		    if (newResumptionMap != null) {
+			String newResumptionToken = (String)newResumptionMap.get("resumptionToken");
+			String expirationDate = (String)newResumptionMap.get("expirationDate");
+			String completeListSize = (String)newResumptionMap.get("completeListSize");
+			String cursor = (String)newResumptionMap.get("cursor");
+			sb.append("<resumptionToken");
+			if (expirationDate != null) {
+			    sb.append(" expirationDate=\"");
+			    sb.append(expirationDate);
+			    sb.append("\"");
+			}
+			if (completeListSize != null) {
+			    sb.append(" completeListSize=\"");
+			    sb.append(completeListSize);
+			    sb.append("\"");
+			}
+			if (cursor != null) {
+			    sb.append(" cursor=\"");
+			    sb.append(cursor);
+			    sb.append("\"");
+			}
+			sb.append(">");
+			sb.append(newResumptionToken);
+			sb.append("</resumptionToken>");
+		    } else if (oldResumptionToken != null) {
+			sb.append("<resumptionToken />");
+		    }
+		    sb.append("</ListIdentifiers>");
+		}
+	    }
+	}
+	sb.append("</OAI-PMH>");
+	if (debug) {
+	    System.out.println("ListIdentifiers.constructListIdentifiers: returning: "
+			       + sb.toString());
+	}
+        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/ListMetadataFormats.java b/src/ORG/oclc/oai/server/verb/ListMetadataFormats.java
new file mode 100644
index 0000000..0b80a33
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/ListMetadataFormats.java
@@ -0,0 +1,187 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.crosswalk.Crosswalk;
+import ORG.oclc.oai.server.crosswalk.CrosswalkItem;
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+
+/**
+ * This class represents a ListMetadataFormats verb on either
+ * the client or on the server.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class ListMetadataFormats extends ServerVerb {
+    private static ArrayList validParamNames = new ArrayList();
+    static {
+        validParamNames.add("verb");
+        validParamNames.add("identifier");
+    }
+    private static ArrayList requiredParamNames = new ArrayList();
+    static {
+        requiredParamNames.add("verb");
+    }
+
+    /**
+     * Server-side construction of the xml response
+     *
+     * @param context the servlet context
+     * @param request the servlet request
+     * @exception OAIBadRequestException an http 400 status code problem
+     * @exception OAINotFoundException an http 404 status code problem
+     * @exception OAIInternalServerError an http 500 status code problem
+     */
+    public static String construct(HashMap context,
+            HttpServletRequest request,
+            HttpServletResponse response,
+            Transformer serverTransformer)
+    throws OAIInternalServerError, TransformerException {
+        Properties properties = (Properties)context.get("OAIHandler.properties");
+        AbstractCatalog abstractCatalog =
+            (AbstractCatalog)context.get("OAIHandler.catalog");
+        String baseURL = properties.getProperty("OAIHandler.baseURL");
+        if (baseURL == null) {
+            try {
+                baseURL = request.getRequestURL().toString();
+            } catch (java.lang.NoSuchMethodError f) {
+                baseURL = request.getRequestURL().toString();
+            }
+        }
+        StringBuffer sb = new StringBuffer();
+        String identifier = request.getParameter("identifier");
+        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+        String styleSheet = properties.getProperty("OAIHandler.styleSheet");
+        if (styleSheet != null) {
+            sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
+            sb.append(styleSheet);
+            sb.append("\"?>");
+        }
+        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
+        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
+        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
+        sb.append("<responseDate>");
+        sb.append(createResponseDate(new Date()));
+        sb.append("</responseDate>");
+//      sb.append("<requestURL>");
+//      sb.append(getRequestURL(request));
+//      sb.append("</requestURL>");
+        sb.append(getRequestElement(request, validParamNames, baseURL));
+        if (hasBadArguments(request, requiredParamNames.iterator(),
+                validParamNames, abstractCatalog)) {
+            sb.append(new BadArgumentException().getMessage());
+        } else {
+            Crosswalks crosswalks = abstractCatalog.getCrosswalks();
+            if (identifier == null || identifier.length() == 0) {
+                Iterator iterator = crosswalks.iterator();
+                sb.append("<ListMetadataFormats>");
+                while (iterator.hasNext()) {
+                    Map.Entry entry = (Map.Entry)iterator.next();
+                    String oaiSchemaLabel = (String)entry.getKey();
+                    CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
+                    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
+//                  StringTokenizer tokenizer = new StringTokenizer(crosswalk.getSchemaLocation());
+//                  String namespaceURI = tokenizer.nextToken();
+//                  String schemaURL = tokenizer.nextToken();
+                    String[] tokenizer = split(crosswalk.getSchemaLocation());
+                    String namespaceURI = null;
+                    String schemaURL = null;
+                    if (tokenizer.length == 1) {
+                        schemaURL = tokenizer[0];
+                    } else if (tokenizer.length > 1) {
+                        namespaceURI = tokenizer[0];
+                        schemaURL = tokenizer[1];
+                    }
+                    sb.append("<metadataFormat>");
+                    sb.append("<metadataPrefix>");
+                    sb.append(oaiSchemaLabel);
+                    sb.append("</metadataPrefix>");
+                    sb.append("<schema>");
+                    sb.append(schemaURL);
+                    sb.append("</schema>");
+                    sb.append("<metadataNamespace>");
+                    if (namespaceURI != null) {
+                    	sb.append(namespaceURI);
+                    }
+                    sb.append("</metadataNamespace>");
+                    sb.append("</metadataFormat>");
+                }
+                sb.append("</ListMetadataFormats>");
+            } else {
+                try {
+                    Vector schemaLocations = abstractCatalog.getSchemaLocations(identifier);
+                    sb.append("<ListMetadataFormats>");
+                    for (int i=0; i<schemaLocations.size(); ++i) {
+                        String schemaLocation = (String)schemaLocations.elementAt(i);
+//                      StringTokenizer tokenizer = new StringTokenizer(schemaLocation);
+//                      String namespaceURI = tokenizer.nextToken();
+//                      String schemaURL = tokenizer.nextToken();
+                        String[] tokenizer = split(schemaLocation);
+                        String namespaceURI = null;
+                        String schemaURL = null;
+                        if (tokenizer.length == 1) {
+                            schemaURL = tokenizer[0];
+                        } else if (tokenizer.length > 1) {
+                            namespaceURI = tokenizer[0];
+                            schemaURL = tokenizer[1];
+                        }
+                        sb.append("<metadataFormat>");
+                        sb.append("<metadataPrefix>");
+                        // make sure it's a space that separates them
+                        sb.append(crosswalks.getMetadataPrefix(namespaceURI, schemaURL));
+                        sb.append("</metadataPrefix>");
+                        sb.append("<schema>");
+                        sb.append(schemaURL);
+                        sb.append("</schema>");
+                        sb.append("<metadataNamespace>");
+                        if (namespaceURI != null) {
+                        	sb.append(namespaceURI);
+                        }
+                        sb.append("</metadataNamespace>");
+                        sb.append("</metadataFormat>");
+                    }
+                    sb.append("</ListMetadataFormats>");
+                } catch (IdDoesNotExistException e) {
+                    sb.append(e.getMessage());
+                } catch (NoMetadataFormatsException e) {
+                    sb.append(e.getMessage());
+                }
+            }
+        }
+        sb.append("</OAI-PMH>");
+        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
+    }
+
+    private static String[] split(String s) {
+        StringTokenizer tokenizer = new StringTokenizer(s);
+        String[] tokens = new String[tokenizer.countTokens()];
+        for (int i=0; i<tokens.length; ++i) {
+            tokens[i] = tokenizer.nextToken();
+        }
+        return tokens;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/ListRecords.java b/src/ORG/oclc/oai/server/verb/ListRecords.java
new file mode 100644
index 0000000..233491d
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/ListRecords.java
@@ -0,0 +1,254 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+
+/**
+ * Represents an OAI ListRecords Verb response. This class is used on both
+ * the client-side and on the server-side to represent a ListRecords response
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class ListRecords extends ServerVerb {
+    private static final boolean debug = false;
+    private static ArrayList validParamNames1 = new ArrayList();
+    static {
+	validParamNames1.add("verb");
+	validParamNames1.add("from");
+	validParamNames1.add("until");
+	validParamNames1.add("set");
+	validParamNames1.add("metadataPrefix");
+    }
+    private static ArrayList validParamNames2 = new ArrayList();
+    static {
+	validParamNames2.add("verb");
+	validParamNames2.add("resumptionToken");
+    }
+    private static ArrayList requiredParamNames1 = new ArrayList();
+    static {
+	requiredParamNames1.add("verb");
+	requiredParamNames1.add("metadataPrefix");
+    }
+    private static ArrayList requiredParamNames2 = new ArrayList();
+    static {
+	requiredParamNames2.add("verb");
+	requiredParamNames2.add("resumptionToken");
+    }
+
+    /**
+     * Server-side method to construct an xml response to a ListRecords verb.
+     */
+    public static String construct(HashMap context,
+                                   HttpServletRequest request, HttpServletResponse response,
+                                   Transformer serverTransformer)
+        throws OAIInternalServerError, TransformerException {
+        if (debug) System.out.println("ListRecords.construct: entered");
+
+        Properties properties = (Properties)context.get("OAIHandler.properties");
+        AbstractCatalog abstractCatalog = (AbstractCatalog)context.get("OAIHandler.catalog");
+	boolean xmlEncodeSetSpec = "true".equalsIgnoreCase(properties.getProperty("OAIHandler.xmlEncodeSetSpec"));
+	boolean urlEncodeSetSpec = !"false".equalsIgnoreCase(properties.getProperty("OAIHandler.urlEncodeSetSpec"));
+	String baseURL = properties.getProperty("OAIHandler.baseURL");
+	if (baseURL == null) {
+	    try {
+		baseURL = request.getRequestURL().toString();
+	    } catch (java.lang.NoSuchMethodError f) {
+		baseURL = request.getRequestURL().toString();
+	    }
+	}
+        StringBuffer sb = new StringBuffer();
+        String oldResumptionToken = request.getParameter("resumptionToken");
+	String metadataPrefix = request.getParameter("metadataPrefix");
+
+	if (metadataPrefix != null && metadataPrefix.length() == 0)
+	    metadataPrefix = null;
+
+        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
+	if (styleSheet != null) {
+	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
+	    sb.append(styleSheet);
+	    sb.append("\"?>");
+	}
+        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
+        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+	String extraXmlns = properties.getProperty("OAIHandler.extraXmlns");
+	if (extraXmlns != null)
+	    sb.append(" ").append(extraXmlns);
+        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
+        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
+        sb.append("<responseDate>");
+	sb.append(createResponseDate(new Date()));
+	sb.append("</responseDate>");
+//         sb.append("<requestURL>");
+//         sb.append(getRequestURL(request));
+//         sb.append("</requestURL>");
+
+	if (!abstractCatalog.isHarvestable()) {
+	    sb.append("<request verb=\"ListRecords\">");
+	    sb.append(baseURL);
+	    sb.append("</request>");
+	    sb.append("<error code=\"badArgument\">Database is unavailable for harvesting</error>");
+	} else {
+// 	    if (debug) {
+// 		System.gc();
+// 		System.gc();
+// 		Runtime rt = Runtime.getRuntime();
+// 		long freeMemoryK = rt.freeMemory() / 1024;
+// 		long totalMemoryK = rt.totalMemory() / 1024;
+// 		System.out.print("ListRecords.construct: " + oldResumptionToken);
+// 		System.out.print(" freeMemory=" + freeMemoryK / 1024.0 + "M");
+// 		System.out.print(" of " + totalMemoryK / 1024.0 + "M ");
+// 		System.out.println("(" + (100 * freeMemoryK) / totalMemoryK + "%)");
+// 	    }
+
+	    Map listRecordsMap = null;
+
+	    ArrayList validParamNames = null;
+	    ArrayList requiredParamNames = null;
+	    if (oldResumptionToken == null) {
+		validParamNames = validParamNames1;
+		requiredParamNames = requiredParamNames1;
+		String from = request.getParameter("from");
+		String until = request.getParameter("until");
+		try {
+		    if (from != null && from.length() > 0 && from.length() < 10) {
+			throw new BadArgumentException();
+		    }
+		    if (until != null && until.length() > 0 && until.length() < 10) {
+			throw new BadArgumentException();
+		    }
+		    if (from != null && until != null && from.length() != until.length()) {
+			throw new BadArgumentException();
+		    }
+		    if (from == null || from.length() == 0) {
+			from = "0001-01-01";
+		    }
+		    if (until == null || until.length() == 0) {
+			until = "9999-12-31";
+		    }
+		    from = abstractCatalog.toFinestFrom(from);
+		    until = abstractCatalog.toFinestUntil(until);
+		    if (from.compareTo(until) > 0)
+			throw new BadArgumentException();
+		    String set = request.getParameter("set");
+                    if (set != null) {
+                        if (set.length() == 0) set = null;
+                        else if (urlEncodeSetSpec) set = set.replace(' ', '+');
+                    }
+		    Crosswalks crosswalks = abstractCatalog.getCrosswalks();
+		    if (metadataPrefix == null) {
+			throw new BadArgumentException();
+		    }
+		    if (!crosswalks.containsValue(metadataPrefix)) {
+			throw new CannotDisseminateFormatException(metadataPrefix);
+		    } else {
+			listRecordsMap = abstractCatalog.listRecords(from, until, set,
+								     metadataPrefix);
+		    }
+		} catch (NoItemsMatchException e) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(e.getMessage());
+		} catch (BadArgumentException e) {
+		    sb.append("<request verb=\"ListRecords\">");
+		    sb.append(baseURL);
+		    sb.append("</request>");
+		    sb.append(e.getMessage());
+// 		} catch (BadGranularityException e) {
+// 		    sb.append(getRequestElement(request));
+// 		    sb.append(e.getMessage());
+		} catch (CannotDisseminateFormatException e) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(e.getMessage());
+		} catch (NoSetHierarchyException e) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(e.getMessage());
+		}
+	    } else {
+		validParamNames = validParamNames2;
+		requiredParamNames = requiredParamNames2;
+		if (hasBadArguments(request, requiredParamNames.iterator(), validParamNames, abstractCatalog)) {
+		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		    sb.append(new BadArgumentException().getMessage());
+		} else {
+		    try {
+			listRecordsMap = abstractCatalog.listRecords(oldResumptionToken);
+		    } catch (BadResumptionTokenException e) {
+			sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+			sb.append(e.getMessage());
+		    }
+		}
+	    }
+	    if (listRecordsMap != null) {
+		sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
+		if (hasBadArguments(request, requiredParamNames.iterator(),
+				    validParamNames, abstractCatalog)) {
+		    sb.append(new BadArgumentException().getMessage());
+		} else {
+		    sb.append("<ListRecords>\n");
+		    Iterator records = (Iterator)listRecordsMap.get("records");
+		    while (records.hasNext()) {
+			sb.append((String)records.next());
+			sb.append("\n");
+		    }
+		    Map newResumptionMap = (Map)listRecordsMap.get("resumptionMap");
+		    if (newResumptionMap != null) {
+			String newResumptionToken = (String)newResumptionMap.get("resumptionToken");
+			String expirationDate = (String)newResumptionMap.get("expirationDate");
+			String completeListSize = (String)newResumptionMap.get("completeListSize");
+			String cursor = (String)newResumptionMap.get("cursor");
+			sb.append("<resumptionToken");
+			if (expirationDate != null) {
+			    sb.append(" expirationDate=\"");
+			    sb.append(expirationDate);
+			    sb.append("\"");
+			}
+			if (completeListSize != null) {
+			    sb.append(" completeListSize=\"");
+			    sb.append(completeListSize);
+			    sb.append("\"");
+			}
+			if (cursor != null) {
+			    sb.append(" cursor=\"");
+			    sb.append(cursor);
+			    sb.append("\"");
+			}
+			sb.append(">");
+			sb.append(newResumptionToken);
+			sb.append("</resumptionToken>");
+		    } else if (oldResumptionToken != null) {
+			sb.append("<resumptionToken />");
+		    }
+		    sb.append("</ListRecords>");
+		}
+	    }
+	}
+        sb.append("</OAI-PMH>");
+        if (debug) {
+            System.out.println("ListRecords.constructListRecords: returning: " + sb.toString());
+        }
+        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/ListSets.java b/src/ORG/oclc/oai/server/verb/ListSets.java
new file mode 100644
index 0000000..32fa579
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/ListSets.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+
+/**
+ * A ListSets OAI verb representation.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class ListSets extends ServerVerb {
+    private static ArrayList validParamNames = new ArrayList();
+    static {
+	validParamNames.add("verb");
+	validParamNames.add("resumptionToken");
+    }
+    private static ArrayList requiredParamNames = new ArrayList();
+    static {
+	validParamNames.add("verb");
+    }
+
+    /**
+     * construct ListSets response
+     *
+     * @param context the context object from the local OAI server
+     * @param request the request object from the local OAI server
+     * @exception OAIInternalServerError
+     */
+    public static String construct(HashMap context, HttpServletRequest request,
+                                   HttpServletResponse response, Transformer serverTransformer)
+	throws OAIInternalServerError, TransformerException {
+        Properties properties =
+	    (Properties)context.get("OAIHandler.properties");
+	AbstractCatalog abstractCatalog =
+	    (AbstractCatalog)context.get("OAIHandler.catalog");
+	String baseURL = properties.getProperty("OAIHandler.baseURL");
+	if (baseURL == null) {
+	    try {
+		baseURL = request.getRequestURL().toString();
+	    } catch (java.lang.NoSuchMethodError f) {
+		baseURL = request.getRequestURL().toString();
+	    }
+	}
+        StringBuffer sb = new StringBuffer();
+	String oldResumptionToken = request.getParameter("resumptionToken");
+        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
+	if (styleSheet != null) {
+	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
+	    sb.append(styleSheet);
+	    sb.append("\"?>");
+	}
+        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
+        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
+        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
+        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
+        sb.append("<responseDate>");
+	sb.append(createResponseDate(new Date()));
+	sb.append("</responseDate>");
+//         sb.append("<requestURL>");
+//         sb.append(getRequestURL(request));
+//         sb.append("</requestURL>");
+	sb.append(getRequestElement(request, validParamNames, baseURL));
+	Map listSetsMap = null;
+	if (hasBadArguments(request, requiredParamNames.iterator(),
+			    validParamNames, abstractCatalog)) {
+	    sb.append(new BadArgumentException().getMessage());
+	} else {
+	    try {
+		if (oldResumptionToken == null) {
+		    listSetsMap = abstractCatalog.listSets();
+		} else {
+		    listSetsMap = abstractCatalog.listSets(oldResumptionToken);
+		}
+		sb.append("<ListSets>");
+		Iterator sets = (Iterator)listSetsMap.get("sets");
+		while (sets.hasNext()) {
+		    sb.append((String)sets.next());
+		}
+		Map newResumptionMap = (Map)listSetsMap.get("resumptionMap");
+		if (newResumptionMap != null) {
+		    String newResumptionToken = (String)newResumptionMap.get("resumptionToken");
+		    String expirationDate = (String)newResumptionMap.get("expirationDate");
+		    String completeListSize = (String)newResumptionMap.get("completeListSize");
+		    String cursor = (String)newResumptionMap.get("cursor");
+		    sb.append("<resumptionToken");
+		    if (expirationDate != null) {
+			sb.append(" expirationDate=\"");
+			sb.append(expirationDate);
+			sb.append("\"");
+		    }
+		    if (completeListSize != null) {
+			sb.append(" completeListSize=\"");
+			sb.append(completeListSize);
+			sb.append("\"");
+		    }
+		    if (cursor != null) {
+			sb.append(" cursor=\"");
+			sb.append(cursor);
+			sb.append("\"");
+		    }
+		    sb.append(">");
+		    sb.append(newResumptionToken);
+		    sb.append("</resumptionToken>");
+		} else if (oldResumptionToken != null) {
+		    sb.append("<resumptionToken />");
+		}
+		sb.append("</ListSets>");
+	    } catch (NoSetHierarchyException e) {
+		sb.append(e.getMessage());
+	    } catch (BadResumptionTokenException e) {
+		sb.append(e.getMessage());
+	    }
+	}
+        sb.append("</OAI-PMH>");
+	return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/NoItemsMatchException.java b/src/ORG/oclc/oai/server/verb/NoItemsMatchException.java
new file mode 100644
index 0000000..1272321
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/NoItemsMatchException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI cannotDisseminateFormatException
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class NoItemsMatchException extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public NoItemsMatchException() {
+	super("<error code=\"noRecordsMatch\">The combination of the values of the from, until, set, and metadataPrefix arguments results in an empty list.</error>");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/NoMetadataFormatsException.java b/src/ORG/oclc/oai/server/verb/NoMetadataFormatsException.java
new file mode 100644
index 0000000..f18f6b3
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/NoMetadataFormatsException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI cannotDisseminateFormatException
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class NoMetadataFormatsException extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public NoMetadataFormatsException() {
+	super("<error code=\"noMetadataFormats\">There are no metadata formats available for the specified item.</error>");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/NoRecordsMatchException.java b/src/ORG/oclc/oai/server/verb/NoRecordsMatchException.java
new file mode 100644
index 0000000..3a8e2fa
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/NoRecordsMatchException.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI cannotDisseminateFormatException
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class NoRecordsMatchException extends NoItemsMatchException {
+
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+}
diff --git a/src/ORG/oclc/oai/server/verb/NoSetHierarchyException.java b/src/ORG/oclc/oai/server/verb/NoSetHierarchyException.java
new file mode 100644
index 0000000..97f3bd7
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/NoSetHierarchyException.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * An OAI cannotDisseminateFormatException
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class NoSetHierarchyException extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public NoSetHierarchyException() {
+	super("<error code=\"noSetHierarchy\">The repository does not support sets.</error>");
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/OAIInternalServerError.java b/src/ORG/oclc/oai/server/verb/OAIInternalServerError.java
new file mode 100644
index 0000000..81e15eb
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/OAIInternalServerError.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+/**
+ * http 500 internal server error exception
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class OAIInternalServerError extends Exception {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Construct the exception
+     *
+     * @param s a message
+     */
+    public OAIInternalServerError(String s) { super(s); }
+}
diff --git a/src/ORG/oclc/oai/server/verb/ServerVerb.java b/src/ORG/oclc/oai/server/verb/ServerVerb.java
new file mode 100644
index 0000000..7995f52
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/ServerVerb.java
@@ -0,0 +1,299 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+import java.util.TimeZone;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.util.OAIUtil;
+//import javax.servlet.http.HttpUtils;
+
+/**
+ * ServerVerb is the parent class for each of the server-side OAI verbs.
+ *
+ * @author Jefffrey A. Young, OCLC Online Computer Library Center
+ */
+public abstract class ServerVerb {
+    private static final boolean debug = false;
+
+    private int statusCode = HttpServletResponse.SC_OK; // http status
+    private String message = null; // http response message
+
+    /**
+     * Complete XML response String
+     */
+    protected String xmlText = null;
+
+    /**
+     * Constructor
+     */
+    protected ServerVerb() { }
+
+    public static void init(Properties properties) throws Exception {}
+
+    /**
+     * initialize the Verb from the specified xml text
+     *
+     * @param xmlText complete XML response string
+     */
+    protected void init(String xmlText) {
+        if (debug) {
+            System.out.println("ServerVerb.init: xmlText=" + xmlText);
+        }
+        this.xmlText = xmlText;
+    }
+
+    /**
+     * Server-side verb constructor
+     *
+     * @param xmlText complete XML response string
+     */
+    public ServerVerb(String xmlText) {
+        init(xmlText);
+    }
+
+    /**
+     * Retrieve the http status code
+     *
+     * @return the http status code;
+     */
+    public int getStatus() { return statusCode; }
+
+    /**
+     * Retrieve the http status message
+     *
+     * @return the http status message;
+     */
+    public String getMessage() { return message; }
+
+    /**
+     * set the http status code and message
+     *
+     * @param statusCode the http status code
+     * @param message the http status message
+     */
+    protected void setError(int statusCode, String message) {
+        this.statusCode = statusCode;
+        this.message = message;
+    }
+
+    /**
+     * Create an OAI response date from the specified date
+     *
+     * @param date the date to be transformed to an OAI response date
+     * @return a String representation of the OAI response Date.
+     */
+    public static String createResponseDate(Date date) {
+        StringBuffer sb = new StringBuffer();
+        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        TimeZone tz = TimeZone.getTimeZone("UTC");
+        formatter.setTimeZone(tz);
+        sb.append(formatter.format(date));
+        return sb.toString();
+    }
+
+    protected static String getRequestElement(HttpServletRequest request,
+            List validParamNames,
+            String baseURL) {
+        return getRequestElement(request, validParamNames, baseURL, false);
+    }
+
+    protected static String getRequestElement(HttpServletRequest request,
+            List validParamNames,
+            String baseURL,
+            boolean xmlEncodeSetSpec) {
+        StringBuffer sb = new StringBuffer();
+        sb.append("<request");
+        Enumeration params = request.getParameterNames();
+        while (params.hasMoreElements()) {
+            String name = (String)params.nextElement();
+            if (validParamNames.contains(name)) {
+                String value = request.getParameter(name);
+                if (value != null && value.length() > 0) {
+                    sb.append(" ");
+                    sb.append(name);
+                    sb.append("=\"");
+                    if (!xmlEncodeSetSpec && "set".equals(name)) {
+//                      try {
+                        sb.append(value);
+//                      sb.append(URLEncoder.encode(value, "UTF-8"));
+//                      } catch (UnsupportedEncodingException e) {
+//                      e.printStackTrace();
+//                      sb.append("UnsupportedEncodingException");
+//                      }
+                    } else {
+                        sb.append(OAIUtil.xmlEncode(value));
+                    }
+                    sb.append("\"");
+                }
+            }
+        }
+        sb.append(">");
+        sb.append(baseURL);
+        sb.append("</request>");
+        return sb.toString();
+    }
+
+    protected static boolean hasBadArguments(HttpServletRequest request,
+            Iterator requiredParamNames,
+            List validParamNames,
+            AbstractCatalog catalog) {
+        while (requiredParamNames.hasNext()) {
+            String name = (String)requiredParamNames.next();
+            String value = request.getParameter(name);
+            if (value == null || value.length() == 0) {
+                return true;
+            }
+        }
+        Enumeration params = request.getParameterNames();
+        while (params.hasMoreElements()) {
+            String name = (String)params.nextElement();
+            String[] values = request.getParameterValues(name);
+            if (!validParamNames.contains(name)) {
+                return true;
+            } else if (values.length > 1) {
+                return true;
+            } else if (values.length == 1 && !catalog.isValidParam(name, values[0])) {
+            	return true;
+            }
+        }
+        String identifier = request.getParameter("identifier");
+        try {
+            if (identifier != null && identifier.length() > 0) {
+                identifier = URLEncoder.encode(identifier, "UTF-8");
+                new URI(identifier);
+            }
+        } catch (Exception e) {
+            return true;
+        }
+        return false;
+    }
+
+//  /**
+//  * Get the OAI requestURL from the verb response
+//  *
+//  * @param request the HTTP servlet request object
+//  * @return the current verb's requestURL value
+//  */
+//  protected static String getRequestURL(HttpServletRequest request) {
+//  StringBuffer sb = new StringBuffer();
+//  sb.append(HttpUtils.getRequestURL(request));
+//  sb.append("?");
+//  Enumeration params = request.getParameterNames();
+//  while (params.hasMoreElements()) {
+//  String name = (String)params.nextElement();
+//  String value = request.getParameter(name);
+//  sb.append(OAIUtil.xmlEncode(name));
+//  sb.append("=");
+//  sb.append(OAIUtil.xmlEncode(value));
+//  if (params.hasMoreElements()) {
+//  sb.append("&amp;");
+//  }
+//  }
+//  return sb.toString();
+//  }
+
+    /**
+     * Get the complete XML response for the verb request
+     *
+     * @return the complete XML response for the verb request
+     */
+    public String toString() {
+        return xmlText;
+    }
+
+    protected static String render(HttpServletResponse response, String contentType, String result,
+            Transformer transformer) throws TransformerException {
+        String renderedResult = null;
+        if (transformer != null) { // render on the server
+            response.setContentType("text/html; charset=UTF-8");
+            StringReader stringReader = new StringReader(result);
+            StreamSource streamSource = new StreamSource(stringReader);
+            StringWriter stringWriter = new StringWriter();
+            synchronized (transformer) {
+                transformer.transform(streamSource, new StreamResult(stringWriter));
+            }
+            renderedResult = stringWriter.toString();
+        } else { // render on the client
+            response.setContentType(contentType);
+            renderedResult = result;
+        }
+        return renderedResult;
+    }
+
+    public static HashMap getVerbs(Properties properties) {
+        HashMap serverVerbsMap = new HashMap();
+        serverVerbsMap.put("ListRecords", ListRecords.class);
+        serverVerbsMap.put("ListIdentifiers", ListIdentifiers.class);
+        serverVerbsMap.put("GetRecord", GetRecord.class);
+        serverVerbsMap.put("Identify", Identify.class);
+        serverVerbsMap.put("ListMetadataFormats", ListMetadataFormats.class);
+        serverVerbsMap.put("ListSets", ListSets.class);
+        return serverVerbsMap;
+    }
+
+    public static HashMap getExtensionVerbs(Properties properties) {
+        HashMap extensionVerbsMap = new HashMap();
+        String propertyPrefix = "ExtensionVerbs.";
+        Enumeration propNames = properties.propertyNames();
+        while (propNames.hasMoreElements()) {
+            String propertyName = (String)propNames.nextElement();
+            if (propertyName.startsWith(propertyPrefix)) {
+                String verb = propertyName.substring(propertyPrefix.length());
+                String verbClassName = (String)properties.get(propertyName);
+                if (debug) {
+                    System.out.println("ExtensionVerb.getVerbs: verb=" + verb);
+                    System.out.println("ExtensionVerb.verbClassName=" + verbClassName);
+                }
+                try {
+                    Class serverVerbClass = Class.forName(verbClassName);
+                    Method init =
+                        serverVerbClass.getMethod("init",
+                                new Class[] {Properties.class});
+                    try {
+                        init.invoke(null, new Object[] {properties});
+                    } catch (InvocationTargetException e) {
+                        throw e.getTargetException();
+                    }
+                    extensionVerbsMap.put(verb, serverVerbClass);
+                    if (debug) {
+                        System.out.println("ExtensionVerb.getVerbs: " + verb + "=" + verbClassName);
+                    }
+                } catch (Throwable e) {
+                    System.err.println("ExtensionVerb: couldn't construct: " + verbClassName);
+                    e.printStackTrace();
+                }
+            }
+        }
+        return extensionVerbsMap;
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/extension/GetMetadata.java b/src/ORG/oclc/oai/server/verb/extension/GetMetadata.java
new file mode 100644
index 0000000..5bf6f52
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/extension/GetMetadata.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb.extension;
+
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+import ORG.oclc.oai.server.verb.BadArgumentException;
+import ORG.oclc.oai.server.verb.BadVerb;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.server.verb.ServerVerb;
+
+/**
+ * This class represents a GetMetadata response on either the server or
+ * the client.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class GetMetadata extends ServerVerb {
+    private static final boolean debug = false;
+    private static ArrayList validParamNames = new ArrayList();
+    static {
+	validParamNames.add("verb");
+	validParamNames.add("identifier");
+	validParamNames.add("metadataPrefix");
+    }
+    
+    /**
+     * Construct the xml response on the server-side.
+     *
+     * @param context the servlet context
+     * @param request the servlet request
+     * @return a String containing the XML response
+     * @exception OAIBadRequestException an http 400 status error occurred
+     * @exception OAINotFoundException an http 404 status error occurred
+     * @exception OAIInternalServerError an http 500 status error occurred
+     */
+    public static String construct(HashMap context,
+                                   HttpServletRequest request, HttpServletResponse response,
+                                   Transformer serverTransformer)
+        throws FileNotFoundException, TransformerException {
+        Properties properties = (Properties)context.get("OAIHandler.properties");
+	AbstractCatalog abstractCatalog =
+	    (AbstractCatalog)context.get("OAIHandler.catalog");
+	String baseURL = properties.getProperty("OAIHandler.baseURL");
+	if (baseURL == null) {
+	    try {
+		baseURL = request.getRequestURL().toString();
+	    } catch (java.lang.NoSuchMethodError f) {
+		baseURL = request.getRequestURL().toString();
+	    }
+	}
+        StringBuffer sb = new StringBuffer();
+        String identifier = request.getParameter("identifier");
+        String metadataPrefix = request.getParameter("metadataPrefix");
+
+        if (debug) {
+            System.out.println("GetMetadata.constructGetMetadata: identifier=" +
+                               identifier);
+            System.out.println("GetMetadata.constructGetMetadata: metadataPrefix="
+                               + metadataPrefix);
+        }
+        Crosswalks crosswalks = abstractCatalog.getCrosswalks();
+        sb.append("<?xml version=\"1.0\" encoding=\"");
+        String encoding = crosswalks.getEncoding(metadataPrefix);
+        if (encoding != null)
+            sb.append(encoding);
+        else
+            sb.append("UTF-8");
+        sb.append("\" ?>\n");
+        
+        String docType = crosswalks.getDocType(metadataPrefix);
+        if (docType != null) {
+            sb.append(docType);
+            sb.append("\n");
+        }
+	try {
+	    if (metadataPrefix == null || metadataPrefix.length() == 0
+		|| identifier == null || identifier.length() == 0) {
+                if (debug) System.out.println("Bad argument");
+		throw new BadArgumentException();
+	    }
+	    else if (!crosswalks.containsValue(metadataPrefix)) {
+                if (debug) System.out.println("crosswalk not present: " + metadataPrefix);
+		throw new CannotDisseminateFormatException(metadataPrefix);
+	    } else {
+ 		String metadata = abstractCatalog.getMetadata(identifier, metadataPrefix);
+ 		if (metadata != null) {
+                    sb.append(metadata);
+ 		} else {
+                    if (debug) System.out.println("ID does not exist");
+ 		    throw new IdDoesNotExistException(identifier);
+ 		}
+	    }
+	} catch (BadArgumentException e) {
+            if (debug) e.printStackTrace();
+            throw new FileNotFoundException();
+	} catch (CannotDisseminateFormatException e) {
+            if (debug) e.printStackTrace();
+            throw new FileNotFoundException();
+	} catch (IdDoesNotExistException e) {
+            if (debug) e.printStackTrace();
+            throw new FileNotFoundException();
+	} catch (OAIInternalServerError e) {
+            e.printStackTrace();
+            return BadVerb.construct(context, request, response, serverTransformer);
+	}
+        if (debug) {
+            System.out.println("GetMetadata.construct: contentType=" + crosswalks.getContentType(metadataPrefix));
+            System.out.println("GetMetadata.construct: prerendered sb=" + sb.toString());
+        }
+        return render(response, crosswalks.getContentType(metadataPrefix), sb.toString(), (Transformer)null);
+    }
+}
diff --git a/src/ORG/oclc/oai/server/verb/extension/Redirect.java b/src/ORG/oclc/oai/server/verb/extension/Redirect.java
new file mode 100644
index 0000000..3b2355d
--- /dev/null
+++ b/src/ORG/oclc/oai/server/verb/extension/Redirect.java
@@ -0,0 +1,180 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.server.verb.extension;
+
+import java.io.FileNotFoundException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.crosswalk.Crosswalks;
+import ORG.oclc.oai.server.verb.BadArgumentException;
+import ORG.oclc.oai.server.verb.BadVerb;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.server.verb.ServerVerb;
+
+/**
+ * This class represents a Redirect response on either the server or
+ * the client.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class Redirect extends ServerVerb {
+    private static final boolean debug = true;
+    private static Transformer transformer;
+    static {
+        String xsltString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
++"<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n"
++"                              xmlns:oai=\"http://www.openarchives.org/OAI/2.0/\"\n"
++"                              xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\"\n"
++"                              xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n"
++"  <xsl:output method=\"html\" version=\"4.0\"/>\n"
++"  <xsl:param name=\"base.url\"/>\n"
++"\n"
++"  <xsl:template match=\"/\">\n"
++"    <html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
++"      <xsl:choose>\n"
++"        <xsl:when test=\"record/metadata/oai_dc:dc/dc:identifier[1]\">\n"
++"          <head>\n"
++"            <meta http-equiv=\"Refresh\">\n"
++"              <xsl:attribute name=\"content\"><xsl:text>0; URL=</xsl:text><xsl:value-of select=\"record/metadata/oai_dc:dc/dc:identifier[1]\" /></xsl:attribute>\n"
++"            </meta>\n"
++"          </head>\n"
++"          <body/>\n"
++"       </xsl:when>\n"            
++"       <xsl:when test=\"$base.url\">\n"
++"          <head>\n"
++"            <meta http-equiv=\"Refresh\">\n"
++"              <xsl:attribute name=\"content\"><xsl:text>0; URL=</xsl:text><xsl:value-of select=\"$base.url\" /><xsl:text>/extension?verb=GetMetadata&amp;metadataPrefix=oai_dc&amp;identifier=</xsl:text><xsl:value-of select=\"record/header/identifier\" /></xsl:attribute>\n"
++"            </meta>\n"
++"          </head>\n"
++"          <body/>\n"
++"       </xsl:when>\n"
++"       <xsl:otherwise>\n"
++"          <head>\n"
++"            <title><xsl:text>No dc:identifier field found for &apos;</xsl:text><xsl:value-of select=\"record/header/identifier\"/><xsl:text>&apos;</xsl:text></title>\n"
++"          </head>\n"
++"          <body>\n"
++"            <h1><xsl:text>No dc:identifier field found for &apos;</xsl:text><xsl:value-of select=\"record/header/identifier\"/><xsl:text>&apos;</xsl:text></h1>\n"
++"          </body>\n"
++"       </xsl:otherwise>\n"            
++"      </xsl:choose>\n"
++"    </html>\n"
++"  </xsl:template>\n"
++"</xsl:stylesheet>\n";
+        try {
+            if (debug) {
+                System.out.println("Redirect.<init>: xsltString=" + xsltString);
+            }
+            StreamSource xslSource = new StreamSource(new StringReader(xsltString));
+            TransformerFactory tFactory = TransformerFactory.newInstance();
+            transformer = tFactory.newTransformer(xslSource);
+        } catch (TransformerException e) {
+            e.printStackTrace();
+        }
+    };
+    private static ArrayList validParamNames = new ArrayList();
+    static {
+	validParamNames.add("verb");
+	validParamNames.add("identifier");
+    }
+    
+    /**
+     * Construct the xml response on the server-side.
+     *
+     * @param context the servlet context
+     * @param request the servlet request
+     * @return a String containing the XML response
+     * @exception OAIBadRequestException an http 400 status error occurred
+     * @exception OAINotFoundException an http 404 status error occurred
+     * @exception OAIInternalServerError an http 500 status error occurred
+     */
+    public static String construct(HashMap context,
+                                   HttpServletRequest request, HttpServletResponse response,
+                                   Transformer serverTransformer)
+        throws FileNotFoundException, TransformerException {
+        Properties properties = (Properties)context.get("OAIHandler.properties");
+	AbstractCatalog abstractCatalog =
+	    (AbstractCatalog)context.get("OAIHandler.catalog");
+	String baseURL = properties.getProperty("OAIHandler.baseURL");
+	if (baseURL == null) {
+	    try {
+		baseURL = request.getRequestURL().toString();
+	    } catch (java.lang.NoSuchMethodError f) {
+		baseURL = request.getRequestURL().toString();
+	    }
+	}
+        StringBuffer sb = new StringBuffer();
+        String identifier = request.getParameter("identifier");
+
+        if (debug) {
+            System.out.println("Redirect.construct: identifier=" +
+                               identifier);
+        }
+        Crosswalks crosswalks = abstractCatalog.getCrosswalks();
+	try {
+	    if (identifier == null || identifier.length() == 0) {
+                if (debug) System.out.println("Bad argument");
+		throw new BadArgumentException();
+	    }
+	    else if (!crosswalks.containsValue("oai_dc")) {
+                if (debug) System.out.println("crosswalk not present: oai_dc");
+		throw new CannotDisseminateFormatException("oai_dc");
+	    } else {
+ 		String metadata = abstractCatalog.getRecord(identifier, "oai_dc");
+ 		if (metadata != null) {
+                    sb.append(metadata);
+  		} else {
+                    if (debug) System.out.println("ID does not exist");
+  		    throw new IdDoesNotExistException(identifier);
+ 		}
+	    }
+	} catch (BadArgumentException e) {
+            if (debug) e.printStackTrace();
+            throw new FileNotFoundException(e.getMessage());
+	} catch (CannotDisseminateFormatException e) {
+            if (debug) e.printStackTrace();
+            throw new FileNotFoundException(e.getMessage());
+ 	} catch (IdDoesNotExistException e) {
+            if (debug) e.printStackTrace();
+            throw new FileNotFoundException(e.getMessage());
+	} catch (OAIInternalServerError e) {
+            e.printStackTrace();
+            return BadVerb.construct(context, request, response, serverTransformer);
+	}
+        if (debug) {
+            System.out.println("Redirect.construct: prerendered sb=" + sb.toString());
+        }
+        if (debug) {
+            System.out.println("Redirect.construct: transformer=" + transformer);
+        }
+        synchronized (transformer) {
+            transformer.setParameter("base.url", baseURL);
+            String out = render(response, (String)null, sb.toString(), transformer);
+            transformer.clearParameters();
+            if (debug) {
+                System.out.println("Redirect.construct: out=" + out);
+            }
+            return out;
+        }
+    }
+}
diff --git a/src/ORG/oclc/oai/util/OAIUtil.java b/src/ORG/oclc/oai/util/OAIUtil.java
new file mode 100644
index 0000000..57d84ed
--- /dev/null
+++ b/src/ORG/oclc/oai/util/OAIUtil.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ORG.oclc.oai.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+
+/**
+ * Utility methods for OAICat and OAIHarvester
+ */
+public class OAIUtil {
+    private static DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+    private static TransformerFactory tFactory = TransformerFactory.newInstance();
+    static {
+        dbFactory.setNamespaceAware(true);
+    }
+    
+    /**
+     * XML encode a string.
+     * @param s any String
+     * @return the String with &amp;, &lt;, and &gt; encoded for use in XML.
+     */
+    public static String xmlEncode(String s) {
+        StringBuffer sb = new StringBuffer();
+
+        for (int i=0; i<s.length(); ++i) {
+            char c = s.charAt(i);
+            switch (c) {
+            case '&':
+                sb.append("&amp;");
+                break;
+            case '<':
+                sb.append("&lt;");
+                break;
+            case '>':
+                sb.append("&gt;");
+                break;
+            case '"':
+                sb.append("&quot;");
+                break;
+            case '\'':
+                sb.append("&apos;");
+                break;
+            default:
+                sb.append(c);
+                break;
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Convert a packed LCCN String to MARC display format.
+     * @param packedLCCN an LCCN String in packed storage format (e.g. 'n&nbsp;&nbsp;2001050268').
+     * @return  an LCCN String in MARC display format (e.g. 'n2001-50268').
+     */
+    public static String toLCCNDisplay(String packedLCCN) {
+	StringBuffer sb = new StringBuffer();
+	if (Character.isDigit(packedLCCN.charAt(2))) {
+	    sb.append(packedLCCN.substring(0, 2).trim());
+	    sb.append(packedLCCN.substring(2, 6));
+	    sb.append("-");
+	    int i = Integer.parseInt(packedLCCN.substring(6).trim());
+	    sb.append(Integer.toString(i));
+	} else {
+	    sb.append(packedLCCN.substring(0, 3).trim());
+	    sb.append(packedLCCN.substring(3, 5));
+	    sb.append("-");
+	    int i = Integer.parseInt(packedLCCN.substring(5).trim());
+	    sb.append(Integer.toString(i));
+	}
+	return sb.toString();
+    }
+
+    /**
+     * convert a packed LCCN to display format.
+     * @param packedLCCN an LCCN String in packed storage format (e.g. 'n&nbsp;&nbsp;2001050268').
+     * @return  an LCCN String in MARC display format (e.g. 'n2001-50268').
+     * @deprecated use toLCCNDisplay() instead.
+     */
+    public static String getLCCN(String packedLCCN) {
+	return toLCCNDisplay(packedLCCN);
+    }
+
+    public static Document parse(InputStream is)
+    throws SAXException, IOException, ParserConfigurationException {
+        return getThreadedDocumentBuilder().parse(is);
+    }
+
+    public static DocumentBuilder getThreadedDocumentBuilder()
+    throws ParserConfigurationException {
+        DocumentBuilder builder = dbFactory.newDocumentBuilder();
+        return builder;
+    }
+    
+    /**
+     * Transform a DOM Node into an XML String.
+     * @param node
+     * @return an XML String representation of the specified Node
+     * @throws TransformerException
+     */
+    public static String toString(Node node)
+    throws TransformerException {
+        return toString(node, true);
+    }
+    
+    /**
+     * Transform a DOM Node into an XML String
+     * @param node
+     * @param omitXMLDeclaration
+     * @return an XML String representation of the specified Node
+     * @throws TransformerException
+     */
+    public static String toString(Node node, boolean omitXMLDeclaration)
+    throws TransformerException {
+        StringWriter writer = new StringWriter();
+        Transformer transformer =
+            getThreadedIdentityTransformer(omitXMLDeclaration);
+        Source source = new DOMSource(node);
+        Result result = new StreamResult(writer);
+        transformer.transform(source, result);
+        return writer.toString();
+    }
+    
+    /**
+     * Get a thread-safe Transformer without an assigned transform. This is useful
+     * for transforming a DOM Document into XML text.
+     * @param omitXmlDeclaration 
+     * @return an "identity" Transformer assigned to the current thread
+     * @throws TransformerConfigurationException
+     */
+    public static Transformer getThreadedIdentityTransformer(
+            boolean omitXmlDeclaration)
+    throws TransformerConfigurationException {
+        return getTransformer(omitXmlDeclaration, (String) null);
+    }
+    
+    /**
+     * Get a thread-safe Transformer.
+     * @param omitXmlDeclaration
+     * @param xslURL
+     * @return a thread-safe Transformer
+     * @throws TransformerConfigurationException
+     */
+    public static Transformer getTransformer(boolean omitXmlDeclaration,
+            String xslURL)
+    throws TransformerConfigurationException {
+        return getTransformer(omitXmlDeclaration, true, xslURL);
+    }
+    
+    /**
+     * @param omitXmlDeclaration
+     * @param standalone
+     * @param xslURL 
+     * @return a Transformer for the specified XSL document
+     * @throws TransformerConfigurationException
+     */
+    public static Transformer getTransformer(
+            boolean omitXmlDeclaration,
+            boolean standalone, String xslURL)
+    throws TransformerConfigurationException {
+        Transformer transformer = null;
+        if (xslURL == null) {
+            transformer = tFactory.newTransformer(); // "never null"
+        } else {
+            Source xslSource = null;
+            if (xslURL.startsWith("file://")) {
+                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(xslURL.substring(6));
+                xslSource = new StreamSource(is);
+            } else {
+                xslSource = new StreamSource(xslURL);
+            }
+            transformer = tFactory.newTransformer(xslSource);
+        }
+//      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty(OutputKeys.STANDALONE,
+                standalone?"yes":"no");
+        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
+                omitXmlDeclaration?"yes":"no");
+        return transformer;
+    }
+    
+}
diff --git a/src/com/acme/oai/DummyOAICatalog.java b/src/com/acme/oai/DummyOAICatalog.java
new file mode 100644
index 0000000..62683b8
--- /dev/null
+++ b/src/com/acme/oai/DummyOAICatalog.java
@@ -0,0 +1,744 @@
+
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.acme.oai;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import ORG.oclc.oai.server.catalog.AbstractCatalog;
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+
+/**
+ * DummyOAICatalog is an example of how to implement the AbstractCatalog interface.
+ * Pattern an implementation of the AbstractCatalog interface after this class
+ * to have OAICat work with your database. Your effort may be minimized by confining
+ * your changes to areas identified by "YOUR CODE GOES HERE" comments. In truth, though,
+ * you can do things however you want, as long as the non-private methods return
+ * what they're supposed to.
+ *
+ * @author Jeffrey A. Young, OCLC Online Computer Library Center
+ */
+public class DummyOAICatalog extends AbstractCatalog {
+    /**
+     * maximum number of entries to return for ListRecords and ListIdentifiers
+     */
+    private static int maxListSize;
+
+    /**
+     * pending resumption tokens
+     */
+    private HashMap resumptionResults = new HashMap();
+
+    /**************************************************************
+     * YOUR CODE GOES HERE
+     * delete dummyDb and create new private variables
+     * to manage your database here
+     **************************************************************/
+    
+    /**
+     * Dummy database. Note that the two items each contain two metadataFormats;
+     * oai_dc and oai_etdms
+     */
+    private String[] dummyDb = {
+        "<record><header><identifier>oai:oaicat.oclc.org:OCLCNo/ocm00000012</identifier><datestamp>2001-02-02</datestamp><setSpec>music</setSpec><setSpec>music:(muzak)</setSpec></header><metadata><oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd\"><dc:language>eng</dc:language><dc:date>1964</dc:date><dc:type>Text data</dc:type><dc:identifier>ocm00000012</dc:identifier><dc:identifier>64063999 //r83</dc:identifier><dc:coverage>n-us---</dc:coverage><dc:subject>KF6369.3--.G3</dc:subject><dc:subject>340</dc:subject><dc:creator>Galles, George Raymond,--1918-</dc:creator><dc:title>Involuntary conversions under the Federal income tax laws.</dc:title><dc:publisher>[University of Idaho, Bureau of Business and Economic Research]</dc:publisher><dc:publisher>Moscow</dc:publisher><dc:format>v, 46 l.</dc:format><dc:format>28 cm.</dc:format><dc:relation>Idaho BBER--monograph no. 2</dc:relation><dc:subject>Income tax--Law and legislation--United States.</dc:subject><dc:relation>University of Idaho.--Bureau of Business and Economic Research.--Idaho BBER monograph ;--no. 2.</dc:relation></oai_dc:dc><oai_etdms:thesis xmlns:oai_etdms=\"http://www.ndltd.org/standards/metadata/etdms/1.0/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd\"><oai_etdms:title>Involuntary conversions under the Federal income tax laws.</oai_etdms:title><oai_etdms:creator>Galles, George Raymond,--1918-</oai_etdms:creator><oai_etdms:subject>KF6369.3--.G3</oai_etdms:subject><oai_etdms:subject>340</oai_etdms:subject><oai_etdms:subject>Income tax--Law and legislation--United States.</oai_etdms:subject><oai_etdms:publisher>[University of Idaho, Bureau of Business and Economic Research]</oai_etdms:publisher><oai_etdms:publisher>Moscow</oai_etdms:publisher><oai_etdms:date>1964</oai_etdms:date><oai_etdms:type>Text data</oai_etdms:type><oai_etdms:format>v, 46 l.--28 cm.</oai_etdms:format><oai_etdms:identifier>ocm00000012</oai_etdms:identifier><oai_etdms:identifier>64063999 //r83</oai_etdms:identifier><oai_etdms:language>eng</oai_etdms:language><oai_etdms:coverage>n-us---</oai_etdms:coverage></oai_etdms:thesis></metadata></record>",
+        "<rec><header><identifier>oai:oaicat.oclc.org:OCLCNo/ocm00003601</identifier><datestamp>2001-02-02</datestamp><setSpec>music</setSpec></header><metadata><oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd\"><dc:language>eng</dc:language><dc:date>1967</dc:date><dc:type>Text data</dc:type><dc:identifier>ocm00003600</dc:identifier><dc:identifier>68066972 //r915</dc:identifier><dc:subject>E78.W3--W3 no. 25</dc:subject><dc:subject>970.4/96/37</dc:subject><dc:creator>Barnes, Paul L.</dc:creator><dc:title>Archaeology of the Dean Site, Twin Falls County, Idaho,</dc:title><dc:publisher>[Pullman,</dc:publisher><dc:format>vii, 89 p.--illus., maps.</dc:format><dc:format>28 cm.</dc:format><dc:relation>Laboratory of Anthropology, Washington State University. Report of investigations,--no. 25</dc:relation><dc:subject>Dean site.</dc:subject><dc:subject>Idaho--Antiquities.</dc:subject><dc:relation>Reports of investigations (Washington State University. Laboratory of Anthropology) ;--no. 25.</dc:relation></oai_dc:dc><oai_etdms:thesis xmlns:oai_etdms=\"http://www.ndltd.org/standards/metadata/etdms/1.0/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd\"><oai_etdms:title>Archaeology of the Dean Site, Twin Falls County, Idaho,</oai_etdms:title><oai_etdms:creator>Barnes, Paul L.</oai_etdms:creator><oai_etdms:subject>E78.W3--W3 no. 25</oai_etdms:subject><oai_etdms:subject>970.4/96/37</oai_etdms:subject><oai_etdms:subject>Dean site.</oai_etdms:subject><oai_etdms:subject>Idaho--Antiquities.</oai_etdms:subject><oai_etdms:publisher>[Pullman,</oai_etdms:publisher><oai_etdms:date>1967</oai_etdms:date><oai_etdms:type>Text data</oai_etdms:type><oai_etdms:format>vii, 89 p.--illus., maps.--28 cm.</oai_etdms:format><oai_etdms:identifier>ocm00003600</oai_etdms:identifier><oai_etdms:identifier>68066972 //r915</oai_etdms:identifier><oai_etdms:language>eng</oai_etdms:language></oai_etdms:thesis></metadata></rec>"
+    };
+    
+    /**************************************************************
+     * YOUR CODE GOES HERE
+     * delete dummySets and then figure out an alternative way
+     * to identify the sets to be returned to ListSets requests.
+     * One way might be to include them in the oaicat.properties
+     * file and then have the constructor grab them. The ideal
+     * solution, though,  is probably to query your database
+     * somehow for any sets that exist there.
+     **************************************************************/
+    
+    /**
+     * Dummy sets.
+     */
+    private String[] dummySets = {
+        "<set><setSpec>music</setSpec><setName>Music collection</setName></set>",
+        "<set><setSpec>music:(muzak)</setSpec><setName>Muzak collection</setName></set>",
+        "<set><setSpec>music:(elec)</setSpec><setName>Electronic Music Collection</setName><setDescription><oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.epenarchives.org/OAI/2.0/oai_dc.xsd\"><dc:description>This set contains metadata describing electronic music recordings made during the 1950ies</dc:description></oai_dc:dc></setDescription></set>"
+    };
+    
+    /**
+     * Construct a DummyOAICatalog object
+     *
+     * @param properties a properties object containing initialization parameters
+     */
+    public DummyOAICatalog(Properties properties) {
+        String maxListSize = properties.getProperty("DummyOAICatalog.maxListSize");
+        if (maxListSize == null) {
+            throw new IllegalArgumentException("DummyOAICatalog.maxListSize is missing from the properties file");
+        } else {
+            DummyOAICatalog.maxListSize = Integer.parseInt(maxListSize);
+        }
+        
+        /************************************************************
+         * YOUR CODE GOES HERE
+         * Load other properties you need to initialize your database
+         * (and perhaps sets) and store them in private instance
+         * variables.
+         ************************************************************/
+        /************************************************************
+         * YOUR CODE GOES HERE
+         * Given the configuration properties above, open your
+         * database here and store any necessary variables
+         * to manage it in private instance variables.
+         ************************************************************/
+    }
+    
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * identifier.
+     *
+     * @param identifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception IdDoesNotExistException the specified identifier can't be found
+     * @exception NoMetadataFormatsException the specified identifier was found
+     * but the item is flagged as deleted and thus no schemaLocations (i.e.
+     * metadataFormats) can be produced.
+     */
+    public Vector getSchemaLocations(String identifier)
+        throws IdDoesNotExistException, NoMetadataFormatsException {
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         * Retrieve the specified native item from your database.
+         **********************************************************************/
+        Object nativeItem = dummyDb[0];
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        /*
+         * Let your recordFactory decide which schemaLocations
+         * (i.e. metadataFormats) it can produce from the record.
+         * Doing so will preserve the separation of database access
+         * (which happens here) from the record content interpretation
+         * (which is the responsibility of the RecordFactory implementation).
+         */
+        if (nativeItem == null) {
+            throw new IdDoesNotExistException(identifier);
+        } else {
+            return getRecordFactory().getSchemaLocations(nativeItem);
+        }
+    }
+
+    /**
+     * Retrieve a list of identifiers that satisfy the specified criteria
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * It may seem strange for the map to include both "headers" and "identifiers"
+     * since the identifiers can be obtained from the headers. This may be true, but
+     * AbstractCatalog.listRecords() can operate quicker if it doesn't
+     * need to parse identifiers from the XML headers itself. Better
+     * still, do like I do below and override AbstractCatalog.listRecords().
+     * AbstractCatalog.listRecords() is relatively inefficient because given the list
+     * of identifiers, it must call getRecord() individually for each as it constructs
+     * its response. It's much more efficient to construct the entire response in one fell
+     * swoop by overriding listRecords() as I've done here.
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix) {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         **********************************************************************/
+        
+        /* Get some records from your database */
+        Object[] nativeItems = dummyDb;
+        int count;
+        
+        /* load the headers and identifiers ArrayLists. */
+        for (count=0; count < maxListSize && count < nativeItems.length; ++count) {
+            /* Use the RecordFactory to extract header/identifier pairs for each item */
+            String[] header = getRecordFactory().createHeader(nativeItems[count]);
+            headers.add(header[0]);
+            identifiers.add(header[1]);
+        }
+        
+        /* decide if you're done */
+        if (count < nativeItems.length) {
+            String resumptionId = getResumptionId();
+            /*****************************************************************
+             * Store an object appropriate for your database API in the
+             * resumptionResults Map in place of nativeItems. This object
+             * should probably encapsulate the information necessary to
+             * perform the next resumption of ListIdentifiers. It might even
+             * be possible to encode everything you need in the
+             * resumptionToken, in which case you won't need the
+             * resumptionResults Map. Here, I've done a silly combination
+             * of the two. Stateless resumptionTokens have some advantages.
+             *****************************************************************/
+            resumptionResults.put(resumptionId, nativeItems);
+
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                                                                     nativeItems.length,
+                                                                     0));
+            //          listIdentifiersMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSb.toString()));
+        }
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     */
+    public Map listIdentifiers(String resumptionToken)
+        throws BadResumptionTokenException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         **********************************************************************/
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+        
+        /* Get some more records from your database */
+        Object[] nativeItems = (Object[])resumptionResults.remove(resumptionId);
+        if (nativeItems == null) {
+            throw new BadResumptionTokenException();
+        }
+        int count;
+        
+        /* load the headers and identifiers ArrayLists. */
+        for (count = 0; count < maxListSize && count+oldCount < nativeItems.length; ++count) {
+            /* Use the RecordFactory to extract header/identifier pairs for each item */
+            String[] header = getRecordFactory().createHeader(nativeItems[count+oldCount]);
+            headers.add(header[0]);
+            identifiers.add(header[1]);
+        }
+        
+        /* decide if you're done. */
+        if (count+oldCount < nativeItems.length) {
+            resumptionId = getResumptionId();
+            
+            /*****************************************************************
+             * Store an object appropriate for your database API in the
+             * resumptionResults Map in place of nativeItems. This object
+             * should probably encapsulate the information necessary to
+             * perform the next resumption of ListIdentifiers. It might even
+             * be possible to encode everything you need in the
+             * resumptionToken, in which case you won't need the
+             * resumptionResults Map. Here, I've done a silly combination
+             * of the two. Stateless resumptionTokens have some advantages.
+             *****************************************************************/
+            resumptionResults.put(resumptionId, nativeItems);
+            
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(oldCount + count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                                                                     nativeItems.length,
+                                                                     oldCount));
+            //          listIdentifiersMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSb.toString()));
+        }
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the specified metadata for the specified identifier
+     *
+     * @param identifier the OAI identifier
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return the <record/> portion of the XML response.
+     * @exception CannotDisseminateFormatException the metadataPrefix is not
+     * supported by the item.
+     * @exception IdDoesNotExistException the identifier wasn't found
+     */
+    public String getRecord(String identifier, String metadataPrefix)
+        throws CannotDisseminateFormatException,
+               IdDoesNotExistException {
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         * Replace this nativeItem assignment with your database API code.
+         **********************************************************************/
+        Object nativeItem = dummyDb[0];
+        if (nativeItem == null)
+            throw new IdDoesNotExistException(identifier);
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        return constructRecord(nativeItem, metadataPrefix);
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria. Note, though,
+     * that unlike the other OAI verb type methods implemented here, both of the
+     * listRecords methods are already implemented in AbstractCatalog rather than
+     * abstracted. This is because it is possible to implement ListRecords as a
+     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
+     * I suggest that you override both the AbstractCatalog.listRecords methods
+     * here since it will probably improve the performance if you create the
+     * response in one fell swoop rather than construct it one GetRecord at a time.
+     *
+     * @param from beginning date using the proper granularity
+     * @param until ending date using the proper granularity
+     * @param set the set name or null if no such limit is requested
+     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
+     * @return a Map object containing entries for a "records" Iterator object
+     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
+     * @exception CannotDisseminateFormatException the metadataPrefix isn't
+     * supported by the item.
+     */
+    public Map listRecords(String from, String until, String set, String metadataPrefix)
+        throws CannotDisseminateFormatException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         **********************************************************************/
+
+        /* Get some records from your database */
+        Object[] nativeItem = dummyDb;
+        int count;
+
+        /* load the records ArrayList */
+        for (count=0; count < maxListSize && count < nativeItem.length; ++count) {
+            String record = constructRecord(nativeItem[count], metadataPrefix);
+            records.add(record);
+        }
+
+        /* decide if you're done */
+        if (count < nativeItem.length) {
+            String resumptionId = getResumptionId();
+            
+            /*****************************************************************
+             * Store an object appropriate for your database API in the
+             * resumptionResults Map in place of nativeItems. This object
+             * should probably encapsulate the information necessary to
+             * perform the next resumption of ListIdentifiers. It might even
+             * be possible to encode everything you need in the
+             * resumptionToken, in which case you won't need the
+             * resumptionResults Map. Here, I've done a silly combination
+             * of the two. Stateless resumptionTokens have some advantages.
+             *****************************************************************/
+            resumptionResults.put(resumptionId, nativeItem);
+
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                                                                 nativeItem.length,
+                                                                 0));
+            //          listRecordsMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSbSb.toString()));
+        }
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing entries for "headers" and "identifiers" Iterators
+     * (both containing Strings) as well as an optional "resumptionMap" Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken argument
+     * is invalid or expired.
+     */
+    public Map listRecords(String resumptionToken)
+        throws BadResumptionTokenException {
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        purge(); // clean out old resumptionTokens
+        
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         **********************************************************************/
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+        
+        /* Get some more records from your database */
+        Object[] nativeItem = (Object[])resumptionResults.remove(resumptionId);
+        if (nativeItem == null) {
+            throw new BadResumptionTokenException();
+        }
+        int count;
+        
+        /* load the headers and identifiers ArrayLists. */
+        for (count = 0; count < maxListSize && count+oldCount < nativeItem.length; ++count) {
+            try {
+                String record = constructRecord(nativeItem[count+oldCount], metadataPrefix);
+                records.add(record);
+            } catch (CannotDisseminateFormatException e) {
+                /* the client hacked the resumptionToken beyond repair */
+                throw new BadResumptionTokenException();
+            }
+        }
+        
+        /* decide if you're done */
+        if (count+oldCount < nativeItem.length) {
+            resumptionId = getResumptionId();
+            
+            /*****************************************************************
+             * Store an object appropriate for your database API in the
+             * resumptionResults Map in place of nativeItems. This object
+             * should probably encapsulate the information necessary to
+             * perform the next resumption of ListIdentifiers. It might even
+             * be possible to encode everything you need in the
+             * resumptionToken, in which case you won't need the
+             * resumptionResults Map. Here, I've done a silly combination
+             * of the two. Stateless resumptionTokens have some advantages.
+             *****************************************************************/
+            resumptionResults.put(resumptionId, nativeItem);
+            
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(oldCount + count));
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(metadataPrefix);
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                                                                 nativeItem.length,
+                                                                 oldCount));
+            //          listRecordsMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSb.toString()));
+        }
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(Object nativeItem, String metadataPrefix)
+        throws CannotDisseminateFormatException {
+        String schemaURL = null;
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix);
+    }
+
+    /**
+     * Retrieve a list of sets that satisfy the specified criteria
+     *
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception OAIBadRequestException signals an http status code 400 problem
+     */
+    public Map listSets() {
+        purge(); // clean out old resumptionTokens
+        Map listSetsMap = new HashMap();
+        ArrayList sets = new ArrayList();
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         **********************************************************************/
+
+        /* decide which sets you're going to support */
+        String[] dbSets = dummySets;
+        int count;
+
+        /* load the sets ArrayList */
+        for (count=0; count < maxListSize && count < dbSets.length; ++count) {
+            sets.add(dbSets[count]);
+        }
+
+        /* decide if you're done */
+        if (count < dbSets.length) {
+            String resumptionId = getResumptionId();
+            
+            /*****************************************************************
+             * Store an object appropriate for your database API in the
+             * resumptionResults Map in place of nativeItems. This object
+             * should probably encapsulate the information necessary to
+             * perform the next resumption of ListIdentifiers. It might even
+             * be possible to encode everything you need in the
+             * resumptionToken, in which case you won't need the
+             * resumptionResults Map. Here, I've done a silly combination
+             * of the two. Stateless resumptionTokens have some advantages.
+             *****************************************************************/
+            resumptionResults.put(resumptionId, dbSets);
+
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(count));
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                                                              dbSets.length,
+                                                              0));
+            //          listSetsMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSbSb.toString()));
+        }
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        listSetsMap.put("sets", sets.iterator());
+        return listSetsMap;
+    }
+
+    /**
+     * Retrieve the next set of sets associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listSets() Map result.
+     * @return a Map object containing "sets" Iterator object (contains
+     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
+     * @exception BadResumptionTokenException the value of the resumptionToken
+     * is invalid or expired.
+     */
+    public Map listSets(String resumptionToken)
+        throws BadResumptionTokenException {
+        Map listSetsMap = new HashMap();
+        ArrayList sets = new ArrayList();
+        purge(); // clean out old resumptionTokens
+        
+        /**********************************************************************
+         * YOUR CODE GOES HERE
+         **********************************************************************/
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+        /* Get some more sets */
+        String[] dbSets = (String[])resumptionResults.remove(resumptionId);
+        if (dbSets == null) {
+            throw new BadResumptionTokenException();
+        }
+        int count;
+
+        /* load the sets ArrayList */
+        for (count = 0; count < maxListSize && count+oldCount < dbSets.length; ++count) {
+            sets.add(dbSets[count+oldCount]);
+        }
+
+        /* decide if we're done */
+        if (count+oldCount < dbSets.length) {
+            resumptionId = getResumptionId();
+            
+            /*****************************************************************
+             * Store an object appropriate for your database API in the
+             * resumptionResults Map in place of nativeItems. This object
+             * should probably encapsulate the information necessary to
+             * perform the next resumption of ListIdentifiers. It might even
+             * be possible to encode everything you need in the
+             * resumptionToken, in which case you won't need the
+             * resumptionResults Map. Here, I've done a silly combination
+             * of the two. Stateless resumptionTokens have some advantages.
+             *****************************************************************/
+            resumptionResults.put(resumptionId, dbSets);
+            
+            /*****************************************************************
+             * Construct the resumptionToken String however you see fit.
+             *****************************************************************/
+            StringBuffer resumptionTokenSb = new StringBuffer();
+            resumptionTokenSb.append(resumptionId);
+            resumptionTokenSb.append(":");
+            resumptionTokenSb.append(Integer.toString(oldCount + count));
+            
+            /*****************************************************************
+             * Use the following line if you wish to include the optional
+             * resumptionToken attributes in the response. Otherwise, use the
+             * line after it that I've commented out.
+             *****************************************************************/
+            listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+                                                              dbSets.length,
+                                                              oldCount));
+            //          listSetsMap.put("resumptionMap",
+            //                                 getResumptionMap(resumptionTokenSb.toString()));
+        }
+        /***********************************************************************
+         * END OF CUSTOM CODE SECTION
+         ***********************************************************************/
+        listSetsMap.put("sets", sets.iterator());
+        return listSetsMap;
+    }
+
+    /**
+     * close the repository
+     */
+    public void close() { }
+    
+    /**
+     * Purge tokens that are older than the configured time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date now = new Date();
+        Iterator keySet = resumptionResults.keySet().iterator();
+        while (keySet.hasNext()) {
+            String key = (String)keySet.next();
+            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            String key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+    
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a String version of the current time
+     */
+    private synchronized static String getResumptionId() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/edu/getty/oai/server/catalog/GettyFileRecordFactory.java b/src/edu/getty/oai/server/catalog/GettyFileRecordFactory.java
new file mode 100644
index 0000000..642f3c7
--- /dev/null
+++ b/src/edu/getty/oai/server/catalog/GettyFileRecordFactory.java
@@ -0,0 +1,164 @@
+
+/**
+*Copyright (c) 2000-2002 OCLC Online Computer Library Center,
+*Inc. and other contributors. All rights reserved.  The contents of this file, as updated
+*from time to time by the OCLC Office of Research, are subject to OCLC Research
+*Public License Version 2.0 (the "License"); you may not use this file except in
+*compliance with the License. You may obtain a current copy of the License at
+*http://www.apache.org/licenses/LICENSE-2.0.html.  Software distributed under the License is
+*distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
+*or implied. See the License for the specific language governing rights and limitations
+*under the License.  This software consists of voluntary contributions made by many
+*individuals on behalf of OCLC Research. For more information on OCLC Research,
+*please see http://www.oclc.org/oclc/research/.
+*
+*The Original Code is NewFileRecordFactory.java.
+*The Initial Developer of the Original Code is Jeff Young.
+*Portions created by Cristian Grunca 
+*    CDWALite support
+*    File name encoding
+*Copyright (C) 2005 J. Paul Getty. All Rights Reserved.
+*Contributor(s): Ted Dancescu, Steven Swimmer.
+*/
+
+package edu.getty.oai.server.catalog;
+
+import java.io.UnsupportedEncodingException;
+//import java.util.ArrayList;
+//import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+//import java.util.StringTokenizer;
+import ORG.oclc.oai.server.catalog.*;
+
+/**
+ * GettyFileRecordFactory converts native XML "items" to "record" Strings.
+ * This factory assumes the native XML item looks exactly like the <record>
+ * element of an OAI GetRecord response, with the possible exception that the
+ * <metadata> element contains multiple metadataFormats from which to choose.
+ */
+public class GettyFileRecordFactory extends RecordFactory {
+    private String repositoryIdentifier = null;
+    
+    /**
+     * Construct an GettyFileRecordFactory capable of producing the Crosswalk(s)
+     * specified in the properties file.
+     * @param properties Contains information to configure the factory:
+     *                   specifically, the names of the crosswalk(s) supported
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public GettyFileRecordFactory(Properties properties)
+	throws IllegalArgumentException {
+	super(properties);
+	repositoryIdentifier = properties.getProperty("GettyFileRecordFactory.repositoryIdentifier");
+	if (repositoryIdentifier == null) {
+	    throw new IllegalArgumentException("GettyFileRecordFactory.repositoryIdentifier is missing from the properties file");
+	}
+    }
+
+    /**
+     * Utility method to parse the 'local identifier' from the OAI identifier
+     *
+     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
+     * @return local identifier (e.g. ID/12345).
+     */
+    public String fromOAIIdentifier(String identifier) {
+        return identifier;
+    }
+
+    /**
+     * Construct an OAI identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return OAI identifier
+     */
+    public String getOAIIdentifier(Object nativeItem) {
+	StringBuffer sb = new StringBuffer();
+	sb.append(getLocalIdentifier(nativeItem));
+	return sb.toString();
+    }
+
+    /**
+     * Extract the local identifier from the native item
+     *
+     * @param nativeItem native Item object
+     * @return local identifier
+     */
+    public String getLocalIdentifier(Object nativeItem) {
+        return (String)((HashMap)nativeItem).get("localIdentifier");
+    }
+
+    /**
+     * get the datestamp from the item
+     *
+     * @param nativeItem a native item presumably containing a datestamp somewhere
+     * @return a String containing the datestamp for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public String getDatestamp(Object nativeItem)
+	throws IllegalArgumentException  {
+        return (String)((HashMap)nativeItem).get("lastModified");
+    }
+
+    /**
+     * get the setspec from the item
+     *
+     * @param nativeItem a native item presumably containing a setspec somewhere
+     * @return a String containing the setspec for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getSetSpecs(Object nativeItem)
+	throws IllegalArgumentException  {
+        return (Iterator)((HashMap)nativeItem).get("setSpecs");
+    }
+
+    /**
+     * Get the about elements from the item
+     *
+     * @param nativeItem a native item presumably containing about information somewhere
+     * @return a Iterator of Strings containing &lt;about&gt;s for the item
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
+	return null;
+    }
+
+    /**
+     * Is the record deleted?
+     *
+     * @param nativeItem a native item presumably containing a possible delete indicator
+     * @return true if record is deleted, false if not
+     * @exception IllegalArgumentException Something is wrong with the argument.
+     */
+    public boolean isDeleted(Object nativeItem)
+	throws IllegalArgumentException {
+	return false;
+    }
+
+    /**
+     * Allows classes that implement RecordFactory to override the default create() method.
+     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
+     * record. Return null if you want the default handler to create it by calling the methods
+     * above individually.
+     * 
+     * @param nativeItem the native record
+     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
+     * used.
+     */
+    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
+	// Don't perform quick creates
+        try {
+            String result = new String((byte[])((HashMap)nativeItem).get("recordBytes"), "UTF-8");
+            if (result.startsWith("<?")) {
+                int offset = result.indexOf("?>");
+                result = result.substring(offset+2);
+            }
+            return result;
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+            return null;
+        }
+// 	return null;
+    }
+}
diff --git a/src/edu/getty/oai/server/catalog/GettyFileSystemOAICatalog.java b/src/edu/getty/oai/server/catalog/GettyFileSystemOAICatalog.java
new file mode 100644
index 0000000..1dccff7
--- /dev/null
+++ b/src/edu/getty/oai/server/catalog/GettyFileSystemOAICatalog.java
@@ -0,0 +1,832 @@
+/*
+ * The contents of this file, as updated from time to time by the OCLC Office
+ * of Research, are subject to the OCLC Office of Research Public License
+ * Version 1.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a current copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.html.
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the License.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * on behalf of the OCLC Office of Research. For more information on the OCLC
+ * Office of Research, please see http://www.oclc.org/oclc/research/.
+ *
+ * This is Original Code.
+ *
+ * The Initial Developer(s) of the Original Code is (are):
+ *  - Ralph LeVan <levan@oclc.org>
+ * 
+ * Portions created by OCLC are Copyright (C) 2001.
+ *
+ * 2002-04-09 Created
+ */
+
+package edu.getty.oai.server.catalog;
+import ORG.oclc.oai.server.catalog.*;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.Vector;
+import ORG.oclc.oai.server.verb.BadResumptionTokenException;
+import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+import ORG.oclc.oai.server.verb.IdDoesNotExistException;
+import ORG.oclc.oai.server.verb.BadArgumentException;
+import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
+import ORG.oclc.oai.server.verb.NoSetHierarchyException;
+import ORG.oclc.oai.server.verb.NoItemsMatchException;
+
+
+
+/**
+ * GettyFileSystemOAICatalog is an implementation of AbstractCatalog interface
+ * with the data sitting in a directory on a filesystem.
+ *
+ * @author Cristian Grunca, J. Paul Getty Trust
+ */
+
+public class GettyFileSystemOAICatalog extends AbstractCatalog {
+    static final boolean debug=false;
+
+    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
+    protected String           homeDir;
+    private HashMap          fileDateMap = new HashMap();
+    private HashMap setMap = new HashMap();
+    private HashMap          resumptionResults=new HashMap();
+    private int              maxListSize;
+    private ArrayList sets = null;
+
+    public GettyFileSystemOAICatalog(Properties properties) throws IOException {
+        String          temp;
+
+        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        temp=properties.getProperty("GettyFileSystemOAICatalog.maxListSize");
+        if (temp==null)
+            throw new IllegalArgumentException("GettyFileSystemOAICatalog."+
+                "maxListSize is missing from the properties file");
+        maxListSize = Integer.parseInt(temp);
+        if(debug)
+            System.out.println("in GettyFileSystemOAICatalog(): maxListSize="+
+                maxListSize);
+
+        homeDir=properties.getProperty("GettyFileSystemOAICatalog.homeDir");
+        if (homeDir==null)
+            throw new IllegalArgumentException("GettyFileSystemOAICatalog."+
+                "homeDir is missing from the properties file");
+        if(debug)
+            System.out.println("in GettyFileSystemOAICatalog(): homeDir="+
+                homeDir);
+
+
+	File homeFile = new File(homeDir);
+	int homeDirLen = homeFile.getPath().length()+1;
+	loadFileMap(homeDirLen, homeFile);
+// 	Iterator iterator = fileDateMap.entrySet().iterator();
+// 	while (iterator.hasNext()) {
+// 	    Map.Entry entry = (Map.Entry)iterator.next();
+// 	    System.out.println(entry.getKey() + ":" + entry.getValue());
+// 	}
+	sets = getSets(properties);
+    }
+
+    private static ArrayList getSets(Properties properties) {
+        TreeMap treeMap = new TreeMap();
+	String propertyPrefix = "Sets.";
+	Enumeration propNames = properties.propertyNames();
+	while (propNames.hasMoreElements()) {
+	    String propertyName = (String)propNames.nextElement();
+	    if (propertyName.startsWith(propertyPrefix)) {
+                treeMap.put(propertyName, properties.get(propertyName));
+	    }
+	}
+ 	return new ArrayList(treeMap.values());
+    }
+    
+    private void loadFileMap(int homeDirLen, File currentDir)
+        throws IOException {
+        try {
+            String[] list = currentDir.list();
+            //DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            //DocumentBuilder builder = factory.newDocumentBuilder();
+            for (int i=0; i<list.length; ++i) {
+                File child = new File(currentDir, list[i]);
+                if (child.isDirectory() && !"CVS".equals(child.getName())) {
+                    loadFileMap(homeDirLen, child);
+                } else if (isMetadataFile(child)) {
+                    String path = file2path(homeDirLen, child);
+                    System.out.println("parsing : " + path);
+                    String datestamp = date2OAIDatestamp(new Date(child.lastModified()));
+
+					fileDateMap.put(path, datestamp);
+                                                        
+                    String setSpec = file2OAISetSpec(child);
+                    if(setSpec != null)
+                    {
+						ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
+						if (setSpecList == null) {
+							setSpecList = new ArrayList();
+							setMap.put(setSpec, setSpecList);
+						}
+						
+						setSpecList.add(path);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new IOException(e.getMessage());
+        }
+    }
+
+    /**
+     * Override this method if some files exist in the
+     * filesystem that aren't metadata records.
+     *
+     * @param child the File to be investigated
+     * @return true if it contains metadata, false otherwise
+     */
+    protected boolean isMetadataFile(File child) {
+        return true;
+    }
+
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param homeDirLen the length of the home directory path
+     * @param file the File object containing the native record
+     * @return localIdentifier
+     */
+    protected String file2path(int homeDirLen, File file) {
+	String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
+	return fileName;
+    }
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param file the File object containing the native record
+     * @return localIdentifier
+     */
+    protected String file2OAISetSpec(File file) {
+	String fileName = file.getName();
+	int lastIndex = fileName.lastIndexOf(".");
+	if(lastIndex < 0)
+		return null;
+	lastIndex = fileName.lastIndexOf(".", lastIndex - 1);	
+	if(lastIndex < 0)
+		return null;
+	int firstIndex = fileName.lastIndexOf(".", lastIndex - 1);
+	if(firstIndex < 0)
+		return null;	
+	String setSpec = fileName.substring(firstIndex+1,lastIndex);
+	
+	return setSpec;
+    }
+
+    /**
+     * Override this method if you don't like the default localIdentifiers.
+     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
+     * @return the File object containing the native record
+     */
+    protected File localIdentifier2File(String localIdentifier) {
+        String fileName = localIdentifier.replace('/', File.separatorChar);
+	return new File(homeDir, fileName);
+    }
+
+    private String date2OAIDatestamp(Date date) {
+         return dateFormatter.format(date);
+    }
+
+    private HashMap getNativeHeader(String path)
+        throws IOException {
+        HashMap recordMap = null;
+        if (fileDateMap.containsKey(path)) {
+            recordMap = new HashMap();
+            recordMap.put("localIdentifier", path.substring(0, path.lastIndexOf(".")));
+            recordMap.put("lastModified", fileDateMap.get(path));
+            ArrayList setSpecs = new ArrayList();
+            Iterator keySet = setMap.keySet().iterator();
+            while (keySet.hasNext()) {
+                String key = (String)keySet.next();
+                ArrayList identifierList = (ArrayList)setMap.get(key);
+                if (identifierList.contains(path)) {
+                    setSpecs.add(key);
+                }
+            }
+            recordMap.put("setSpecs", setSpecs.iterator());
+            return recordMap;
+        }
+        return recordMap;
+    }
+
+    private ArrayList getExtensionList(String localIdentifier) {
+//         System.out.println("getExtensionList: localIdentifier="
+//                            + localIdentifier);
+        ArrayList list = new ArrayList();
+        Iterator iterator = fileDateMap.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry entry = (Map.Entry)iterator.next();
+//             System.out.println("entry.getKey()=" + entry.getKey());
+            if (((String)entry.getKey()).startsWith(localIdentifier)) {
+                list.add(((String)entry.getKey()).substring(localIdentifier.length()+1));
+            }
+        }
+        return list;
+    }
+    
+    private HashMap getNativeRecord(String path)
+        throws IOException {
+        HashMap recordMap = getNativeHeader(path);
+        if (recordMap == null) {
+            return null;
+        } else {
+            File file = localIdentifier2File(path);
+            try {
+                FileInputStream fis = new FileInputStream(file);
+                BufferedInputStream bis = new BufferedInputStream(fis);
+                byte[] buffer = new byte[(int)file.length()];
+                bis.read(buffer, 0, (int)file.length());
+                recordMap.put("recordBytes", buffer);
+                bis.close();
+                fis.close();
+                return recordMap;
+            } catch (FileNotFoundException e) {
+                return null;
+            }
+        }
+    }
+    
+    /**
+     * Retrieve the specified metadata for the specified oaiIdentifier
+     *
+     * @param     oaiIdentifier the OAI identifier
+     * @param     metadataPrefix the OAI metadataPrefix
+     * @return    the Record object containing the result.
+     * @exception CannotDisseminateFormatException signals an http status
+     *                code 400 problem
+     * @exception IdDoesNotExistException signals an http status code 404
+     *                problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *                problem
+     */
+    public String getRecord(String oaiIdentifier, String metadataPrefix)
+        throws IdDoesNotExistException, CannotDisseminateFormatException,
+               OAIInternalServerError {
+        HashMap nativeItem = null;
+        try {
+	    String localIdentifier
+		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+
+            nativeItem = getNativeRecord(localIdentifier + "." + metadataPrefix);
+            if (nativeItem == null)
+                throw new IdDoesNotExistException(oaiIdentifier);
+            return constructRecord(nativeItem, metadataPrefix);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError("Database Failure");
+        }
+    }
+
+
+    /**
+     * Retrieve a list of schemaLocation values associated with the specified
+     * oaiIdentifier.
+     *
+     * We get passed the ID for a record and are supposed to return a list
+     * of the formats that we can deliver the record in.  Since we are assuming
+     * that all the records in the directory have the same format, the
+     * response to this is static;
+     *
+     * @param oaiIdentifier the OAI identifier
+     * @return a Vector containing schemaLocation Strings
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAINotFoundException signals an http status code 404 problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Vector getSchemaLocations(String oaiIdentifier)
+      throws IdDoesNotExistException, OAIInternalServerError, NoMetadataFormatsException {
+        ArrayList extensionList = null;
+         try {
+	    String localIdentifier
+		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
+            extensionList = getExtensionList(localIdentifier);
+         } catch (Exception e) {
+             e.printStackTrace();
+             throw new OAIInternalServerError("Database Failure");
+         }
+        
+        if (extensionList != null) {
+            return getRecordFactory().getSchemaLocations(extensionList);
+        } else {
+            throw new IdDoesNotExistException(oaiIdentifier);
+        }
+    }
+
+
+    /**
+     * Retrieve a list of Identifiers that satisfy the criteria parameters
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
+        throws BadArgumentException, CannotDisseminateFormatException, OAIInternalServerError,
+               NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+	Iterator iterator = fileDateMap.entrySet().iterator();
+	int numRows = fileDateMap.entrySet().size();
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            String path = (String)entryDateMap.getKey();
+            if (fileDate.compareTo(from) >= 0
+                && fileDate.compareTo(until) <= 0
+                && (set == null || setIdentifiers != null && setIdentifiers.contains(path))) {
+                try {
+                    HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
+                    String[] header = getRecordFactory().createHeader(nativeHeader);
+                    headers.add(header[0]);
+                    identifiers.add(header[1]);
+                    count++;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            }
+	}
+
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listIdentifiersMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listIdentifiersMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+    /**
+     * Retrieve the next set of Identifiers associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listIdentifiers() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
+     * identifier keys with corresponding values of "true" or null depending on
+     * whether the identifier is deleted or not.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listIdentifiers(String resumptionToken)
+      throws BadResumptionTokenException, OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listIdentifiersMap = new HashMap();
+        ArrayList headers = new ArrayList();
+        ArrayList identifiers = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        String set;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+            if(set.equals("null")) set = null;            
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("GettyFileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
+	    iterator = fileDateMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the headers and identifiers ArrayLists. */
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String path = (String)entryDateMap.getKey();
+            if (set == null || setIdentifiers != null && setIdentifiers.contains(path)) {
+                try {
+                    HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
+                    String[] header = getRecordFactory().createHeader(nativeHeader);
+                    headers.add(header[0]);
+                    identifiers.add(header[1]);
+                    count++;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            }
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listIdentifiersMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listIdentifiersMap.put("headers", headers.iterator());
+        listIdentifiersMap.put("identifiers", identifiers.iterator());
+        return listIdentifiersMap;
+    }
+
+
+    /**
+     * Utility method to construct a Record object for a specified
+     * metadataFormat from a native record
+     *
+     * @param nativeItem native item from the dataase
+     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
+     * @return the <record/> String
+     * @exception CannotDisseminateFormatException the record is not available
+     * for the specified metadataPrefix.
+     */
+    private String constructRecord(HashMap nativeItem, String metadataPrefix)
+        throws CannotDisseminateFormatException, OAIInternalServerError {
+        String schemaURL = null;
+	Iterator setSpecs = getSetSpecs(nativeItem);
+	Iterator abouts = getAbouts(nativeItem);
+
+        if (metadataPrefix != null) {
+            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
+                throw new CannotDisseminateFormatException(metadataPrefix);
+        }
+        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
+    }
+
+    /**
+     * get an Iterator containing the setSpecs for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of setSpec values for this nativeItem
+     */
+    private Iterator getSetSpecs(HashMap nativeItem)
+	throws OAIInternalServerError {
+        return null;
+    }
+
+    /**
+     * get an Iterator containing the abouts for the nativeItem
+     *
+     * @param rs ResultSet containing the nativeItem
+     * @return an Iterator containing the list of about values for this nativeItem
+     */
+    private Iterator getAbouts(HashMap nativeItem)
+	throws OAIInternalServerError {
+        return null;
+    }
+
+    /**
+     * Retrieve a list of records that satisfy the specified criteria
+     *
+     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
+     * date is desired
+     * @param until ending date in the form of YYYY-MM-DD or null if latest
+     * date is desired
+     * @param set set name or null if no set is desired
+     * @param metadataPrefix the OAI metadataPrefix
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listRecords(String from, String until, String set,
+                                    String metadataPrefix)
+      throws BadArgumentException, CannotDisseminateFormatException,
+      OAIInternalServerError, NoItemsMatchException {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+	Iterator iterator = fileDateMap.entrySet().iterator();
+	int numRows = fileDateMap.entrySet().size();
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String fileDate = (String)entryDateMap.getValue();
+            String path = (String)entryDateMap.getKey();
+            String extension = path.substring(path.lastIndexOf(".")+1);
+            if (fileDate.compareTo(from) >= 0
+                && fileDate.compareTo(until) <= 0
+                && extension.equals(metadataPrefix)
+                && (set == null || setIdentifiers != null && setIdentifiers.contains(path))) {
+                try {
+                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                    count++;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new OAIInternalServerError(e.getMessage());
+                }
+            }
+	}
+        
+        if (count == 0)
+            throw new NoItemsMatchException();
+        
+	/* decide if you're done */
+	if (iterator.hasNext()) {
+	    String resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(count));
+	    resumptionTokenSb.append(":");
+ 	    resumptionTokenSb.append(Integer.toString(numRows));
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+ 	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+ 	    listRecordsMap.put("resumptionMap",
+ 				   getResumptionMap(resumptionTokenSb.toString(),
+ 						    numRows,
+ 						    0));
+// 	    listRecordsMap.put("resumptionMap",
+// 				   getResumptionMap(resumptionTokenSb.toString()));
+	}
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    /**
+     * Retrieve the next set of records associated with the resumptionToken
+     *
+     * @param resumptionToken implementation-dependent format taken from the
+     * previous listRecords() Map result.
+     * @return a Map object containing an optional "resumptionToken" key/value
+     * pair and a "records" Iterator object. The "records" Iterator contains a
+     * set of Records objects.
+     * @exception OAIBadRequestException signals an http status code 400
+     *            problem
+     * @exception OAIInternalServerError signals an http status code 500
+     *            problem
+     */
+    public Map listRecords(String resumptionToken)
+      throws BadResumptionTokenException,
+      OAIInternalServerError {
+        purge(); // clean out old resumptionTokens
+        Map listRecordsMap = new HashMap();
+        ArrayList records = new ArrayList();
+        
+        /**********************************************************************
+         * parse your resumptionToken and look it up in the resumptionResults,
+         * if necessary
+         **********************************************************************/
+        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
+        String resumptionId;
+        int oldCount;
+        String metadataPrefix;
+	int numRows;
+        String set;
+        try {
+            resumptionId = tokenizer.nextToken();
+            oldCount = Integer.parseInt(tokenizer.nextToken());
+	    numRows = Integer.parseInt(tokenizer.nextToken());
+            metadataPrefix = tokenizer.nextToken();
+            set = tokenizer.nextToken();
+            if(set.equals("null")) set = null;
+        } catch (NoSuchElementException e) {
+            throw new BadResumptionTokenException();
+        }
+
+	/* Get some more records from your database */
+	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
+	if (iterator == null) {
+	    System.out.println("GettyFileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
+	    iterator = fileDateMap.entrySet().iterator();
+	    for (int i = 0; i<oldCount; ++i)
+		iterator.next();
+	}
+        
+	/* load the records ArrayLists. */
+	int count = 0;
+        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
+	while (count < maxListSize && iterator.hasNext()) {
+	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
+            String path = (String)entryDateMap.getKey();
+            if (set == null || setIdentifiers != null && setIdentifiers.contains(path)) {
+                try {
+                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
+                    String record = constructRecord(nativeItem, metadataPrefix);
+                    records.add(record);
+                    count++;
+                } catch (CannotDisseminateFormatException e) {
+                    /* the client hacked the resumptionToken beyond repair */
+                    throw new BadResumptionTokenException();
+                } catch (IOException e) {
+                    /* the file is probably missing */
+                    throw new BadResumptionTokenException();
+                }
+            }
+	}
+        
+	/* decide if you're done. */
+	if (iterator.hasNext()) {
+	    resumptionId = getRSName();
+	    resumptionResults.put(resumptionId, iterator);
+            
+	    /*****************************************************************
+	     * Construct the resumptionToken String however you see fit.
+	     *****************************************************************/
+	    StringBuffer resumptionTokenSb = new StringBuffer();
+	    resumptionTokenSb.append(resumptionId);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(oldCount + count));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(Integer.toString(numRows));
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(metadataPrefix);
+	    resumptionTokenSb.append(":");
+	    resumptionTokenSb.append(set);
+            
+	    /*****************************************************************
+	     * Use the following line if you wish to include the optional
+	     * resumptionToken attributes in the response. Otherwise, use the
+	     * line after it that I've commented out.
+	     *****************************************************************/
+	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
+								     numRows,
+								     oldCount));
+	    //          listRecordsMap.put("resumptionMap",
+	    //                                 getResumptionMap(resumptionTokenSb.toString()));
+	}
+
+        listRecordsMap.put("records", records.iterator());
+        return listRecordsMap;
+    }
+
+
+    public Map listSets() throws OAIInternalServerError,
+				 NoSetHierarchyException {
+        if (sets.size() == 0)
+            throw new NoSetHierarchyException();
+         Map listSetsMap = new LinkedHashMap();
+         listSetsMap.put("sets", sets.iterator());
+         return listSetsMap;
+    }
+
+
+    public Map listSets(String resumptionToken)
+      throws BadResumptionTokenException, OAIInternalServerError {
+	throw new BadResumptionTokenException();
+    }
+
+
+    /**
+     * close the repository
+     */
+    public void close() { }
+
+
+    /**
+     * Purge tokens that are older than the time-to-live.
+     */
+    private void purge() {
+        ArrayList old = new ArrayList();
+        Date      then, now = new Date();
+        Iterator  keySet = resumptionResults.keySet().iterator();
+        String    key;
+
+        while (keySet.hasNext()) {
+            key=(String)keySet.next();
+            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
+            if (now.after(then)) {
+                old.add(key);
+            }
+        }
+        Iterator iterator = old.iterator();
+        while (iterator.hasNext()) {
+            key = (String)iterator.next();
+            resumptionResults.remove(key);
+        }
+    }
+
+
+    /**
+     * Use the current date as the basis for the resumptiontoken
+     *
+     * @return a long integer version of the current time
+     */
+    private synchronized static String getRSName() {
+        Date now = new Date();
+        return Long.toString(now.getTime());
+    }
+}
diff --git a/src/gov/lanl/myProject/oaicat/crosswalk/XSLToai_wdCrosswalk.java b/src/gov/lanl/myProject/oaicat/crosswalk/XSLToai_wdCrosswalk.java
new file mode 100644
index 0000000..8a5ec3a
--- /dev/null
+++ b/src/gov/lanl/myProject/oaicat/crosswalk/XSLToai_wdCrosswalk.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
+ * agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package gov.lanl.myProject.oaicat.crosswalk;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import ORG.oclc.oai.server.crosswalk.XSLTCrosswalk;
+import ORG.oclc.oai.server.verb.OAIInternalServerError;
+
+/**
+ * Convert native "item" to pro. In this case, the native "item"
+ * is assumed to already be formatted as an OAI <record> element,
+ * with the possible exception that multiple metadataFormats may
+ * be present in the <metadata> element. The "crosswalk", merely
+ * involves pulling out the one that is requested.
+ */
+public class XSLToai_wdCrosswalk extends XSLTCrosswalk {
+    /**
+     * The constructor assigns the schemaLocation associated with this crosswalk. Since
+     * the crosswalk is trivial in this case, no properties are utilized.
+     *
+     * @param properties properties that are needed to configure the crosswalk.
+     */
+    public XSLToai_wdCrosswalk(Properties properties)
+    throws OAIInternalServerError {
+        super(properties, "http://www.lanl.gov/nisac/schemas/watchdog/ http://www.lanl.gov/nisac/schemas/oai_wd.xsd", null);
+        try {
+            String xsltName = properties.getProperty("XSLToai_wdCrosswalk.xsltName");
+            TransformerFactory tFactory = TransformerFactory.newInstance();
+            if (xsltName != null) {
+                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
+                this.transformer = tFactory.newTransformer(xslSource);
+            } else {
+                this.transformer = tFactory.newTransformer();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new OAIInternalServerError(e.getMessage());
+        }
+    }
+}

Fixed code: <?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="src" path="src"/>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
	<classpathentry kind="lib" path="lib/servlet-api.jar"/>
	<classpathentry kind="lib" path="lib/commons-logging.jar"/>
	<classpathentry kind="lib" path="lib/xalan.jar"/>
	<classpathentry kind="output" path="bin"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
	<name>OAICat</name>
	<comment></comment>
	<projects>
	</projects>
	<buildSpec>
		<buildCommand>
			<name>org.eclipse.jdt.core.javabuilder</name>
			<arguments>
			</arguments>
		</buildCommand>
	</buildSpec>
	<natures>
		<nature>org.eclipse.jdt.core.javanature</nature>
	</natures>
</projectDescription>
#app.version=1.5.20
#catalina.home=/servers/tomcat/apache-tomcat-7.0.2
#catalina.home=/home/jyoung/tomcat/apache-tomcat-7.0.2
#catalina.home=E:/Program Files/Apache Software Foundation/Tomcat 5.5
#junit.jar=/home/jyoung/lib/junit.jar
#log4j.jar=/home/jyoung/lib/log4j.jar
#xalan.jar=lib/xalan.jar
#commons-logging.jar=lib/commons-logging.jar
#oaicatnewton.jar=/home/jyoung/proj/cvs/oai/oaicatnewton/dist/oaicatnewton.jar
<!-- A "project" describes a set of targets that may be requested
     when Ant is executed.  The "default" attribute defines the
     target which is executed if no specific target is requested,
     and the "basedir" attribute defines the current working directory
     from which Ant executes the requested task.  This is normally
     set to the current working directory.
-->


<project name="oaicatjar" default="dist" basedir=".">



<!-- ===================== Property Definitions =========================== -->

<!--

  Each of the following properties are used in the build script.
  Values for these properties are set by the first place they are
  defined, from the following list:
  * Definitions on the "ant" command line (ant -Dcatalina.home=xyz compile)
  * Definitions from a "build.properties" file in the top level
    source directory
  * Definitions from a "build.properties" file in the developer's
    home directory
  * Default definitions in this build.xml file

  You will note below that property values can be composed based on the
  contents of previously defined properties.  This is a powerful technique
  that helps you minimize the number of changes required when your development
  environment is modified.  Note that property composition is allowed within
  "build.properties" files as well as in the "build.xml" script.

-->

  <property file="build.properties"/>
  <property file="${user.home}/build.properties"/>


<!-- ==================== File and Directory Names ======================== -->

<!--

  These properties generally define file and directory names (or paths) that
  affect where the build process stores its outputs.

  app.name             Base name of this application, used to
                       construct filenames and directories.
                       Defaults to "myapp".

  appp.version          Version identifier for this application.

  build.home           The directory into which the "prepare" and
                       "compile" targets will generate their output.
                       Defaults to "build".

  catalina.home        The directory in which you have installed
                       a binary distribution of Tomcat 4.  This will
                       be used by the "deploy" target.

  dist.home            The name of the base directory in which
                       distribution files are created.
                       Defaults to "dist".

  distv.home           The name of the distributiondirectory in which
                       distribution files are created for a version.
                       Defaults to "app.name-appp.version".

-->

  <property name="app.name"      value="oaicat"/>
  <property name="appp.version"   value="1.5.62"/>
  <property name="build.home"    value="build"/>
  <property name="catalina.home" value="../../../.."/> <!-- UPDATE THIS! -->
  <property name="dist.home"     value="dist"/>
  <property name="distv.home"    value="${app.name}-${appp.version}"/>
  <property name="test.dir"      location="${build.home}/test" />
  <property name="test.data.dir" location="${test.dir}/data" />
  <property name="test.reports.dir" location="${test.dir}/reports" />

<!--  ==================== Compilation Control Options ==================== -->

<!--

  These properties control option settings on the Javac compiler when it
  is invoked using the <javac> task.

  compile.debug        Should compilation include the debug option?

  compile.deprecation  Should compilation include the deprecation option?

  compile.optimize     Should compilation include the optimize option?

-->

  <property name="compile.debug"       value="true"/>
  <property name="compile.deprecation" value="true"/>
  <property name="compile.optimize"    value="true"/>



<!-- ==================== External Dependencies =========================== -->


<!--

  Use property values to define the locations of external JAR files on which
  your application will depend.  In general, these values will be used for
  two purposes:
  * Inclusion on the classpath that is passed to the Javac compiler

  Because we will automatically include all of the Java classes that Tomcat 4
  exposes to web applications, we will not need to explicitly list any of those
  dependencies.  You only need to worry about external dependencies for JAR
  files that you are going to include inside your "/WEB-INF/lib" directory.

-->

<!-- external dependency -->
  <!--
  <property name="foo.jar"
           value="/home/me/foo.jar"/>
  -->


<!-- ==================== Compilation Classpath =========================== -->

<!--

  Rather than relying on the CLASSPATH environment variable, Ant includes
  features that makes it easy to dynamically construct the classpath you
  need for each compilation.  The example below constructs the compile
  classpath to include the servlet.jar file, as well as the other components
  that Tomcat makes available to web applications automatically, plus anything
  that you explicitly added.

-->

  <path id="compile.classpath">

    <!-- Include all JAR files that will be included in /WEB-INF/lib -->
    <!-- *** CUSTOMIZE HERE AS REQUIRED BY YOUR APPLICATION *** -->

    <pathelement location="${junit.jar}"/>
<!--
    <pathelement location="${srw.jar}" />
-->
    <pathelement location="${commons-logging.jar}"/>
    <pathelement location="${xalan.jar}"/>

    <!-- Include all elements that Tomcat exposes to applications -->
    <pathelement location="${catalina.home}/common/classes"/>
    <fileset dir="${catalina.home}/lib">
      <include name="*.jar"/>
    </fileset>
    <pathelement location="${catalina.home}/classes"/>
    <fileset dir="${catalina.home}/lib">
      <include name="*.jar"/>
    </fileset>

  </path>



<!-- ==================== All Target ====================================== -->

<!--

  The "all" target is a shortcut for running the "clean" target followed
  by the "compile" target, to force a complete recompile.

-->

  <target name="all" depends="clean,dist"
   description="Clean build and dist, then compile and test"/>



<!-- ==================== Clean Target ==================================== -->

<!--

  The "clean" target deletes any previous "build" and "dist" directory,
  so that you can be ensured the application can be built from scratch.

-->

  <target name="clean"
   description="Delete old build and dist directories">
    <delete dir="${build.home}"/>
    <delete dir="${dist.home}"/>
    <delete dir="${distv.home}"/>
  </target>



<!-- ==================== Compile Target ================================== -->
  <target name="JUNIT">
    <available property="junit.present" classname="junit.framework.TestCase"
               classpathref="compile.classpath" />
  </target>

<!--

  The "compile" target transforms source files (from your "src" directory)
  into object files in the appropriate location in the build directory.
  This example assumes that you will be including your classes as a jar file.

-->

  <target name="compile" depends="prepare"
   description="Compile Java sources">

    <!-- Compile Java classes as necessary -->
    <javac srcdir="src"
          destdir="${build.home}"
           debug="${compile.debug}"
           includeAntRuntime="false"
     deprecation="${compile.deprecation}"
        optimize="${compile.optimize}">
        <classpath refid="compile.classpath"/>
    </javac>

    <!-- Copy associated resource files -->
    <copy  todir="${build.home}">
    <fileset dir="src" includes="**/*.properties"/>
    </copy>

  </target>



<!-- ==================== Jar Target ===================================== -->


<!--

  The "dist" target creates a binary distribution of your application
  in a directory structure ready to be archived in a tar.gz or zip file.
  Note that this target depends on two others:
  * "javadoc" so that the application Javadocs will have been created

-->

  <target name="jar" depends="compile"
   description="Create binary distribution">

    <mkdir dir="${build.home}/lib" />
    <!-- Create application JAR file -->
    <jar jarfile="${build.home}/lib/${app.name}.jar"
         basedir="${build.home}"
         includes="ORG/** com/** edu/**">
      <manifest>
        <attribute name="Specification-Title" value="OAI-PMH"/>
        <attribute name="Specification-Version" value="2.0"/>
        <attribute name="Specification-Vendor" value="Open Archives Initiative"/>
        <attribute name="Implementation-Title" value="OAICat"/>
        <attribute name="Implementation-Version" value="${appp.version} ${TODAY}"/>
        <attribute name="Implementation-Vendor" value="OCLC, Online Computer Library Center"/>
        <attribute name="Implementation-URL" value="http://www.oclc.org/research/software/oai/cat.shtm"/>
      </manifest>
    </jar>

  </target>

<!-- ==================== Dist Target ===================================== -->


<!--

  The "dist" target creates a binary distribution of your application
  in a directory structure ready to be archived in a tar.gz or zip file.
  Note that this target depends on two others:
  * "javadoc" so that the application Javadocs will have been created

-->

  <target name="dist" depends="javadoc"
   description="Create binary distribution">

    <!-- Copy documentation subdirectory -->
    <copy    todir="${dist.home}/docs">
      <fileset dir="${distv.home}/docs"/>
    </copy>

    <!-- Copy stuff into dist-version -->
    <copy file="${build.home}/lib/${app.name}.jar" todir="${distv.home}" />
    <copy file="${build.home}/lib/${app.name}.jar" todir="${dist.home}" />
<!--
    <copy file="${oaicatnewton.jar}" todir="${distv.home}" />
    <copy file="${oaicatnewton.jar}" todir="${dist.home}" />
-->
    <copy    todir="${distv.home}">
      <fileset dir="docs"/>
    </copy>
    <copy    todir="${dist.home}">
      <fileset dir="docs"/>
    </copy>
    <copy    todir="${distv.home}/src">
      <fileset dir="src" excludes="test/**,**/*.BAK"/>
    </copy>

    <!-- Create dist TAR file -->
    <delete file="${distv.home}.tar.gz" />
    <tar tarfile="${distv.home}.tar.gz"
         includes="${distv.home}/**"
         compression="gzip"
         basedir="."/>
    <tar tarfile="src${distv.home}.tar.gz"
         includes="src/**"
         compression="gzip"
         basedir="."/>

    <delete file="${distv.home}.zip" />
    <zip destfile="${distv.home}.zip"
         includes="${distv.home}/**"
         basedir="."/>
    <zip destfile="src${distv.home}.zip"
         includes="src/**"
         basedir="."/>

    <copy    todir="${dist.home}">
      <fileset dir="." includes="*.tar.gz *.zip"/>
    </copy>

    <!-- Copy additional files to ${dist.home} as necessary -->

  </target>



<!-- ==================== Test Target ===================================== -->


<!--

  The "test" target does unit testing using JUnit.
-->

<!--
  <target name="test" depends="jar"
   description="Test the application">

    <mkdir dir="${test.data.dir}" />
    <mkdir dir="${test.reports.dir}" />
    <junit printsummary="withOutAndErr" fork="yes"
           errorProperty="test.failed"
           failureProperty="test.failed">
        <sysproperty key="app.version" value="${appp.version}" />
        <formatter type="brief" usefile="false"/>
        <formatter type="xml" />
        <batchtest todir="${test.data.dir}">
          <fileset dir="${build.home}" includes="test/**/*Test.class" />
        </batchtest>
        <classpath>
          <pathelement location="${junit.jar}" />
          <pathelement location="${xalan.jar}" />
          <pathelement location="${distv.home}/${app.name}.jar" />
          <pathelement location="${build.home}" />
          <pathelement location="${catalina.home}/common/classes"/>
          <fileset dir="${catalina.home}/common/lib">
            <include name="*.jar"/>
          </fileset>
          <pathelement location="${catalina.home}/classes"/>
          <fileset dir="${catalina.home}/common/lib">
            <include name="*.jar"/>
          </fileset>
        </classpath>
    </junit>

    <junitreport todir="${test.data.dir}">
      <fileset dir="${test.data.dir}">
        <include name="TEST-*.xml" />
      </fileset>
      <report format="frames" todir="${test.reports.dir}" />
    </junitreport>


    <fail message="Tests failed. Check log and/or reports." if="test.failed" />
  </target>
-->


<!-- ==================== Javadoc Target ================================== -->

<!--

  The "javadoc" target creates Javadoc API documentation for the Java
  classes included in your application.  Normally, this is only required
  when preparing a distribution release, but is available as a separate
  target in case the developer wants to create Javadocs independently.

-->

  <target name="javadoc" depends="jar"
   description="Create Javadoc API documentation">

    <mkdir          dir="${distv.home}/docs/api"/>
    <javadoc sourcepath="src"
                destdir="${distv.home}/docs/api"
           packagenames="ORG.oclc.*,edu.*">
      <classpath refid="compile.classpath"/>
    </javadoc>

  </target>



<!-- ==================== Prepare Target ================================== -->

<!--

  The "prepare" target is used to create the "build" destination directory,
  and copy the static contents of your web application to it.  If you need
  to copy static files from external dependencies, you can customize the
  contents of this task.

  Normally, this task is executed indirectly when needed.

-->

  <target name="prepare">

    <!-- Create build directory and copy static content -->
    <mkdir  dir="${build.home}"/>
    <tstamp/>
  </target>

</project>
To upgrade OAICat with the latest code changes, copy the latest
oaicat.jar file to webapps/oaicat/WEB-INF/lib/.

Before customizing OAICat, first install oaicat.war in a J2EE Servlet
Engine and verify that the default configuration works. If so, proceed
with any necessary code and configuration changes as described below.

Before building this probject with Ant, create a 'build.properties'
file in the project directory with the following entries:

catalina.home=/path/to/jakarta-tomcat

To create a new distribution set, issue the command:

ant dist

To customize OAICat, answer these questions:

Q1: What Java package should I use to hold my custom classes?
    a) For example, if you work for Acme Inc., create a directory
       hierarchy somewhere named:

       com/acme/oai

Q2: What database engine will I use?
    a) For example, if using the Foo database, copy
       oaicatjar/src/ORG/oclc/oai/server/catalog/DummyOAICatalog.java
       to com/acme/oai/server/catalog/FooOAICatalog.java and modify the
       code so the class name matches the new filename.
    b) Change the code in this class to use the Foo database Java API.
       In general, all this class needs to know about the records
       is that they are black-box Java Objects. To make life easier
       downstream, however, it may be worthwhile to convert the records
       to a more convenient processing form immediately after reading.
       For example, if the records are stored as XML Strings, load
       them into DOM objects as soon as they are read. Beyond that,
       though, leave it to the Crosswalk and RecordFactory
       implementations to understand the true semantics of the records.
       Doing this may mean you can't reuse this class for cases where
       the database returns non-XML byte arrays, but then again, what
       are the chances of that?
    c) Make a corresponding package/class name change to the
       AbstractCatalog.oaiCatalogClassName entry in the
       webapps/oaicat/WEB-INF/oaicat.properties file to have OAICat use
       your custom class.

Q3: What are the semantics of these record objects?
    a) If FooOAICatalog returns records as byte arrays, examples can be
       anything such as MARC Communications Format. If FooOAICatalog
       returns Strings, examples might include MARC BER, or any kind of
       XML String. If FooOAICatalog returns DOM Documents, examples can
       be any XML-based metadata format. Let's assume FooOAICatalog
       returns records as DOM Documents containing MARCXML content.
    b) Copy oaicatjar/src/ORG/oclc/oai/server/catalog/XMLRecordFactory.java
       to com/acme/oai/server/catalog/MARCXMLDOMRecordFactory.java
       and modify the code so the class name matches the new filename.
    c) Change the methods to cast each Object nativeItem parameter to a
       org.w3c.dom.Document and use it to extract the relevant data for
       each method.
    d) Make a corresponding package/class name change to the
       AbstractCatalog.recordFactoryClassName entry in the
       webapps/oaicat/WEB-INF/oaicat.properties file to have OAICat use
       your custom class.

Q4: What OAI metadatdaFormats will be supported?
    a) Examples include oai_dc, marcxml, or oai_etdms.
    b) For oai_dc, copy oaicatjar/src/ORG/oclc/oai/crosswalk/XML2oai_dc.java
       to com/acme/oai/server/catalog/MARCXMLDOM2oai_dc.java and modify
       the code so the class name matches the new filename.
    c) Change the constructor to use the appropriate schemaLocation for
       this metadataFormat.
    d) Change the methods to cast each Object nativeItem parameter to a
       org.w3c.dom.Document and use it to service the method accordingly.
       In this case, you could use the Library of Congress MARCXML to DC
       XSL stylesheet (see http://www.loc.gov/standards/marcxml/) to
       perform the crosswalk to Dublin Core.
    e) Repeat steps b, c, and d for each metadatdaFormat to be supported.
    f) Make a corresponding package/class name change to the
       Crosswalks.* entries in the webapps/oaicat/WEB-INF/oaicat.properties
       file to have OAICat use your custom classes.

Finally, change other properties in oaicat.properties according to your
preferences.

That's essentially what it takes to customize OAICat. Contact Jeff Young
at jyoung@oclc.org with questions and comments.
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
  <head>
    <title>OAICat Distribution Files</title>
  </head>

  <body>
    <h1>OAICat Distribution Files</h1>
    <ul>
      <li><a href="oaicat-1.5.62.tar.gz">oaicat-1.5.62.tar.gz</a> Latest version</li>
      <li><a href="oaicat-1.5.62.zip">oaicat-1.5.62.zip</a> Latest version</li>

      <li><a href="oaicat-1.5.61.tar.gz">oaicat-1.5.61.tar.gz</a></li>
      <li><a href="oaicat-1.5.61.zip">oaicat-1.5.61.zip</a></li>

      <li><a href="oaicat-1.5.60.tar.gz">oaicat-1.5.60.tar.gz</a></li>
      <li><a href="oaicat-1.5.60.zip">oaicat-1.5.60.zip</a></li>

      <li><a href="oaicat-1.5.59.tar.gz">oaicat-1.5.59.tar.gz</a></li>
      <li><a href="oaicat-1.5.59.zip">oaicat-1.5.59.zip</a></li>

      <li><a href="oaicat-1.5.58.tar.gz">oaicat-1.5.58.tar.gz</a></li>
      <li><a href="oaicat-1.5.58.zip">oaicat-1.5.58.zip</a></li>

      <li><a href="oaicat-1.5.57.tar.gz">oaicat-1.5.57.tar.gz</a></li>
      <li><a href="oaicat-1.5.57.zip">oaicat-1.5.57.zip</a></li>

      <li><a href="oaicat-1.5.56.tar.gz">oaicat-1.5.56.tar.gz</a></li>
      <li><a href="oaicat-1.5.56.zip">oaicat-1.5.56.zip</a></li>

      <li><a href="oaicat-1.5.55.tar.gz">oaicat-1.5.55.tar.gz</a></li>
      <li><a href="oaicat-1.5.55.zip">oaicat-1.5.55.zip</a></li>

      <li><a href="oaicat-1.5.54.tar.gz">oaicat-1.5.54.tar.gz</a></li>
      <li><a href="oaicat-1.5.54.zip">oaicat-1.5.54.zip</a></li>

      <li><a href="oaicat-1.5.53.tar.gz">oaicat-1.5.53.tar.gz</a></li>
      <li><a href="oaicat-1.5.53.zip">oaicat-1.5.53.zip</a></li>

      <li><a href="oaicat-1.5.52.tar.gz">oaicat-1.5.52.tar.gz</a></li>
      <li><a href="oaicat-1.5.52.zip">oaicat-1.5.52.zip</a></li>

      <li><a href="oaicat-1.5.51.tar.gz">oaicat-1.5.51.tar.gz</a></li>
      <li><a href="oaicat-1.5.51.zip">oaicat-1.5.51.zip</a></li>

      <li><a href="oaicat-1.5.50.tar.gz">oaicat-1.5.50.tar.gz</a></li>
      <li><a href="oaicat-1.5.50.zip">oaicat-1.5.50.zip</a></li>

      <li><a href="oaicat-1.5.49.tar.gz">oaicat-1.5.49.tar.gz</a></li>
      <li><a href="oaicat-1.5.49.zip">oaicat-1.5.49.zip</a></li>

      <li><a href="oaicat-1.5.48.tar.gz">oaicat-1.5.48.tar.gz</a></li>
      <li><a href="oaicat-1.5.48.zip">oaicat-1.5.48.zip</a></li>

      <li><a href="oaicat-1.5.47.tar.gz">oaicat-1.5.47.tar.gz</a></li>
      <li><a href="oaicat-1.5.47.zip">oaicat-1.5.47.zip</a></li>

      <li><a href="oaicat-1.5.46.tar.gz">oaicat-1.5.46.tar.gz</a></li>
      <li><a href="oaicat-1.5.46.zip">oaicat-1.5.46.zip</a></li>

      <li><a href="oaicat-1.5.45.tar.gz">oaicat-1.5.45.tar.gz</a></li>
      <li><a href="oaicat-1.5.45.zip">oaicat-1.5.45.zip</a></li>

      <li><a href="oaicat-1.5.44.tar.gz">oaicat-1.5.44.tar.gz</a></li>
      <li><a href="oaicat-1.5.44.zip">oaicat-1.5.44.zip</a></li>

      <li><a href="oaicat-1.5.43.tar.gz">oaicat-1.5.43.tar.gz</a></li>
      <li><a href="oaicat-1.5.43.zip">oaicat-1.5.43.zip</a></li>

      <li><a href="oaicat-1.5.42.tar.gz">oaicat-1.5.42.tar.gz</a></li>
      <li><a href="oaicat-1.5.42.zip">oaicat-1.5.42.zip</a></li>

      <li><a href="oaicat-1.5.41.tar.gz">oaicat-1.5.41.tar.gz</a></li>
      <li><a href="oaicat-1.5.41.zip">oaicat-1.5.41.zip</a></li>

      <li><a href="oaicat-1.5.40.tar.gz">oaicat-1.5.40.tar.gz</a></li>
      <li><a href="oaicat-1.5.40.zip">oaicat-1.5.40.zip</a></li>

      <li><a href="oaicat-1.5.39.tar.gz">oaicat-1.5.39.tar.gz</a></li>
      <li><a href="oaicat-1.5.39.zip">oaicat-1.5.39.zip</a></li>

      <li><a href="oaicat-1.5.38.tar.gz">oaicat-1.5.38.tar.gz</a></li>
      <li><a href="oaicat-1.5.38.zip">oaicat-1.5.38.zip</a></li>

      <li><a href="oaicat-1.5.37.tar.gz">oaicat-1.5.37.tar.gz</a></li>
      <li><a href="oaicat-1.5.37.zip">oaicat-1.5.37.zip</a></li>

      <li><a href="oaicat-1.5.36.tar.gz">oaicat-1.5.36.tar.gz</a></li>
      <li><a href="oaicat-1.5.36.zip">oaicat-1.5.36.zip</a></li>

      <li><a href="oaicat-1.5.35.tar.gz">oaicat-1.5.35.tar.gz</a></li>
      <li><a href="oaicat-1.5.35.zip">oaicat-1.5.35.zip</a></li>

      <li><a href="oaicat-1.5.34.tar.gz">oaicat-1.5.34.tar.gz</a></li>
      <li><a href="oaicat-1.5.34.zip">oaicat-1.5.34.zip</a></li>

      <li><a href="oaicat-1.5.33.tar.gz">oaicat-1.5.33.tar.gz</a></li>
      <li><a href="oaicat-1.5.33.zip">oaicat-1.5.33.zip</a></li>

      <li><a href="oaicat-1.5.32.tar.gz">oaicat-1.5.32.tar.gz</a></li>
      <li><a href="oaicat-1.5.32.zip">oaicat-1.5.32.zip</a></li>

      <li><a href="oaicat-1.5.31.tar.gz">oaicat-1.5.31.tar.gz</a></li>
      <li><a href="oaicat-1.5.31.zip">oaicat-1.5.31.zip</a></li>

      <li><a href="oaicat-1.5.30.tar.gz">oaicat-1.5.30.tar.gz</a></li>
      <li><a href="oaicat-1.5.30.zip">oaicat-1.5.30.zip</a></li>

      <li><a href="oaicat-1.5.29.tar.gz">oaicat-1.5.29.tar.gz</a></li>
      <li><a href="oaicat-1.5.29.zip">oaicat-1.5.29.zip</a></li>

      <li><a href="oaicat-1.5.28.tar.gz">oaicat-1.5.28.tar.gz</a></li>
      <li><a href="oaicat-1.5.28.zip">oaicat-1.5.28.zip</a></li>

      <li><a href="oaicat-1.5.27.tar.gz">oaicat-1.5.27.tar.gz</a></li>
      <li><a href="oaicat-1.5.27.zip">oaicat-1.5.27.zip</a></li>

      <li><a href="oaicat-1.5.26.tar.gz">oaicat-1.5.26.tar.gz</a></li>
      <li><a href="oaicat-1.5.26.zip">oaicat-1.5.26.zip</a></li>

      <li><a href="oaicat-1.5.25.tar.gz">oaicat-1.5.25.tar.gz</a></li>
      <li><a href="oaicat-1.5.25.zip">oaicat-1.5.25.zip</a></li>

      <li><a href="oaicat-1.5.24.tar.gz">oaicat-1.5.24.tar.gz</a></li>
      <li><a href="oaicat-1.5.24.zip">oaicat-1.5.24.zip</a></li>

      <li><a href="oaicat-1.5.23.tar.gz">oaicat-1.5.23.tar.gz</a></li>
      <li><a href="oaicat-1.5.23.zip">oaicat-1.5.23.zip</a></li>

      <li><a href="oaicat-1.5.22.tar.gz">oaicat-1.5.22.tar.gz</a></li>
      <li><a href="oaicat-1.5.22.zip">oaicat-1.5.22.zip</a></li>

      <li><a href="oaicat-1.5.21.tar.gz">oaicat-1.5.21.tar.gz</a></li>
      <li><a href="oaicat-1.5.21.zip">oaicat-1.5.21.zip</a></li>

      <li><a href="oaicat-1.5.20.tar.gz">oaicat-1.5.20.tar.gz</a></li>
      <li><a href="oaicat-1.5.20.zip">oaicat-1.5.20.zip</a></li>

      <li><a href="oaicat-1.5.19.tar.gz">oaicat-1.5.19.tar.gz</a></li>
      <li><a href="oaicat-1.5.19.zip">oaicat-1.5.19.zip</a></li>

      <li><a href="oaicat-1.5.18.tar.gz">oaicat-1.5.18.tar.gz</a></li>
      <li><a href="oaicat-1.5.18.zip">oaicat-1.5.18.zip</a></li>

      <li><a href="oaicat-1.5.17.tar.gz">oaicat-1.5.17.tar.gz</a></li>
      <li><a href="oaicat-1.5.17.zip">oaicat-1.5.17.zip</a></li>

      <li><a href="oaicat-1.5.16.tar.gz">oaicat-1.5.16.tar.gz</a></li>
      <li><a href="oaicat-1.5.16.zip">oaicat-1.5.16.zip</a></li>
      <li><a href="oaicat-1.5.15.tar.gz">oaicat-1.5.15.tar.gz</a></li>
      <li><a href="oaicat-1.5.15.zip">oaicat-1.5.15.zip</a></li>
      <li><a href="oaicat-1.5.14.tar.gz">oaicat-1.5.14.tar.gz</a></li>
      <li><a href="oaicat-1.5.14.zip">oaicat-1.5.14.zip</a></li>
      <li><a href="oaicat-1.5.13.tar.gz">oaicat-1.5.13.tar.gz</a></li>
      <li><a href="oaicat-1.5.13.zip">oaicat-1.5.13.zip</a></li>
      <li><a href="oaicat-1.5.12.tar.gz">oaicat-1.5.12.tar.gz</a></li>
      <li><a href="oaicat-1.5.12.zip">oaicat-1.5.12.zip</a></li>
      <li><a href="oaicat-1.5.10.tar.gz">oaicat-1.5.10.tar.gz</a></li>
      <li><a href="oaicat-1.5.10.zip">oaicat-1.5.10.zip</a></li>
      <!--
      <li><a href="oaicat-1.5.9.tar.gz">oaicat-1.5.9.tar.gz</a></li>
      <li><a href="oaicat-1.5.9.zip">oaicat-1.5.9.zip</a></li>
      <li><a href="oaicat-1.5.8.tar.gz">oaicat-1.5.8.tar.gz</a></li>
      <li><a href="oaicat-1.5.8.zip">oaicat-1.5.8.zip</a></li>
      -->
    </ul>


    <hr>
    <address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address>
<!-- Created: Tue Apr 22 10:48:02 EDT 2003 -->
<!-- hhmts start -->
Last modified: Thu Sep 08 16:00:50 Eastern Daylight Time 2005
<!-- hhmts end -->
  </body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>OAICat - File System Implementation</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="oaicat.css" rel="stylesheet" type="text/css">
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="100%" border="0" cellspacing="20" cellpadding="0">
<tr valign="top">
<td bgcolor="#669933">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td>
<h2>File System Implementation</h2>
<p>A file system implementation is included with the OAICat package. The purpose is to provide OAI access to metadata records stored in files in the server's directory structure.</p>
<p>To test the file system implementation, follow these steps:</p>
<ol>
<li>Copy oaicat.war to Tomcat's webapps directory, restart Tomcat, and test the default configuration.</li>
<li>Restart Tomcat and test.</li>
</ol>
<p>If this works, change the values in webapps/oaicat/WEB-INF/oaicat.properties to suit your needs.</p>
</td>
</tr>
<tr valign="top">
<td bgcolor="#669933" height="1"></td>
</tr>
<tr valign="top">
<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
</tr>
<tr valign="top">
<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
<!-- hhmts start -->
Last modified: Mon Apr 21 15:38:29 EDT 2003
<!-- hhmts end -->
</span></td>
</tr>
</table>
</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>OAICat - Gwen Implementation</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="oaicat.css" rel="stylesheet" type="text/css">
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="100%" border="0" cellspacing="20" cellpadding="0">
<tr valign="top">
<td bgcolor="#669933">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td>
<h2>Gwen Implementation</h2>
<p>A Gwen implementation is included with the OAICat package. <a href="http://www.oclc.org/research/software/gwen/">Gwen</a> is an open-source search engine intended to work with OCLC's open-source <a href="http://www.oclc.org/research/software/pears/">Pears</a> database engine. Contact me for details.</p>
</td>
</tr>
<tr valign="top">
<td bgcolor="#669933" height="1"></td>
</tr>
<tr valign="top">
<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
</tr>
<tr valign="top">
<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
<!-- hhmts start -->
Last modified: Mon Apr 21 15:38:25 EDT 2003
<!-- hhmts end -->
</span></td>
</tr>
</table>
</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>OAICat - An OAI-PMH v2 Repository Framework</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="oaicat.css" rel="stylesheet" type="text/css">
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="100%" border="0" cellspacing="20" cellpadding="0">
<tr valign="top">
<td bgcolor="#669933">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td>
	  <h2>Description</h2>
    <p>The <a href="http://www.oclc.org/research/software/oai/cat.shtm">OAICat</a> Open Source project is a Java Servlet web application providing an <a href="http://www.openarchives.org/">OAI-PMH v2.0</a>
    repository framework. This framework can be customized to work with arbitrary data repositories
    by implementing some Java interfaces.</p>
          <h2>Installation</h2>
          <p>This distribution includes the oaicat.jar file containing the Java Servlet classes only. To update existing OAICat installations, replace the old oaicat/WEB-INF/lib/oaicat.jar file with the one included with this distribution. To do an initial installation of OAICat, download the OAICat <a href="http://www.oclc.org/research/software/oai/cat.shtm">Webapp</a> instead, place it in the J2EE webapp directory, and bounce the server.</p>
          <h2>For Your Own Good!</h2>
          <p>This is an open-source project, and the source code for oaicat.jar is included in this distribution along with a prebuild oaicat.jar file. It's in everyone's interest, though, if you resist the urge to change this code to satisfy local needs. It is much better to extend a given class and override its methods rather than change the code directly. This approach will allow you to download a fresh copy of oaicat.jar in the future to pick up bug fixes and enhancements without worrying about reconciling your changes with mine. This isn't always possible, though, so if you have any doubt or trouble with this request, please contact <a href="mailto:jyoung@oclc.org">Jeff Young</a> for assistance.</p>
    <h2>License Information</h2>
    <a href="http://purl.oclc.org/oclc/research/ORPL/">OCLC Research Public License</a>
</td>
</tr>
<tr valign="top">
<td>
<h2>Documentation</h2>
          <ul>
            <li><a href="docs/api/index.html">Javadocs</a></li>
            <li><a href="properties.html">Description of entries in the <code>oaicat.properties</code> file</a></li>
            <li><a href="README.txt">Customization Notes</a></li>
            <li>Sample Implementations
<ol>
<li><a href="filesys.html">File System Implementation (default)</a></li>
<li><a href="xmlfile.html">Single XML File Implementation</a></li>
<li><a href="sru.html">SRW/U Implementation</a></li>
<li><a href="jdbc.html">JDBC Implementation</a></li>
<li><a href="newton.html">Newton Implementation</a></li>
<li><a href="gwen.html">Gwen Implementation</a></li>
</ol></li>
          </ul>
</td>
</tr>
<tr valign="top">
<td bgcolor="#669933" height="1"></td>
</tr>
<tr valign="top">
<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
</tr>
<tr valign="top">
<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
<!-- hhmts start -->
Last modified: Tue Jan 04 09:40:18 Eastern Standard Time 2005
<!-- hhmts end -->
</span></td>
</tr>
</table>
</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>OAICat - JDBC Implementation</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="oaicat.css" rel="stylesheet" type="text/css">
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="100%" border="0" cellspacing="20" cellpadding="0">
<tr valign="top">
<td bgcolor="#669933">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td>
<h2>JDBC Implementation</h2>
<p>A JDBC implementation is included with the OAICat package. In theory, it should be possible to customize OAICat to work with any JDBC accessible database by merely changing a few entries in the application's properties file. If the reality isn't so simple, let me know and I can attempt to generalize it still further.</p>
<p>For Microsoft Windows users, a sample Access database named oaicatdemo.mdb is included in the war file distribution. To test the JDBC implementation against this JDBC source, follow these steps:</p>
<ol>
<li>Copy oaicat.war to Tomcat's webapps directory, restart Tomcat, and test the default configuration.</li>
<li>If it works, start the ODBC Data Source Administrator control panel, click the "System DSN" tab, then "Add...".</li>
<li>Choose "Microsoft Access Driver (*.mdb).</li>
<li>Type "oaicatdemo" in the Data Source Name box, and browse to select "oaicatdemo.mdb" located in the webapps/oaicat/ directory.</li>
<li>Close the ODBC Data Source Administrator control panel.</li>
<li>Delete webapps/oaicat/WEB-INF/oaicat.properties.</li>
<li>Rename webapps/oaicat/WEB-INF/jdbc.properties to oaicat.properties.</li>
<li>Restart Tomcat and test.</li>
</ol>
<p>If this works, change the values in webapps/oaicat/WEB-INF/oaicat.properties (formerly jdbc.properties) to suit your needs.</p>
</td>
</tr>
<tr valign="top">
<td bgcolor="#669933" height="1"></td>
</tr>
<tr valign="top">
<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
</tr>
<tr valign="top">
<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
<!-- hhmts start -->
Last modified: Mon Apr 21 15:38:16 EDT 2003
<!-- hhmts end -->
</span></td>
</tr>
</table>
</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>OAICat - Newton Implementation</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="oaicat.css" rel="stylesheet" type="text/css">
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="100%" border="0" cellspacing="20" cellpadding="0">
<tr valign="top">
<td bgcolor="#669933">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td>
<h2>Newton Implementation</h2>
<p>A Newton implementation is included with the OAICat package. Newton is a proprietary database bundled with OCLC's <a href="http://www.sitesearch.oclc.org/">SiteSearch</a> product. Contact me for details.</p>
</td>
</tr>
<tr valign="top">
<td bgcolor="#669933" height="1"></td>
</tr>
<tr valign="top">
<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
</tr>
<tr valign="top">
<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
<!-- hhmts start -->
Last modified: Mon Apr 21 15:38:11 EDT 2003
<!-- hhmts end -->
</span></td>
</tr>
</table>
</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>OAICat - An OAI-PMH v2 Repository Framework</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="oaicat.css" rel="stylesheet" type="text/css">
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="100%" border="0" cellspacing="20" cellpadding="0">
<tr valign="top">
<td bgcolor="#669933">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td>
	  <h2>Description of Entries in oaicat.properties</h2>
          <ul>
	    <li><b>OAIHandler.extraXmlns=[xmlns attributes] (Optional)</b>: This string gets inserted into the <oai:OAI-PMH> root element in ListRecords and GetRecord responses. It was added to make large-scale harvesting of DSpace METS records more efficient.</li>
            <li><b>OAIHandler.serviceUnavailable=[true|false] (Optional)</b>: Forces the server to return a SC_SERVICE_UNAVAILABLE code to inform the user that it is currently being worked on.</li>
            <li><b>OAIHandler.styleSheet=[path-to-xsl-stylesheet] (Optional)</b>: If present, a reference to this XSLT stylesheet will be included in the OAI responses.</li>
            <li><b>OAIHandler.renderForOldBrowsers=[true|false] (Optional)</b>: If present and true, the XSLT stylesheet will be applied to the OAI response on the the server before sending back the response, if the client is an old browser. This is a good idea if you've defined OAIHandler.styleSheet because some old browsers don't do a very good job of rendering XSLT on the client side.</li>
            <li><b>OAIHandler.forceRender=[true|false] (Optional)</b>: If present and true, the XSLT stylesheet will be applied to the OAI response on the the server before sending back the response. This is useful if you want to use OAICat as a web application framework, but want to disable OAI harvesting because the data is proprietary (e.g. <a href="http://alcme.oclc.org/gsafd/">The GSAFD Thesaurus</a>).</li>
            <li><b>OAIHandler.baseURL=[OAI baseURL] (Optional)</b>: Force OAI responses to include the specified baseURL instead of getting it from the HttpServletRequest. This may be necessary if your firewall/router/port-mapper is messing with the request in some way.</li>
          </ul>
          <ul>
            <li><b>AbstractCatalog.oaiCatalogClassName=[package.classname] (Required)</b>: The fully qualified Java class name that implements ORG.oclc.oai.server.catalog.AbstractCatalog.</li>
            <li><b>AbstractCatalog.recordFactoryClassName=[package.classname] (Required)</b>: The fully qualified Java class name that implements ORG.oclc.oai.server.catalog.RecordFactory.</li>
            <li><b>AbstractCatalog.secondsToLive=[# of seconds] (Optional)</b>: The number of seconds a resumptionToken is retained for reuse. The default is -1, which means that resumptionTokens are stateless and can be resent anytime.</li>
            <li><b>AbstractCatalog.harvestable=[true|false] (Optional)</b>: If present and false, the ListRecords and ListIdentifiers verbs will send back an error condition. This might be useful if you want to provide access to the other verbs but want to discourage general harvesting.</li>
            <li><b>AbstractCatalog.granularity=[YYYY-MM-DD|YYYY-MM-DDThh:mm:ssZ] (Required)</b>: The supported level of datetime granularity for from/until parameters.</li>
          </ul>
          <ul>
            <li><b>Identify.repositoryName=[text] (Required)</b>: Text to use as the Identify verb's repositoryName value.</li>
            <li><b>Identify.adminEmail[mailto:abc@xyz.org] (Required)</b>: Text to use as the Identify verb's adminEmail value.</li>
            <li><b>Identify.earliestDatestamp=[datetime stamp of earliest] (Required)</b>: Text to use as the Identify verb's earliestDatestamp value.</li>
            <li><b>Identify.deletedRecord=[no|transient|persistent] (Required)</b>: Text to use as the Identify verb's deletedRecord value.</li>
            <li><b>Identify.description.[0-9]*=[xml fragment] (Optional; Repeatable)</b>: XML fragments (&lt;description&gt;) to include in the Identify verb's response.</li>
          </ul>
          <ul>
            <li><b>Crosswalks.[metadataPrefix]=[package.classname] (Required; Repeatable)</b>: Java class(es) that implement the ORG.oclc.oai.server.crosswalk.Crosswalk class.</li>
          </ul>
          <ul>
            <li><b>ExtensionVerbs.[verb name]=[package.classname] (Optional; Repeatable)</b>: Java class(es) that extend the ORG.oclc.oai.server.verb.ServerVerb class. This allow you to easily create new verbs to perform application functions beyond the set of OAI verbs. These verbs can be accessed by adding &quot;/extension&quot; to the baseURL.</li>
          </ul>
          <ul>
            <li>Other properties that support various combinations of classes mentioned above.</li>
          </ul>
</td>
</tr>
<tr valign="top">
<td bgcolor="#669933" height="1"></td>
</tr>
<tr valign="top">
<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
</tr>
<tr valign="top">
<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
<!-- hhmts start -->
Last modified: Fri Jul 29 10:03:16 Eastern Daylight Time 2005
<!-- hhmts end -->
</span></td>
</tr>
</table>
</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
  <head>
    <title>OAICat - SRW/U Implementation</title>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
    <link href="oaicat.css" rel="stylesheet" type="text/css">
  </head>
  <body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
    <table width="100%" border="0" cellspacing="20" cellpadding="0">
      <tr valign="top">
	<td bgcolor="#669933">
	  <table width="100%" border="0" cellspacing="0" cellpadding="0">
	    <tr valign="top">
	      <td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
	      <td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
	    </tr>
	  </table>
	</td>
      </tr>
      <tr valign="top">
	<td>
	  <h2>SRW/U Implementation</h2>
	  <p>An SRW/U implementation is included with the OAICat package. Any SRW/U database can be configured for OAI access if it conforms to the following (prototype) profile:
	  </p>
	  <ul>
	    <li>An oai.identifier index</li>
	    <li>An oai.datestamp index</li>
	    <li>An oai.set scan index (not implemented yet)</li>
	    <li>An OaiHeader recordSchema</li>
	  </ul>
	  <p>Caveat: the details of this profile are still being debated and could change.</p>
	  <p>To configure OAICat to use the SRW/U implementation, simply rename the sru.properties file to oaicat.properties and bounce the server. If this demo works, edit the oaicat.properties file to change the SRWOAICatalog.sruURL to the target SRW/U service URL and bounce the server again.</p>
	</td>
      </tr>
      <tr valign="top">
	<td bgcolor="#669933" height="1"></td>
      </tr>
      <tr valign="top">
	<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
      </tr>
      <tr valign="top">
	<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
	    <!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
	    <!-- hhmts start -->
	    Last modified: Tue Oct 14 10:28:18 EDT 2003
	    <!-- hhmts end -->
	  </span></td>
      </tr>
    </table>
  </body>
</html>
OAICat version file. This file records major changes to the OAICat software.

2013-10-18 1.5.62
Bug fix:
- Added a AbstractCatalog.paramRegex property to address an SQL injection vulnerability that was reported by Ward van Wanrooij. [https://code.google.com/p/oaicat/issues/detail?id=7]

2013-04-18 1.5.61
Enhancement:
- change mechanism for locating the oaicat.properties configuration file.

2011-04-22 1.5.60
Bug Fix:
- Add missing closure to 'if' condition when handling "global" attributes.

2011-04-22 1.5.59
Bug Fix:
- Close java.sql.Statements after use in ExtendedJDBCOAICatalog.

2011-03-31 1.5.58
Bug Fix:
- Deprecate old JDBC AbstractCatalog implementations in favor of ExtendedJDBCOAICatalog.
Enhancement:
- Cleaned up various build warnings.

2009-03-26 1.5.57
Bug Fix:
- Fix persistentConnection bug in NewJDBCOAICatalog

2008-10-27 1.5.56
Bug Fix:
- Change toolkit.xsd reference to the alcme.oclc.org domain since the VT version vanished

2007-11-01 1.5.55
Enhancement:
- Make OAIHandler.getAttributes methods public

2007-05-22 1.5.54
Enhancement:
- Switch license banners to Apache 2.0

2007-01-25 1.5.53
Enhancement:
- Add support for java.sql.Date datestamps in the JDBC implementation.

2007-01-25 1.5.52
Enhancement:
- add Getty implementation to support museums and archives.

2006-12-20 1.5.51
Enhancement:
- add OAIHandler.missingVerbClassName parameter support

2006-12-20 1.5.50
Enhancement:
- add OAIHandler.isServiceUnavailable so developers can decide how this gets
done themselves.

2006-11-06 1.5.49
Enhancement:
- add filterRequest() to OAIHandler to allow overriding classes to control
which requests get through.

2006-10-23 1.5.48
Enhancement:
- add a getDescriptions() method to the AbstractCatalog and Identify verb

2006-09-06 1.5.47
Bug Fix:
- XML-encode attribute values
- fixed a synchronization problem in XSLTCrosswalk

2006-06-30 1.5.46
Enhancement:
- add support for multiple databases

2006-06-22 1.5.45
Cleanup:
- Removed some issues that were cluttering my Eclipse "Problems" view.
It was mainly things like "throws" that were declared but never used and
private methods and variables that were never used. I'm fairly confident
that their removal won't affect anyone's custom classes.

2006-04-06 1.5.44
Bug Fix:
- Restore JDBCOAICatalog, JDBCLimitedOAICatalog, and ExtendedJDBCOAICatalog in favor of NewJDBCOAICatalog

2006-04-04 1.5.43
Bug Fix:
- Add close statements to Statements and ResultSets in JDBCOAICatalog, JDBCLimitedOAICatalog, and ExtendedJDBCOAICatalog to plug a memory leak

2006-02-06 1.5.42
Enhancement:
- Add support for repeatable fields to JDBC2oai_dc.java

2005-12-08 1.5.41
Bug Fix:
- Add support for multiple metadataPrefixes that share the same schemaLocation

2005-09-26 1.5.40
Enhancement:
- Allow configuration files to reside on the classpath as an alternative to
hardcoding them relative to $TOMCAT_HOME

2005-09-08 1.5.39
Bug Fix:
- Exit the loop in RecordHandler so no more than one metadata record gets created.

2005-07-29 1.5.38
Enhancement:
- Added optional OAIHandler.extraXmlns parameter to oaicat.properties handler. This string will be inserted into the <oai:OAI-PMH> to allow extra xmlns declarations to be included in the response.

2005-07-21 1.5.37
Bug Fix:
- Omit XML declaration from FileMap2oai_dc identity transformer

2005-06-07 1.5.36
Enhancement:
- Added a feature contributed by Peter Guldbaek to support an alternative method of loading the Properties object.

2005-03-25 1.5.35
Enhancement:
- Make the xsl stylesheet optional on FileMap2.oai_dc to make it easier to store oai_dc records instead of oai_etdms.

2005-02-28 1.5.34
Bug Fix:
- Force identifier parameters to conform to URI syntax or else throw a badArgument error.

2005-02-04 1.5.33
Enhancement:
- Add an optional String parameter to the Crosswalk constructor to tell it which metadataPrefix it is being configured for.

2005-01-31 1.5.32
Bug Fix:
- Don't assume the objects in JDBCRecordFactory are strings.

2004-10-11 1.5.31
Bug Fix:
- Fix BadArgumentException for missing metadataPrefix in ListRecords and ListIdentifiers
- Change various RecordFactory implementations to use a public fromOAIIdentifier method rather than protected.

2004-10-11 1.5.30
Bug Fix:
- Remove Java 1.4 String.split() dependency.

2004-10-06 1.5.29
Bug Fix:
- Fix JDBC2oai_dc crosswalk to exclude empty fields from the result.

2004-09-15 1.5.28
Enhancement:
- Add dateFormat property support for JDBCOAICatalog and JDBCLimitedOAICatalog

2004-08-31 1.5.27
Enhancement:
- Add SRUOAICatalog support

2004-06-10 1.5.26
Enhancement:
- Add support for dynamic crosswalks.

2004-06-07 1.5.25
Enhancement:
- Add OAIHandler.urlEncodeSetSpec argument

2004-05-21 1.5.24
Enhancement:
- Construct verbs using a HashMap rather than a ServletContext so they can be called in a more generic way.
- Add a few set method to AbstractCatalog so implementations can be created directly rather than being forced to use the factory method.

2004-05-10 1.5.23
Bug Fix:
- Changed ExtendedJDBCOAICatalog and JDBCOAICatalog to use '!' instead of ':' as the resumptionToken separator to accommodate long datestamps.
Enhancement:
- Make OAIHandler getWriter and OAIHandler.getResult public.

2004-05-05 1.5.22
Bug Fix:
- Changed ExtendedJDBCRecordFactory.getLocalIdentifier to convert value to String rather than assume it is a string.

2004-04-06 1.5.21
Enhancement:
- Add Copymets crosswalk.

2004-03-15 1.5.20
Bug Fix:
- Fix incorrect setName in ListSets for NewFileSystemOAIHandler.

2004-03-08 1.5.19
Bug Fix:
- Fix ListMetadataFormats(identifier) for XMLFileOAICatalog

2004-02-18 1.5.18
Bug Fix:
- Add Opera to the list of User-Agents that have XSL stylesheets applied on the browser.

2003-12-22 1.5.17
Enhancement:
- Add OAIHandler.xmlEncodeSetSpec property option

2003-11-05 1.5.16
Bug Fix:
- Added synchronized to javax.xml.transform.Transformer invocations.

2003-10-22 1.5.15
Enhancement:
- Added oaicatnewton.jar to the distribution.

2003-10-14 1.5.14
Enhancement:
- Added "info:ofi/pro" to the list of namespaces that can be parsed by XMLFileOAICatalog.
- Added XSLTproCrosswalk
- Changed the Redirect extension verb to return the oai_dc record if no dc:identifier is found.

2003-06-13 1.5.13
Bug Fix:
- Allow for missing User-Agent in HTTP header.

2003-06-05 1.5.12
Bug Fix:
- XML Encode the <identifier> portion of the response.

2003-06-03 1.5.11
Bug Fix:
- Fixed a few more bugs in JDBCOAICatalog where OAI-type Exceptions were
  being caught and transformed into Error 500 failures.

2003-05-14 1.5.10
Bug Fix:
- Fixed a bug in JDBCOAICatalog where invalid identifiers weren't being reported properly from GetRecord verbs.

2003-05-05 1.5.10
Enhancement:
- Add call to ServerVerb.init for extension verbs to allow them to initialize themselves.
- Add call to HttpServletRequest.setCharacterEncoding("UTF-8") to handle escaped characters in the URL.

2003-04-29 1.5.9
Bug Fix:
- Moved extension verbs so they only work from baseURL + "/extension". This should satisfy those who worry about adding non-OAI verbs to the baseURL. Also changed the property from ServerVerbs.* to ExtensionVerbs.*
- Fixed ListRecords and ListIdentifiers to convert blanks back to '+'. Hopefully, this won't cause people too much grief.

2003-04-21 1.5.8
Enhancements:
- Added an optional Redirect verb to support POIs (http://www.ukoln.ac.uk/distributed-systems/poi/resolver-guidelines/.
- Added JUnit tests to the Ant build scripts.
- Added a little more documentation
- Improved version control.
- Fixed a bug where setting the HTTP Content-Encoding prevented the return of HTTP status codes.

2003-04-17 1.5.7
Bg fixes:
- Removed URLEncoder.encode() calls in RecordFactory and ServerVerb because URLEncoding setSpecs isn't the right solution. I don't recall why I did this in the first place, and now I worry that it will break someone's existing installation because they are gone. If so, hopefully they will contact me and we can fix it propertly.
Enhancements
- Add an OAIHarvester.forceRender property to discourage the harvesting of proprietary data.
- Add a RecordFactory.encodeSetSpec parameter (default=true) since it's not really supposed to encode colons. Eventually, this will need to be fixed properly to encode only those characters that actually require it.

2003-03-28 1.5.6
Enhancements
- Allow for additional verbs to be configured in oaicat.properties.
- Added an optional GetMetadata verb.

2003-03-06 1.5.5
Enhancements
- Added an optional OAIHandler.renderForOldBrowsers property to recognize old "Mozilla" browsers and render the response into HTML if an OAIHandler.styleSheet property is present.

2002-11-18 1.5.4
Bug fixes:
- Removed 'compress' compression option since there is some question about its correctness.
- URLEncoded the request element's set value.
- Fixed a bug in the JDBC implementation that caused resumptionTokens to always respond with initial results.
Enhancements
- Added a FileSystem implementation and made it the default instead of the silly useless DummyOAICatalog.
- Moved the toolkit element to the bottom of the Identify response to make it less prominent.

2002-11-01 1.5.3
Bug fixes:
- Made JDBC2oai_dc real.
- Change setSpec handling to allow them to be collected from the database itself.
- Use a HashMap to represent a record rather than depending on the cursor of an entire resultSet.
- Changes too numerous to mention regarding JDBCOAICatalog. If you're using it, consider using JDBCLimitedOAICatalog instead. It should be more memory effecient.
- Added a new class named NoRecordMatchException which is a more appropriate name than NoItemsMatchException. Both work, though.
- Fixed a bug in the Identify response where the toolkit description wasn't passing validation.
- Changed the default 'from' and 'until' dates to something more realistic.


2002-10-22 1.5.2
Bug Fixes:
- Fixed a bug in the JDBC implementation that caused resumptionToken type
  requests to return the initial set of records each time.
- Made resumptionTokens in the JDBC implementation idempotent.
- Changed the default 'from' date from 01/01/0000 to 01/01/0001 to
  accommodate Oracle JDBC limitations.
- Fixed a problem with ListMetadataFormats in the JDBC implementation where it didn't recognize oaiidentifiers that weren't in sets.

2002-10-16 1.5.1
Enhancements:
- Added a JDBC implementation

2002-10-04 1.5
Enhancements:
- Added toolkit description to the Identify response and version number tracking.

2002-10-02 1.4
Bug Fixes:
- Added a check for bad arguments when they accompany resumptionTokens
  to satisfy the OAI repository registration validator.

2002-09-27 1.3
Enhancements:
- Added cusomization instructions to the Readme.txt file.
- Improved javadocs.

2002-09-25 1.2
Bug Fixes:
- Changed the way <description> elements are managed by the Identify verb.

2002-09-20 1.1
Bug Fixes:
- Added checks for BadArgumentExceptions to satisfy Hussein's Repository
  Explorer validation.

Enhancements:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>OAICat - Single XML File Implementation</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link href="oaicat.css" rel="stylesheet" type="text/css">
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="100%" border="0" cellspacing="20" cellpadding="0">
<tr valign="top">
<td bgcolor="#669933">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr valign="top">
<td width="50%"><a href="index.html"><img src="banner01.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="388" height="120" border="0"></a></td>
<td width="50%" align="right"><a href="http://www.oclc.org/research/"><img src="banner02.gif" alt="A Project of OCLC Research" width="202" height="120" border="0"></a></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td>
<h2>Single XML File Implementation</h2>
<p>A single XML file implementation is included with the OAICat package. The purpose is to provide OAI access to metadata records stored in a single XML file.</p>
<p>Contact <a href="mailto:jyoung@oclc.org">Jeff Young</a> for details about setting this up.</p>
</td>
</tr>
<tr valign="top">
<td bgcolor="#669933" height="1"></td>
</tr>
<tr valign="top">
<td><a href="http://www.oclc.org/research/software/oai/cat.shtm"><img border="0" src="oaicat_icon.gif" alt="OAICat - An OAI-PMH v2 Repository Framework" width="120" height="60"></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href="http://www.openarchives.org"><img border="0" src="http://www.openarchives.org/images/OA100.gif"/></a></td>
</tr>
<tr valign="top">
<td><address><a href="mailto:jyoung@oclc.org">Jeff Young</a></address><span class="footer">
<!-- Created: Wed Oct  2 16:53:15 EDT 2002 -->
<!-- hhmts start -->
Last modified: Mon Apr 21 15:38:05 EDT 2003
<!-- hhmts end -->
</span></td>
</tr>
</table>
</body>
</html>
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.SocketException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.verb.BadVerb;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.server.verb.ServerVerb;

/**
 * OAIHandler is the primary Servlet for OAICat.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class OAIHandler extends HttpServlet {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    
    public static final String PROPERTIES_SERVLET_CONTEXT_ATTRIBUTE = OAIHandler.class.getName() + ".properties";
    
    private static final String VERSION = "1.5.62";
    private static boolean debug = false;

//    private Transformer transformer = null;
//    private boolean serviceUnavailable = false;
//    private boolean monitor = false;
//    private boolean forceRender = false;
    protected HashMap attributesMap = new HashMap();
//    private HashMap serverVerbs = null;
//    private HashMap extensionVerbs = null;
//    private String extensionPath = null;
    
//    private static Logger logger = Logger.getLogger(OAIHandler.class);
//    static {
//        BasicConfigurator.configure();
//    }
    
    private Log log = LogFactory.getLog(OAIHandler.class);
    
    /**
     * Get the VERSION number
     */
    public static String getVERSION() { return VERSION; }
    
    /**
     * init is called one time when the Servlet is loaded. This is the
     * place where one-time initialization is done. Specifically, we
     * load the properties file for this application, and create the
     * AbstractCatalog object for subsequent use.
     *
     * @param config servlet configuration information
     * @exception ServletException there was a problem with initialization
     */
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        
        try {
            HashMap attributes = null;
            ServletContext context = getServletContext();
            Properties properties = (Properties) context.getAttribute(PROPERTIES_SERVLET_CONTEXT_ATTRIBUTE);
            if (properties == null) {
                final String PROPERTIES_INIT_PARAMETER = "properties";
                log.debug("OAIHandler.init(..): No '" + PROPERTIES_SERVLET_CONTEXT_ATTRIBUTE + "' servlet context attribute. Trying to use init parameter '" + PROPERTIES_INIT_PARAMETER + "'");
                
                String fileName = config.getServletContext().getInitParameter(PROPERTIES_INIT_PARAMETER);
                InputStream in;
                try {
                    log.debug("fileName=" + fileName);
                    in = new FileInputStream(fileName);
                } catch (FileNotFoundException e) {
                    log.debug("file not found. Try the classpath: " + fileName);
                    in = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);
                }
                if (in != null) {
                    log.debug("file was found: Load the properties");
                    properties = new Properties();
                    properties.load(in);
                    attributes = getAttributes(properties);
                    if (debug) System.out.println("OAIHandler.init: fileName=" + fileName);
                }
            } else {
                log.debug("Load context properties");
                attributes = getAttributes(properties);
            }

            log.debug("Store global properties");
            attributesMap.put("global", attributes);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        } catch (Throwable e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        }
    }
    
    public HashMap getAttributes(Properties properties)
    throws Throwable {
        HashMap attributes = new HashMap();
        Enumeration attrNames = getServletContext().getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = (String)attrNames.nextElement();
            attributes.put(attrName, getServletContext().getAttribute(attrName));
        }
        attributes.put("OAIHandler.properties", properties);
//        String temp = properties.getProperty("OAIHandler.debug");
//        if ("true".equals(temp)) debug = true;
        String missingVerbClassName = properties.getProperty("OAIHandler.missingVerbClassName", "ORG.oclc.oai.server.verb.BadVerb");
        Class missingVerbClass = Class.forName(missingVerbClassName);
        attributes.put("OAIHandler.missingVerbClass", missingVerbClass);
        if (!"true".equals(properties.getProperty("OAIHandler.serviceUnavailable"))) {
            attributes.put("OAIHandler.version", VERSION);
            AbstractCatalog abstractCatalog = AbstractCatalog.factory(properties, getServletContext());
            attributes.put("OAIHandler.catalog", abstractCatalog);
        }
        boolean forceRender = false;
        if ("true".equals(properties.getProperty("OAIHandler.forceRender"))) {
            forceRender = true;
        }
        String xsltName = properties.getProperty("OAIHandler.styleSheet");
        String appBase = properties.getProperty("OAIHandler.appBase");
        if (appBase == null) appBase = "webapps";
        if (xsltName != null
                && ("true".equalsIgnoreCase(properties.getProperty("OAIHandler.renderForOldBrowsers"))
                        || forceRender)) {
            InputStream is;
            try {
                is = new FileInputStream(appBase + "/" + xsltName);
            } catch (FileNotFoundException e) {
                // This is a silly way to skip the context name in the xsltName
                is = new FileInputStream(getServletContext().getRealPath(xsltName.substring(xsltName.indexOf("/", 1)+1)));
            }
            StreamSource xslSource = new StreamSource(is);
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer(xslSource);
            attributes.put("OAIHandler.transformer", transformer);
        }
        return attributes;
    }
    
    public HashMap getAttributes(String pathInfo) {
        HashMap attributes = null;
        log.debug("pathInfo=" + pathInfo);
        if (pathInfo != null && pathInfo.length() > 0) {
            if (attributesMap.containsKey(pathInfo)) {
                log.debug("attributesMap containsKey");
                attributes = (HashMap) attributesMap.get(pathInfo);
            } else {
                log.debug("!attributesMap containsKey");
                try {
                    String fileName = pathInfo.substring(1) + ".properties";
                    log.debug("attempting load of " + fileName);
                    InputStream in = Thread.currentThread()
                    .getContextClassLoader()
                    .getResourceAsStream(fileName);
                    if (in != null) {
                        log.debug("file found");
                        Properties properties = new Properties();
                        properties.load(in);
                        attributes = getAttributes(properties);
                    } else {
                        log.debug("file not found");
                    }
                    attributesMap.put(pathInfo, attributes);
                } catch (Throwable e) {
                    log.debug("Couldn't load file", e);
                    // do nothing
                }
            }
        }
        if (attributes == null) {
            log.debug("use global attributes");
            attributes = (HashMap) attributesMap.get("global");
        }
        return attributes;
    }

    /**
     * Peform the http GET action. Note that POST is shunted to here as well.
     * The verb widget is taken from the request and used to invoke an
     * OAIVerb object of the corresponding kind to do the actual work of the verb.
     *
     * @param request the servlet's request information
     * @param response the servlet's response information
     * @exception IOException an I/O error occurred
     */
    public void doGet(HttpServletRequest request,
            HttpServletResponse response)
    throws IOException {
        HashMap attributes = getAttributes(request.getPathInfo());
        if (!filterRequest(request, response)) {
            return;
        }
        log.debug("attributes=" + attributes);
        Properties properties =
            (Properties) attributes.get("OAIHandler.properties");
        boolean monitor = false;
        if (properties.getProperty("OAIHandler.monitor") != null) {
            monitor = true;
        }
        boolean serviceUnavailable = isServiceUnavailable(properties);
        String extensionPath = properties.getProperty("OAIHandler.extensionPath", "/extension");
        
        HashMap serverVerbs = ServerVerb.getVerbs(properties);
        HashMap extensionVerbs = ServerVerb.getExtensionVerbs(properties);
        
        Transformer transformer =
            (Transformer) attributes.get("OAIHandler.transformer");
        
        boolean forceRender = false;
        if ("true".equals(properties.getProperty("OAIHandler.forceRender"))) {
            forceRender = true;
        }
        
//      try {
        request.setCharacterEncoding("UTF-8");
//      } catch (UnsupportedEncodingException e) {
//      e.printStackTrace();
//      throw new IOException(e.getMessage());
//      }
        Date then = null;
        if (monitor) then = new Date();
        if (debug) {
            Enumeration headerNames = request.getHeaderNames();
            System.out.println("OAIHandler.doGet: ");
            while (headerNames.hasMoreElements()) {
                String headerName = (String)headerNames.nextElement();
                System.out.print(headerName);
                System.out.print(": ");
                System.out.println(request.getHeader(headerName));
            }
        }
        if (serviceUnavailable) {
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
            "Sorry. This server is down for maintenance");
        } else {
            try {
                String userAgent = request.getHeader("User-Agent");
                if (userAgent == null) {
                    userAgent = "";
                } else {
                    userAgent = userAgent.toLowerCase();
                }
                Transformer serverTransformer = null;
                if (transformer != null) {
                    
                    // return HTML if the client is an old browser
                    if (forceRender
                            || userAgent.indexOf("opera") != -1
                            || (userAgent.startsWith("mozilla")
                                    && userAgent.indexOf("msie 6") == -1
                            /* && userAgent.indexOf("netscape/7") == -1 */)) {
                        serverTransformer = transformer;
                    }
                }
                String result = getResult(attributes, request, response, serverTransformer, serverVerbs, extensionVerbs, extensionPath);
//              log.debug("result=" + result);
                
//              if (serverTransformer) { // render on the server
//              response.setContentType("text/html; charset=UTF-8");
//              StringReader stringReader = new StringReader(getResult(request));
//              StreamSource streamSource = new StreamSource(stringReader);
//              StringWriter stringWriter = new StringWriter();
//              transformer.transform(streamSource, new StreamResult(stringWriter));
//              result = stringWriter.toString();
//              } else { // render on the client
//              response.setContentType("text/xml; charset=UTF-8");
//              result = getResult(request);
//              }
                
                Writer out = getWriter(request, response);
                out.write(result);
                out.close();
            } catch (FileNotFoundException e) {
                if (debug) {
                    e.printStackTrace();
                    System.out.println("SC_NOT_FOUND: " + e.getMessage());
                }
                response.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
            } catch (TransformerException e) {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            } catch (OAIInternalServerError e) {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            } catch (SocketException e) {
                System.out.println(e.getMessage());
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            } catch (Throwable e) {
                e.printStackTrace();
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            }
        }
        if (monitor) {
            StringBuffer reqUri = new StringBuffer(request.getRequestURI().toString());
            String queryString = request.getQueryString();   // d=789
            if (queryString != null) {
                reqUri.append("?").append(queryString);
            }
            Runtime rt = Runtime.getRuntime();
            System.out.println(rt.freeMemory() + "/" + rt.totalMemory() + " "
                    + ((new Date()).getTime()-then.getTime()) + "ms: "
                    + reqUri.toString());
        }
    }
    
    /**
     * Should the server report itself down for maintenance? Override this
     * method if you want to do this check another way.
     * @param properties
     * @return true=service is unavailable, false=service is available
     */
    protected boolean isServiceUnavailable(Properties properties) {
        if (properties.getProperty("OAIHandler.serviceUnavailable") != null) {
            return true;
        }
        return false;
    }

    /**
     * Override to do any prequalification; return false if
     * the response should be returned immediately, without
     * further action.
     * 
     * @param request
     * @param response
     * @return false=return immediately, true=continue
     */
    protected boolean filterRequest(HttpServletRequest request,
            HttpServletResponse response) {
        return true;
    }

    public static String getResult(HashMap attributes,
            HttpServletRequest request,
            HttpServletResponse response,
            Transformer serverTransformer,
            HashMap serverVerbs,
            HashMap extensionVerbs,
            String extensionPath)
    throws Throwable {
        try {
            boolean isExtensionVerb = extensionPath.equals(request.getPathInfo());
            String verb = request.getParameter("verb");
            if (debug) {
                System.out.println("OAIHandler.g<etResult: verb=>" + verb + "<");
            }
            String result;
            Class verbClass = null;
            if (isExtensionVerb) {
                verbClass = (Class)extensionVerbs.get(verb);
            } else {
                verbClass = (Class)serverVerbs.get(verb);
            }
            if (verbClass == null) {
                verbClass = (Class) attributes.get("OAIHandler.missingVerbClass");
            }
            Method construct = verbClass.getMethod("construct",
                    new Class[] {HashMap.class,
                    HttpServletRequest.class,
                    HttpServletResponse.class,
                    Transformer.class});
            try {
                result = (String)construct.invoke(null,
                        new Object[] {attributes,
                        request,
                        response,
                        serverTransformer});
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
            if (debug) {
                System.out.println(result);
            }
            return result;
        } catch (NoSuchMethodException e) {
            throw new OAIInternalServerError(e.getMessage());
        } catch (IllegalAccessException e) {
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Get a response Writer depending on acceptable encodings
     * @param request the servlet's request information
     * @param response the servlet's response information
     * @exception IOException an I/O error occurred
     */
    public static Writer getWriter(HttpServletRequest request, HttpServletResponse response)
    throws IOException {
        Writer out;
        String encodings = request.getHeader("Accept-Encoding");
        if (debug) {
            System.out.println("encodings=" + encodings);
        }
        if (encodings != null && encodings.indexOf("gzip") != -1) {
//          System.out.println("using gzip encoding");
//          log.debug("using gzip encoding");
            response.setHeader("Content-Encoding", "gzip");
            out = new OutputStreamWriter(new GZIPOutputStream(response.getOutputStream()),
            "UTF-8");
//          } else if (encodings != null && encodings.indexOf("compress") != -1) {
//          //  	    System.out.println("using compress encoding");
//          response.setHeader("Content-Encoding", "compress");
//          ZipOutputStream zos = new ZipOutputStream(response.getOutputStream());
//          zos.putNextEntry(new ZipEntry("dummy name"));
//          out = new OutputStreamWriter(zos, "UTF-8");
        } else if (encodings != null && encodings.indexOf("deflate") != -1) {
//          System.out.println("using deflate encoding");
//          log.debug("using deflate encoding");
            response.setHeader("Content-Encoding", "deflate");
            out = new OutputStreamWriter(new DeflaterOutputStream(response.getOutputStream()),
            "UTF-8");
        } else {
//          log.debug("using no encoding");
            out = response.getWriter();
        }
        return out;
    }
    
    /**
     * Peform a POST action. Actually this gets shunted to GET
     *
     * @param request the servlet's request information
     * @param response the servlet's response information
     * @exception IOException an I/O error occurred
     */
    public void doPost(HttpServletRequest request,
            HttpServletResponse response)
    throws IOException {
        doGet(request, response);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ORG.oclc.oai.server.catalog;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;

import javax.servlet.ServletContext;

import ORG.oclc.oai.server.crosswalk.Crosswalks;
import ORG.oclc.oai.server.verb.BadArgumentException;
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.server.verb.ServerVerb;

/**
 * AbstractCatalog is the generic interface between OAICat and any arbitrary
 * database. Implement this interface to have OAICat work with your database.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public abstract class AbstractCatalog {
    private static final boolean debug = false;
    /**
     * The RecordFactory that understands how to convert this database's
     * native "item" to the various metadataFormats to be supported.
     */
    private RecordFactory recordFactory;

    /**
     * is this repository harvestable?
     */
    private boolean harvestable = true;

    /**
     * optional property to limit the life of resumptionTokens (<0 indicates no limit)
     **/
    private int millisecondsToLive = -1;

    /**
     * Index into VALID_GRANULARITIES and FROM_GRANULARITIES
     */
    private int supportedGranularityOffset = -1;

    /**
     * All possible valid granularities
     */
    private static final String[] VALID_GRANULARITIES = {
        "YYYY-MM-DD",
        "YYYY-MM-DDThh:mm:ssZ"
    };

    /**
     * minimum valid 'from' granularities
     */
    private static final String[] FROM_GRANULARITIES = {
        "0000-01-01",
        "0000-01-01T00:00:00Z"
    };

    /**
     * Default regex pattern for validating parameters (can be overridden using an AbstractCatalog.paramRegex property
     */
    private String paramRegex = "^[a-zA-Z0-9\\.\\/\\:\\_\\-]*$";

    /**
     * return a handle to the RecordFactory
     * @return guess
     */
    public RecordFactory getRecordFactory() { return recordFactory; }

    public void setHarvestable(boolean harvestable) {
        this.harvestable = harvestable;
    }

    public void setParamRegex(String s) {
    	this.paramRegex = s;
    }

    /**
     * Is this repository harvestable?
     * @return true if harvestable, false otherwise.
     */
    public boolean isHarvestable() { return harvestable; }

    /**
     * get the optional millisecondsToLive property (<0 indicates no limit)
     **/
    public int getMillisecondsToLive() { return millisecondsToLive; }

    public void setRecordFactory(RecordFactory recordFactory) {
        this.recordFactory = recordFactory;
    }

    public void setSupportedGranularityOffset(int i) {
        supportedGranularityOffset = i;
    }

    /**
     * Convert the requested 'from' parameter to the finest granularity supported
     * by this repository.
     * @exception BadArgumentException one or more of the arguments are bad.
     */
    public String toFinestFrom(String from)
    throws BadArgumentException {
        if (debug) {
            System.out.println("AbstractCatalog.toFinestFrom: from=" + from);
            System.out.println("                            target=" + VALID_GRANULARITIES[supportedGranularityOffset]);
        }
        if (from.length() > VALID_GRANULARITIES[supportedGranularityOffset].length()) {
            throw new BadArgumentException();
        }
        if (from.length() != VALID_GRANULARITIES[supportedGranularityOffset].length()) {
            StringBuffer sb = new StringBuffer(from);
            if (sb.charAt(sb.length()-1) == 'Z')
                sb.setLength(sb.length()-1);

            sb.append(FROM_GRANULARITIES[supportedGranularityOffset].substring(sb.length()));
            from = sb.toString();
        }

        if (!isValidGranularity(from)) {
            throw new BadArgumentException();
        }

        return from;
    }

    /**
     * Convert the requested 'until' paramter to the finest granularity supported
     * by this repository
     * @exception BadArgumentException one or more of the arguments are bad.
     */
    public String toFinestUntil(String until)
    throws BadArgumentException {
        if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
            if (!isValidGranularity(until))
                throw new BadArgumentException();
            return until;
        }
        if (until.length() > VALID_GRANULARITIES[supportedGranularityOffset].length()) {
            throw new BadArgumentException();
        }

        StringBuffer sb = new StringBuffer(until);
        if (sb.charAt(sb.length()-1) == 'Z')
            sb.setLength(sb.length()-1);

        if (sb.length() < VALID_GRANULARITIES[0].length()) {
            while (sb.length() < 4) sb.append("9");
            switch (sb.length()) {
            case 4: // YYYY
                sb.append("-");
            case 5: // YYYY-
                sb.append("12");
            case 7: // YYYY-MM
                sb.append("-");
            case 8: // YYYY-MM-
                sb.append("31");
                break;

            case 6: // YYYY-M
            case 9: // YYYY-MM-D
                throw new BadArgumentException();
            }
        }

        until = sb.toString();
        if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
            if (!isValidGranularity(until))
                throw new BadArgumentException();
            return until;
        }

        if (sb.length() < VALID_GRANULARITIES[1].length()) {
            switch (sb.length()) {
            case 10: // YYYY-MM-DD
                sb.append("T");
            case 11: // YYYY-MM-DDT
                sb.append("23");
            case 13: // YYYY-MM-DDThh
                sb.append(":");
            case 14: // YYYY-MM-DDThh:
                sb.append("59");
//              case 16: // YYYY-MM-DDThh:mm
//              sb.append("Z");
//              break;

//              case 12: // YYYY-MM-DDTh
//              case 15: // YYYY-MM-DDThh:m
//              throw new BadGranularityException();
//              }
//              }

//              until = sb.toString();
//              if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
//              if (!isValidGranularity(until))
//              throw new BadGranularityException();
//              return until;
//              }

//              if (sb.charAt(sb.length()-1) == 'Z')
//              sb.setLength(sb.length()-1); // remove the trailing 'Z'

//              if (sb.length() < VALID_GRANULARITIES[2].length()) {
//              switch (sb.length()) {
            case 16: // YYYY-MM-DDThh:mm
                sb.append(":");
            case 17: // YYYY-MM-DDThh:mm:
                sb.append("59");
            case 19: // YYYY-MM-DDThh:mm:ss
                sb.append("Z");
                break;

            case 18: // YYYY-MM-DDThh:mm:s
                throw new BadArgumentException();
            }
        }

//      until = sb.toString();
//      if (until.length() == VALID_GRANULARITIES[supportedGranularityOffset].length()) {
//      if (!isValidGranularity(until))
//      throw new BadGranularityException();
//      return until;
//      }

//      if (sb.charAt(sb.length()-1) == 'Z')
//      sb.setLength(sb.length()-1); // remove the trailing 'Z'

//      switch (sb.length()) {
//      case 19: // YYYY-MM-DDThh:mm:ss
//      sb.append(".");
//      case 20: // YYYY-MM-DDThh:mm:ss.
//      sb.append("0");
//      case 21: // YYYY-MM-DDThh:mm:ss.s
//      sb.append("Z");
//      break;
//      }

        until = sb.toString();
        if (!isValidGranularity(until))
            throw new BadArgumentException();
        return until;
    }

    /**
     * Does the specified date conform to the supported granularity of this repository?
     * @param date a UTC date
     * @return true if date conforms to the supported granularity of this repository,
     * false otherwise.
     */
    private boolean isValidGranularity(String date) {
        if (date.length() > VALID_GRANULARITIES[supportedGranularityOffset].length())
            return false;

        if (date.length() < VALID_GRANULARITIES[0].length()
                || !Character.isDigit(date.charAt(0)) // YYYY
                || !Character.isDigit(date.charAt(1))
                || !Character.isDigit(date.charAt(2))
                || !Character.isDigit(date.charAt(3))
                || date.charAt(4) != '-'
                    || !Character.isDigit(date.charAt(5)) // MM
                    || !Character.isDigit(date.charAt(6))
                    || date.charAt(7) != '-'
                        || !Character.isDigit(date.charAt(8)) // DD
                        || !Character.isDigit(date.charAt(9))) {
            return false;
        }

        if (date.length() > VALID_GRANULARITIES[0].length()) {
            if (date.charAt(10) != 'T'
                || date.charAt(date.length()-1) != 'Z'
                    || !Character.isDigit(date.charAt(11)) // hh
                    || !Character.isDigit(date.charAt(12))
                    || date.charAt(13) != ':'
                        || !Character.isDigit(date.charAt(14)) // mm
                        || !Character.isDigit(date.charAt(15))
//                      ) {
//                      return false;
//                      }
//                      }

//                      if (date.length() > VALID_GRANULARITIES[1].length()) {
//                      if (
                        || date.charAt(16) != ':'
                            || !Character.isDigit(date.charAt(17)) // ss
                            || !Character.isDigit(date.charAt(18))) {
                return false;
            }
        }

//      if (date.length() > VALID_GRANULARITIES[2].length()) {
//      if (date.charAt(19) != '.'
//      || !Character.isDigit(date.charAt(20))) { // s
//      return false;
//      }

//      }
        return true;
    }

    /**
     * Retrieve the Crosswalks property
     *
     * @return the Crosswalks object containing a detailed list of oai
     * formats supported by this application.
     */
    public Crosswalks getCrosswalks() { return recordFactory.getCrosswalks(); }

    /**
     * Retrieve the list of supported Sets. This should probably be initialized
     * by the constructor from the properties object that is passed to it.
     *
     * @return a Map object containing <setSpec> values as the Map keys and
     * <setName> values for the corresponding the Map values.
     * @exception NoSetHierarchyException No sets are defined for this repository
     * @exception OAIInternalServerError An error occurred
     */
    public abstract Map listSets() throws NoSetHierarchyException, OAIInternalServerError;

    /**
     * Retrieve the next cluster of supported sets.
     * @return a Map object containing <setSpec> values as the Map keys and
     * <setName> values for the corresponding the Map values.
     * @exception BadResumptionTokenException The resumptionToken is bad.
     * @exception OAIInternalServerError An error occurred
     */
    public abstract Map listSets(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError;

    /**
     * Factory method for creating an AbstractCatalog instance. The properties
     * object must contain the following entries:
     * <ul>
     *   <li><b>AbstractCatalog.className</b> property which points to a class
     *       that implements the AbstractCatalog interface. Note that this class
     *       must have a constructor that accepts a properties object as a
     *       parameter.</li>
     *   <li><b>Crosswalks.&lt;supported formats&gt;</b> properties which
     *       satisfy the constructor for the Crosswalks class</li>
     * </ul>
     *
     * @param properties Properties object containing entries necessary to
     * initialize the class
     * to be created.
     * @return on object instantiating the AbstractCatalog interface.
     * @exception Throwable some sort of problem occurred.
     */
    public static AbstractCatalog factory(Properties properties,
            ServletContext context)
    throws Throwable {
        AbstractCatalog oaiCatalog = null;
        String oaiCatalogClassName =
            properties.getProperty("AbstractCatalog.oaiCatalogClassName");
        String recordFactoryClassName =
            properties.getProperty("AbstractCatalog.recordFactoryClassName");
        if (oaiCatalogClassName == null) {
            throw new ClassNotFoundException(
            "AbstractCatalog.oaiCatalogClassName is missing from properties file");
        }
        if (recordFactoryClassName == null) {
            throw new ClassNotFoundException(
            "AbstractCatalog.recordFactoryClassName is missing from properties file");
        }
        Class oaiCatalogClass = Class.forName(oaiCatalogClassName);
        try {
            Constructor oaiCatalogConstructor = null;
            try {
                oaiCatalogConstructor =
                    oaiCatalogClass.getConstructor(new Class[] {Properties.class,
                            ServletContext.class});
                oaiCatalog =
                    (AbstractCatalog)oaiCatalogConstructor.newInstance(new Object[]
                                                                                  {properties, context});
            } catch (NoSuchMethodException e) {
                oaiCatalogConstructor =
                    oaiCatalogClass.getConstructor(new Class[] {Properties.class});
                oaiCatalog =
                    (AbstractCatalog)oaiCatalogConstructor.newInstance(new Object[]
                                                                                  {properties});
            }
            if (debug) {
                System.out.println("AbstractCatalog.factory: recordFactoryClassName="
                        + recordFactoryClassName);
            }
            Class recordFactoryClass = Class.forName(recordFactoryClassName);
            Constructor recordFactoryConstructor =
                recordFactoryClass.getConstructor(new Class[] {Properties.class});
            oaiCatalog.recordFactory =
                (RecordFactory)recordFactoryConstructor.newInstance(new Object[] {properties});
            if (debug) {
                System.out.println("AbstractCatalog.factory: recordFactory=" + oaiCatalog.recordFactory);
            }
            String harvestable = properties.getProperty("AbstractCatalog.harvestable");
            if (harvestable != null && harvestable.equals("false")) {
                oaiCatalog.harvestable = false;
            }
            String secondsToLive =
                properties.getProperty("AbstractCatalog.secondsToLive");
            if (secondsToLive != null) {
                oaiCatalog.millisecondsToLive = Integer.parseInt(secondsToLive) * 1000;
            }
            String granularity = properties.getProperty("AbstractCatalog.granularity");
            for (int i = 0; granularity != null && i < VALID_GRANULARITIES.length; ++i) {
                if (granularity.equalsIgnoreCase(VALID_GRANULARITIES[i])) {
                    oaiCatalog.supportedGranularityOffset = i;
                    break;
                }
            }
            if (oaiCatalog.supportedGranularityOffset == -1) {
                oaiCatalog.supportedGranularityOffset = 0;
                System.err.println("AbstractCatalog.factory: Invalid or missing AbstractCatalog.granularity property. Setting value to default: " +
                        VALID_GRANULARITIES[oaiCatalog.supportedGranularityOffset]);
            }
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
        return oaiCatalog;
    }

    /**
     * Allow the database to return some Identify &lt;description&gt; elements
     *
     * @return an XML String fragment containing description elements
     */
    public String getDescriptions() {
        return null;
    }

    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * identifier.
     *
     * @param identifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception IdDoesNotExistException The specified identifier doesn't exist.
     * @exception NoMetadataFormatsException The identifier exists, but no metadataFormats are
     * provided for it.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public abstract Vector getSchemaLocations(String identifier)
    throws IdDoesNotExistException, NoMetadataFormatsException, OAIInternalServerError;

    /**
     * Retrieve a list of Identifiers that satisfy the criteria parameters
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "headers" Map object. The "headers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception BadArgumentException one or more of the arguments are bad.
     * @exception CannotDisseminateFormatException the requested metadataPrefix isn't supported
     * @exception NoItemsMatchException no items fit the criteria
     * @exception NoSetHierarchyException sets aren't defined for this repository
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public abstract Map listIdentifiers(String from, String until, String set, String metadataPrefix)
    throws BadArgumentException, CannotDisseminateFormatException, NoItemsMatchException,
    NoSetHierarchyException, OAIInternalServerError;

    /**
     * Retrieve the next set of Identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "headers" Map object. The "headers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception BadResumptionTokenException The resumptionToken is bad.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public abstract Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError;

    /**
     * Retrieve the specified metadata for the specified identifier
     *
     * @param identifier the OAI identifier
     * @return the String containing the result record.
     * @exception IdDoesNotExistException The specified identifier doesn't exist.
     * @exception CannotDisseminateFormatException The identifier exists, but doesn't support
     * the specified metadataPrefix.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public abstract String getRecord(String identifier, String metadataPrefix)
    throws IdDoesNotExistException, CannotDisseminateFormatException, OAIInternalServerError;

    /**
     * Retrieve the specified metadata for the specified identifier
     *
     * @param identifier the OAI identifier
     * @return the String containing the result record.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @throws CannotDisseminateFormatException
     * @throws IdDoesNotExistException
     * @throws IdDoesNotExistException
     */
    public String getMetadata(String identifier, String metadataPrefix)
    throws OAIInternalServerError, IdDoesNotExistException,
    IdDoesNotExistException, CannotDisseminateFormatException {
        throw new OAIInternalServerError("You need to override AbstractCatalog.getMetadata()");
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception BadArgumentException one or more of the arguments are bad.
     * @exception CannotDisseminateFormatException the requested metadataPrefix isn't supported
     * @exception NoItemsMatchException no items fit the criteria
     * @exception NoSetHierarchyException sets aren't defined for this repository
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listRecords(String from, String until, String set, String metadataPrefix)
    throws BadArgumentException, CannotDisseminateFormatException, NoItemsMatchException,
    NoSetHierarchyException, OAIInternalServerError {
        if (debug) {
            System.out.println("in AbstractCatalog.listRecords");
        }
        Map listIdentifiersMap = listIdentifiers(from, until, set, metadataPrefix);
        String resumptionToken = (String)listIdentifiersMap.get("resumptionToken");
        Iterator identifiers = (Iterator)listIdentifiersMap.get("identifiers");

        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();

        while (identifiers.hasNext()) {
            String identifier = (String)identifiers.next();
            try {
                records.add(getRecord(identifier, metadataPrefix));
            } catch (IdDoesNotExistException e) {
                throw new OAIInternalServerError("GetRecord failed to retrieve identifier '"
                        + identifier + "'");
            }
        }
        listRecordsMap.put("records", records.iterator());
        if (resumptionToken != null) {
            listRecordsMap.put("resumptionToken", resumptionToken);
        }
        return listRecordsMap;
    }

    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception BadResumptionTokenException The resumptionToken is bad.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        Map listIdentifiersMap = listIdentifiers(resumptionToken);
        resumptionToken = (String)listIdentifiersMap.get("resumptionToken");
        Iterator identifiers = (Iterator)listIdentifiersMap.get("identifiers");
        String metadataPrefix = (String)listIdentifiersMap.get("metadataPrefix");

        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();

        while (identifiers.hasNext()) {
            String identifier = (String)identifiers.next();
            try {
                records.add(getRecord(identifier, metadataPrefix));
            } catch (IdDoesNotExistException e) {
                throw new OAIInternalServerError("GetRecord failed to retrieve identifier '"
                        + identifier + "'");
            } catch (CannotDisseminateFormatException e) {
                // someone cheated
                throw new BadResumptionTokenException();
            }
        }
        listRecordsMap.put("records", records.iterator());
        if (resumptionToken != null) {
            listRecordsMap.put("resumptionToken", resumptionToken);
        }
        return listRecordsMap;
    }

    public Map getResumptionMap(String resumptionToken) {
        return getResumptionMap(resumptionToken, -1, -1);
    }

    public Map getResumptionMap(String resumptionToken, int completeListSize, int cursor) {
        Map resumptionMap = null;
        if (resumptionToken != null) {
            resumptionMap = new HashMap();
            resumptionMap.put("resumptionToken", resumptionToken);
            if (millisecondsToLive > 0) {
//              Date now = new Date();
                Date then = new Date((new Date()).getTime() + millisecondsToLive);
                resumptionMap.put("expirationDate", ServerVerb.createResponseDate(then));
            }
            if (completeListSize >= 0) {
                resumptionMap.put("completeListSize", Integer.toString(completeListSize));
            }
            if (cursor >= 0) {
                resumptionMap.put("cursor", Integer.toString(cursor));
            }
        }
        return resumptionMap;
    }

    /**
     * close the repository
     */
    public abstract void close();

    /**
     * Validate the parameter value
     * The default regex pattern can be overridden by setting a AbstractCatalog.paramRegex property
     *
     * @param key
     * @param value
     * @return true if the parameter is valid, false if not
     */
	public boolean isValidParam(String key, String value) {
		if (paramRegex != null && paramRegex.length() > 0) {
			return value.matches(paramRegex);
		}
		return true;
	}
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.util.OAIUtil;

/**
 * ExtendedJDBCOAICatalog is an example of how to implement the AbstractCatalog interface
 * for JDBC. Pattern an implementation of the AbstractCatalog interface after this class
 * to have OAICat work with your JDBC database.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class ExtendedJDBCOAICatalog extends AbstractCatalog {
    private static final boolean debug = true;

    /**
     * SQL identifier query (loaded from properties)
     * \\i -> localIdentifier, \\o -> oaiIdentifier
     */
    private String identifierQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until
     */
    private String rangeQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until, \\s -> set
     */
    private String rangeSetQuery = null;

    /**
     * SQL query to get a list of available sets
     */
    private String setQuery = null;

    /**
     * SQL query to get a list of available sets that apply to a particular identifier
     */
    private String setSpecQuery = null;

    /**
     * SQL query to get a list of available abouts that apply to a particular identifier
     */
    private String aboutQuery = null;

    /**
     * SQL column labels containing the values of particular interest
     */
    private String aboutValueLabel = null;
    private String setSpecItemLabel = null;
    private String setSpecListLabel = null;
    private String setNameLabel = null;
    private String setDescriptionLabel = null;

    /**
     * maximum number of entries to return for ListRecords and ListIdentifiers
     * (loaded from properties)
     */
    private int maxListSize;

    /**
     * Set Strings to be loaded from the properties file
     * (if they are to be loaded from properties rather than queried from the database)
     */
    ArrayList sets = new ArrayList();

    /**
     * The JDBC Connection
     */
    private boolean isPersistentConnection = true;
    private Connection persistentConnection;
    private String jdbcURL = null;
    private String jdbcLogin = null;
    private String jdbcPasswd = null;

    /**
     * pending resumption tokens
     */
    private HashMap resumptionResults = new HashMap();

    /**
     * Construct a ExtendedJDBCOAICatalog object
     *
     * @param properties a properties object containing initialization parameters
     * @exception IOException an I/O error occurred during database initialization.
     */
    public ExtendedJDBCOAICatalog(Properties properties) throws IOException {
        String maxListSize = properties.getProperty("ExtendedJDBCOAICatalog.maxListSize");
        if (maxListSize == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.maxListSize is missing from the properties file");
        } else {
            this.maxListSize = Integer.parseInt(maxListSize);
        }

        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
        if (paramRegex != null) {
            setParamRegex(paramRegex);
        }
        String jdbcDriverName = properties.getProperty("ExtendedJDBCOAICatalog.jdbcDriverName");
        if (jdbcDriverName == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcDriverName is missing from the properties file");
        }
        jdbcURL = properties.getProperty("ExtendedJDBCOAICatalog.jdbcURL");
        if (jdbcURL == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcURL is missing from the properties file");
        }

        jdbcLogin = properties.getProperty("ExtendedJDBCOAICatalog.jdbcLogin");
        if (jdbcLogin == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcLogin is missing from the properties file");
        }

        jdbcPasswd = properties.getProperty("ExtendedJDBCOAICatalog.jdbcPasswd");
        if (jdbcPasswd == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcPasswd is missing from the properties file");
        }

        rangeQuery = properties.getProperty("ExtendedJDBCOAICatalog.rangeQuery");
        if (rangeQuery == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.rangeQuery is missing from the properties file");
        }

        rangeSetQuery = properties.getProperty("ExtendedJDBCOAICatalog.rangeSetQuery");
        if (rangeSetQuery == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.rangeSetQuery is missing from the properties file");
        }

        identifierQuery = properties.getProperty("ExtendedJDBCOAICatalog.identifierQuery");
        if (identifierQuery == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.identifierQuery is missing from the properties file");
        }

        aboutQuery = properties.getProperty("ExtendedJDBCOAICatalog.aboutQuery");
        if (aboutQuery != null) {
            aboutValueLabel = properties.getProperty("ExtendedJDBCOAICatalog.aboutValueLabel");
            if (aboutValueLabel == null) {
                throw new IllegalArgumentException("ExtendedJDBCOAICatalog.aboutValueLabel is missing from the properties file");
            }
        }

        setSpecQuery = properties.getProperty("ExtendedJDBCOAICatalog.setSpecQuery");
        setSpecItemLabel = properties.getProperty("ExtendedJDBCOAICatalog.setSpecItemLabel");
        if (setSpecItemLabel == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setSpecItemLabel is missing from the properties file");
        }
        setSpecListLabel = properties.getProperty("ExtendedJDBCOAICatalog.setSpecListLabel");
        if (setSpecListLabel == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setSpecListLabel is missing from the properties file");
        }
        setNameLabel = properties.getProperty("ExtendedJDBCOAICatalog.setNameLabel");
        if (setNameLabel == null) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setNameLabel is missing from the properties file");
        }
        setDescriptionLabel = properties.getProperty("ExtendedJDBCOAICatalog.setDescriptionLabel");
//      if (setDescriptionLabel == null) {
//      throw new IllegalArgumentException("ExtendedJDBCOAICatalog.setDescriptionLabel is missing from the properties file");
//      }

        // See if a setQuery exists
        setQuery = properties.getProperty("ExtendedJDBCOAICatalog.setQuery");
        if (setQuery == null) {
            // if not, load the set Strings from the properties file (if present)
            String propertyPrefix = "Sets.";
            Enumeration propNames = properties.propertyNames();
            while (propNames.hasMoreElements()) {
                String propertyName = (String)propNames.nextElement();
                if (propertyName.startsWith(propertyPrefix)) {
                    sets.add(properties.get(propertyName));
                }
            }
        }

        String temp = properties.getProperty("ExtendedJDBCOAICatalog.isPersistentConnection");
        if ("false".equalsIgnoreCase(temp))
            isPersistentConnection = false;

        // open the connection
        try {
            Class.forName(jdbcDriverName);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("ExtendedJDBCOAICatalog.jdbcDriverName is invalid: "
                    + jdbcDriverName);
        }

        if (isPersistentConnection) {
            try {
                persistentConnection = getNewConnection();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new IOException(e.getMessage());
            }
        }
    }

    private Connection getNewConnection() throws SQLException {
        // open the connection
        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
    }

    private Connection startConnection() throws SQLException {
        if (persistentConnection != null) {
            if (persistentConnection.isClosed())
                persistentConnection = getNewConnection();
            return persistentConnection;
        } else {
            return getNewConnection();
        }
    }

    private void endConnection(Connection con) throws OAIInternalServerError {
        try {
            if (persistentConnection == null)
                con.close();
        } catch (SQLException e) {
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
     * but the item is flagged as deleted and thus no schemaLocations (i.e.
     * metadataFormats) can be produced.
     */
    public Vector getSchemaLocations(String oaiIdentifier)
    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
        Connection con = null;
        try {
            con = startConnection();
            Statement stmt = con.createStatement();
            ResultSet rs =
                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
            /*
             * Let your recordFactory decide which schemaLocations
             * (i.e. metadataFormats) it can produce from the record.
             * Doing so will preserve the separation of database access
             * (which happens here) from the record content interpretation
             * (which is the responsibility of the RecordFactory implementation).
             */
            if (!rs.next()) {
                stmt.close();
                endConnection(con);
                throw new IdDoesNotExistException(oaiIdentifier);
            } else {
                /* Make sure the identifierQuery returns the columns you need
                 * (if any) to determine the supported schemaLocations for this item */
                HashMap nativeItem = new HashMap();
                nativeItem.put("coreResult", getColumnValues(rs));
                stmt.close();
                endConnection(con);
                return getRecordFactory().getSchemaLocations(nativeItem);
            }
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Get remainder of the nativeItem. By default, it is assumed
     * the entire nativeItem is produced by the default query.
     * If other tables must be consulted, extend this class and
     * override this method. For best results, create one new entry
     * in the nativeItem HashMap for each additional query peformed.
     */
    protected void extendItem(Connection con, HashMap nativeItem) {
    }

    /**
     * Since the columns should only be read once, copy them into a
     * HashMap and consider that to be the "record"
     * @param rs The ResultSet row
     * @return a HashMap mapping column names with values
     */
    private HashMap getColumnValues(ResultSet rs)
    throws SQLException {
        ResultSetMetaData mdata = rs.getMetaData();
        int count = mdata.getColumnCount();
        HashMap tableItems = new HashMap(count);
        for (int i=1; i<=count; ++i) {
            String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
            tableItems.put(fieldName, rs.getObject(i));
            if (debug) System.out.println(fieldName + "=" + tableItems.get(fieldName));
//          if (debug) {
//          System.out.println("ExtendedJDBCOAICatalog.getColumnValues");
//          System.out.println("catalogName=" + mdata.getCatalogName(i));
//          System.out.println("columnLabel=" + mdata.getColumnLabel(i));
//          System.out.println("columnName=" + mdata.getColumnName(i));
//          System.out.println("schemaName=" + mdata.getSchemaName(i));
//          System.out.println("tableName=" + mdata.getTableName(i));
//          }
        }
        return tableItems;
    }

    /**
     * insert actual from, until, and set parameters into the rangeQuery String
     * NOTE! This retrieves an extra record so we can decide if EOF has been reached.
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateRangeQuery(String from, String until, String set, int offset, int count)
    throws OAIInternalServerError {
        StringBuffer sb = new StringBuffer();
        StringTokenizer tokenizer;
        if (set == null || set.length() == 0)
            tokenizer = new StringTokenizer(rangeQuery, "\\");
        else
            tokenizer = new StringTokenizer(rangeSetQuery, "\\");

        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid query");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'a':
                sb.append(Integer.toString(offset));
                break;
            case 'b':
//              sb.append(Integer.toString(count));
                sb.append(Integer.toString(count+1)); // grab an extra record to decide if EOF
                break;
            case 'f':  // what are the chances someone would use this to indicate a form feed?
                sb.append(formatFromDate(from));
                break;
            case 'u':
                sb.append(formatUntilDate(until));
                break;
            case 's':
                sb.append(set);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatFromDate(String date) {
        return formatDate(date);
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatUntilDate(String date) {
        return formatDate(date);
    }

    /**
     * Change the String from UTC to SQL format
     * If this method doesn't suit your needs, extend this class and override
     * the method rather than change this code directly.
     */
    protected String formatDate(String date) {
        StringBuffer sb = new StringBuffer();
        sb.append(date.substring(5, 7));
        sb.append("/");
        sb.append(date.substring(8));
        sb.append("/");
        sb.append(date.substring(0, 4));
        if (debug) System.out.println("ExtendedJDBCOAICatalog.formatDate: from " + date + " to " + sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateIdentifierQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateSetSpecQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateAboutQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Retrieve a list of identifiers that satisfy the specified criteria
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * It may seem strange for the map to include both "headers" and "identifiers"
     * since the identifiers can be obtained from the headers. This may be true, but
     * AbstractCatalog.listRecords() can operate quicker if it doesn't
     * need to parse identifiers from the XML headers itself. Better
     * still, do like I do below and override AbstractCatalog.listRecords().
     * AbstractCatalog.listRecords() is relatively inefficient because given the list
     * of identifiers, it must call getRecord() individually for each as it constructs
     * its response. It's much more efficient to construct the entire response in one fell
     * swoop by overriding listRecords() as I've done here.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
    throws NoItemsMatchException, OAIInternalServerError {
//      purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        Connection con = null;

        try {
            con = startConnection();
            /* Get some records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
//          rs.last();
//          int numRows = rs.getRow();
//          rs.beforeFirst();
            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count=0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = new HashMap();
                nativeItem.put("coreResult", getColumnValues(rs));
                /* Use the RecordFactory to extract header/identifier pairs for each item */
//                RecordFactory rf = getRecordFactory();
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            if (count == 0) {
                stmt.close();
                endConnection(con);
                throw new NoItemsMatchException();
            }

            /* decide if you're done */
//          if (count == maxListSize) {
            if (rs.next()) {
//              String resumptionId = getResumptionId();
//              resumptionResults.put(resumptionId, rs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append("!");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append("!");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        0));
                //          listIdentifiersMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
            }
            stmt.close();
            endConnection(con);
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
//      purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();

        /****************************
         * parse your resumptionToken
         ****************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
//      String resumptionId;
        String from;
        String until;
        String set;
        int oldCount;
        String metadataPrefix;
//      int numRows;
        try {
//          resumptionId = tokenizer.nextToken();
            from = tokenizer.nextToken();
            until = tokenizer.nextToken();
            set = tokenizer.nextToken();
            if (set.equals("."))
                set = null;
            oldCount = Integer.parseInt(tokenizer.nextToken());
//          numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            if (debug) {
                System.out.println("ExtendedJDBCOAICatalog.listIdentifiers: set=>"
                        + set + "<");
            }
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        Connection con = null;
        try {
            con = startConnection();
            /* Get some more records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
                    oldCount, maxListSize));
            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = new HashMap();
                nativeItem.put("coreResult", getColumnValues(rs));
                /* Use the RecordFactory to extract header/identifier pairs for each item */
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            /* decide if you're done. */
//          if (count == maxListSize) {
            if (rs.next()) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append("!");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append("!");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        oldCount));
                //          listIdentifiersMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
            }
            stmt.close();
            endConnection(con);
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param oaiIdentifier the OAI identifier
     * @param metadataPrefix the OAI metadataPrefix
     * @return the <record/> portion of the XML response.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix is not
     * supported by the item.
     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
    throws OAIInternalServerError, CannotDisseminateFormatException,
    IdDoesNotExistException {
        Connection con = null;
        try {
            con = startConnection();
            Statement stmt = con.createStatement();
            ResultSet rs =
                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
            if (!rs.next()) {
                endConnection(con);
                throw new IdDoesNotExistException(oaiIdentifier);
            }
            HashMap nativeItem = new HashMap();
            nativeItem.put("coreResult", getColumnValues(rs));
            extendItem(con, nativeItem);
            stmt.close();
            endConnection(con);
            return constructRecord(nativeItem, metadataPrefix);
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria. Note, though,
     * that unlike the other OAI verb type methods implemented here, both of the
     * listRecords methods are already implemented in AbstractCatalog rather than
     * abstracted. This is because it is possible to implement ListRecords as a
     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
     * I suggest that you override both the AbstractCatalog.listRecords methods
     * here since it will probably improve the performance if you create the
     * response in one fell swoop rather than construct it one GetRecord at a time.
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for a "records" Iterator object
     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix isn't
     * supported by the item.
     */
    public Map listRecords(String from, String until, String set, String metadataPrefix)
    throws CannotDisseminateFormatException, NoItemsMatchException,
    OAIInternalServerError {
//      purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        Connection con = null;

        try {
            con = startConnection();
            /* Get some records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
//          rs.last();
//          int numRows = rs.getRow();
//          rs.beforeFirst();
            int count;

//          if (debug) System.out.println("ExtendedJDBCOAICatalog.listRecords: numRows=" + numRows);

            /* load the records ArrayList */
            for (count=0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = new HashMap();
                nativeItem.put("coreResult", getColumnValues(rs));
                extendItem(con, nativeItem);
                String record = constructRecord(nativeItem, metadataPrefix);
                records.add(record);
            }

            if (count == 0) {
                endConnection(con);
                throw new NoItemsMatchException();
            }

            /* decide if you're done */
//          if (count == maxListSize) {
            if (rs.next()) {
//              String resumptionId = getResumptionId();
//              resumptionResults.put(resumptionId, rs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append("!");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append("!");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append("!");
//              resumptionTokenSb.append(Integer.toString(numRows));
//              resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        0));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
            }
            stmt.close();
            endConnection(con);
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception BadResumptionTokenException the value of the resumptionToken argument
     * is invalid or expired.
     */
    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
//      purge(); // clean out old resumptionTokens

        /****************************
         * parse your resumptionToken
         ****************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
//      String resumptionId;
        String from;
        String until;
        String set;
        int oldCount;
//      int numRows;
        String metadataPrefix;
        try {
//          resumptionId = tokenizer.nextToken();
            from = tokenizer.nextToken();
            until = tokenizer.nextToken();
            set = tokenizer.nextToken();
            if (set.equals("."))
                set = null;
            oldCount = Integer.parseInt(tokenizer.nextToken());
//          numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        Connection con = null;
        try {
            con = startConnection();
            /* Get some more records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
                    oldCount, maxListSize));

            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && rs.next(); ++count) {
                try {
                    HashMap nativeItem = new HashMap();
                    nativeItem.put("coreResult", getColumnValues(rs));
                    extendItem(con, nativeItem);
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                } catch (CannotDisseminateFormatException e) {
                    /* the client hacked the resumptionToken beyond repair */
                    endConnection(con);
                    throw new BadResumptionTokenException();
                }
            }

            /* decide if you're done */
//          if (count == maxListSize) {
            if (rs.next()) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append("!");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append("!");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append("!");
//              resumptionTokenSb.append(Integer.toString(numRows));
//              resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        oldCount));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
            }
            stmt.close();
            endConnection(con);
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(HashMap nativeItem, String metadataPrefix)
    throws CannotDisseminateFormatException, OAIInternalServerError {
        String schemaURL = null;
        Iterator setSpecs = getSetSpecs(nativeItem);
        Iterator abouts = getAbouts(nativeItem);

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * Retrieve a list of sets that satisfy the specified criteria
     *
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception OAIBadRequestException signals an http status code 400 problem
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
        if (setQuery == null) {
            if (sets.size() == 0)
                throw new NoSetHierarchyException();
            Map listSetsMap = new HashMap();
            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            Connection con = null;
            try {
                if (debug) System.out.println(setQuery);

                con = startConnection();
                /* Get some records from your database */
                Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
                ResultSet rs = stmt.executeQuery(setQuery);
                rs.last();
                int numRows = rs.getRow();
                rs.beforeFirst();
                int count;

//              if (debug) System.out.println("ExtendedJDBCOAICatalog.listSets: numRows=" + numRows);

                /* load the sets ArrayLists. */
                for (count=0; count < maxListSize && rs.next(); ++count) {
                    /* Use the RecordFactory to extract header/set pairs for each item */
                    HashMap nativeItem = getColumnValues(rs);
                    sets.add(getSetXML(nativeItem));
                    if (debug) System.out.println("ExtendedJDBCOAICatalog.listSets: adding an entry");
                }

                /* decide if you're done */
                if (count < numRows) {
                    String resumptionId = getResumptionId();
                    resumptionResults.put(resumptionId, rs);

                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(count));
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            0));
                    //          listSetsMap.put("resumptionMap",
                    //                                 getResumptionMap(resumptionTokenSb.toString()));
                }
                stmt.close();
                endConnection(con);
            } catch (SQLException e) {
                if (con != null)
                    endConnection(con);
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * Retrieve the next set of sets associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listSets() Map result.
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets(String resumptionToken)
    throws OAIInternalServerError, BadResumptionTokenException {
        if (setQuery == null) {
            throw new BadResumptionTokenException();
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            /****************************
             * parse your resumptionToken
             ****************************/
            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
            String resumptionId;
            int oldCount;
            int numRows;
            try {
                resumptionId = tokenizer.nextToken();
                oldCount = Integer.parseInt(tokenizer.nextToken());
                numRows = Integer.parseInt(tokenizer.nextToken());
            } catch (NoSuchElementException e) {
                throw new BadResumptionTokenException();
            }

            try {
                /* Get some more records from your database */
                ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
                if (rs == null) {
                    throw new BadResumptionTokenException();
                }

                if (rs.getRow() != oldCount) {
//                  System.out.println("ExtendedJDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                    rs.absolute(oldCount);
                }

                int count;

                /* load the sets ArrayLists. */
                for (count = 0; count < maxListSize && rs.next(); ++count) {
                    HashMap nativeItem = getColumnValues(rs);
                    /* Use the RecordFactory to extract set for each item */
                    sets.add(getSetXML(nativeItem));
                }

                /* decide if you're done. */
                if (oldCount+count < numRows) {
                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(oldCount + count));
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            oldCount));
                    //          listSetsMap.put("resumptionMap",
                    //                                 getResumptionMap(resumptionTokenSb.toString()));
                }
            } catch (SQLException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(HashMap nativeItem)
    throws OAIInternalServerError {
        Connection con = null;
        try {
            ArrayList setSpecs = new ArrayList();
            if (setSpecQuery != null) {
                con = startConnection();
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                Statement stmt = con.createStatement();
                ResultSet rs = stmt.executeQuery(populateSetSpecQuery(oaiIdentifier));
                while (rs.next()) {
                    HashMap setMap = getColumnValues(rs);
                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
                }
                stmt.close();
                endConnection(con);
            }
            return setSpecs.iterator();
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(HashMap nativeItem)
    throws OAIInternalServerError {
        Connection con = null;
        try {
            ArrayList abouts = new ArrayList();
            if (aboutQuery != null) {
                con = startConnection();
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                Statement stmt = con.createStatement();
                ResultSet rs = stmt.executeQuery(populateAboutQuery(oaiIdentifier));
                while (rs.next()) {
                    HashMap aboutMap = getColumnValues(rs);
                    abouts.add(aboutMap.get(aboutValueLabel));
                }
                stmt.close();
                endConnection(con);
            }
            return abouts.iterator();
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Extract &lt;set&gt; XML string from setItem object
     *
     * @param setItem individual set instance in native format
     * @return an XML String containing the XML &lt;set&gt; content
     */
    public String getSetXML(HashMap setItem)
    throws IllegalArgumentException {
//      try {
        String setSpec = getSetSpec(setItem);
        String setName = getSetName(setItem);
        String setDescription = getSetDescription(setItem);

        StringBuffer sb = new StringBuffer();
        sb.append("<set>");
        sb.append("<setSpec>");
        sb.append(OAIUtil.xmlEncode(setSpec));
        sb.append("</setSpec>");
        sb.append("<setName>");
        sb.append(OAIUtil.xmlEncode(setName));
        sb.append("</setName>");
        if (setDescription != null) {
            sb.append("<setDescription>");
            sb.append(OAIUtil.xmlEncode(setDescription));
            sb.append("</setDescription>");
        }
        sb.append("</set>");
        return sb.toString();
//      } catch (SQLException e) {
//      e.printStackTrace();
//      throw new IllegalArgumentException(e.getMessage());
//      }
    }

    /**
     * get the setSpec XML string. Extend this class and override this method
     * if the setSpec can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setSpec&gt; content
     */
    protected String getSetSpec(HashMap setItem) {
        try {
            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return "UnsupportedEncodingException";
        }
    }

    /**
     * get the setName XML string. Extend this class and override this method
     * if the setName can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setName&gt; content
     */
    protected String getSetName(HashMap setItem) {
        return (String)setItem.get(setNameLabel);
    }

    /**
     * get the setDescription XML string. Extend this class and override this method
     * if the setDescription can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setDescription&gt; content
     */
    protected String getSetDescription(HashMap setItem) {
        if (setDescriptionLabel == null)
            return null;
        return (String)setItem.get(setDescriptionLabel);
    }

    /**
     * close the repository
     */
    public void close() {
        try {
            if (persistentConnection != null)
                persistentConnection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        persistentConnection = null;
    }

    /**
     * Purge tokens that are older than the configured time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date now = new Date();
        Iterator keySet = resumptionResults.keySet().iterator();
        while (keySet.hasNext()) {
            String key = (String)keySet.next();
            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            String key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }

    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a String version of the current time
     */
    private synchronized static String getResumptionId() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * ExtendedJDBCRecordFactory converts JDBC "items" to "record" Strings.
 */
public class ExtendedJDBCRecordFactory extends RecordFactory {
    private String repositoryIdentifier = null;
    protected String identifierLabel = null;
    protected String datestampLabel = null;
    
    /**
     * Construct an ExtendedJDBCRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public ExtendedJDBCRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
	repositoryIdentifier = properties.getProperty("ExtendedJDBCRecordFactory.repositoryIdentifier");
	if (repositoryIdentifier == null) {
	    throw new IllegalArgumentException("ExtendedJDBCRecordFactory.repositoryIdentifier is missing from the properties file");
	}
	identifierLabel = properties.getProperty("ExtendedJDBCRecordFactory.identifierLabel");
	if (identifierLabel == null) {
	    throw new IllegalArgumentException("ExtendedJDBCRecordFactory.identifierLabel is missing from the properties file");
	}
	datestampLabel = properties.getProperty("ExtendedJDBCRecordFactory.datestampLabel");
	if (datestampLabel == null) {
	    throw new IllegalArgumentException("ExtendedJDBCRecordFactory.datestampLabel is missing from the properties file");
	}
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param oaiIdentifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String oaiIdentifier) {
	StringTokenizer tokenizer = new StringTokenizer(oaiIdentifier, ":");
	try {
	    tokenizer.nextToken();
	    tokenizer.nextToken();
	    return tokenizer.nextToken();
	} catch (java.util.NoSuchElementException e) {
	    return null;
	}
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
// 	throws IllegalArgumentException {
// 	try {
	    HashMap table = (HashMap)((HashMap)nativeItem).get("coreResult");
	    return table.get(identifierLabel).toString();
// 	} catch (SQLException e) {
// 	    e.printStackTrace();
// 	    throw new IllegalArgumentException(e.getMessage());
// 	}
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem)
	throws IllegalArgumentException {
	StringBuffer sb = new StringBuffer();
	sb.append("oai:");
	sb.append(repositoryIdentifier);
	sb.append(":");
	sb.append(getLocalIdentifier(nativeItem));
	return sb.toString();
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem) {
// 	throws IllegalArgumentException  {
// 	try {
	    HashMap table = (HashMap)((HashMap)nativeItem).get("coreResult");
	    return ((Timestamp)table.get(datestampLabel)).toString().substring(0, 10);
// 	} catch (SQLException e) {
// 	    e.printStackTrace();
// 	    throw new IllegalArgumentException(e.getMessage());
// 	}
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item. Null if setSpecs aren't
     * derived from the nativeItem.
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
	return null;
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item. Null if
     * abouts aren't derived from the nativeItem
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
	// Don't perform quick creates
	return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * FileRecordFactory converts native XML "items" to "record" Strings.
 * This factory assumes the native XML item looks exactly like the <record>
 * element of an OAI GetRecord response, with the possible exception that the
 * <metadata> element contains multiple metadataFormats from which to choose.
 */
public class FileRecordFactory extends RecordFactory {
    private String repositoryIdentifier = null;
    
    /**
     * Construct an FileRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public FileRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
	repositoryIdentifier = properties.getProperty("FileRecordFactory.repositoryIdentifier");
	if (repositoryIdentifier == null) {
	    throw new IllegalArgumentException("FileRecordFactory.repositoryIdentifier is missing from the properties file");
	}
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
        try {
            StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
            tokenizer.nextToken();
            tokenizer.nextToken();
            return tokenizer.nextToken();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
	StringBuffer sb = new StringBuffer();
	sb.append("oai:");
	sb.append(repositoryIdentifier);
	sb.append(":");
	sb.append(getLocalIdentifier(nativeItem));
	return sb.toString();
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
        return (String)((HashMap)nativeItem).get("localIdentifier");
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException  {
        return (String)((HashMap)nativeItem).get("lastModified");
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
	return null;
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
	// Don't perform quick creates
	return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
// import ORG.oclc.oai.util.*;
// import org.xml.sax.*;


/**
 * FileSystemOAICatalog is an implementation of AbstractCatalog interface
 * with the data sitting in a directory on a filesystem.
 *
 * @author Ralph LeVan, OCLC Online Computer Library Center
 */

public class FileSystemOAICatalog extends AbstractCatalog {
    static final boolean debug=false;

    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
    protected String           homeDir;
    private HashMap          fileDateMap = new HashMap();
    private HashMap          resumptionResults=new HashMap();
    private int              maxListSize;
    private boolean hideExtension = false;

    public FileSystemOAICatalog(Properties properties) {
        String          temp;

        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
        temp=properties.getProperty("FileSystemOAICatalog.maxListSize");
        if (temp==null)
            throw new IllegalArgumentException("FileSystemOAICatalog."+
                "maxListSize is missing from the properties file");
        maxListSize = Integer.parseInt(temp);
        if(debug)
            System.out.println("in FileSystemOAICatalog(): maxListSize="+
                maxListSize);

	hideExtension = "true".equalsIgnoreCase(properties.getProperty("FileSystemOAICatalog.hideExtension"));

        homeDir=properties.getProperty("FileSystemOAICatalog.homeDir");
        if (homeDir==null)
            throw new IllegalArgumentException("FileSystemOAICatalog."+
                "homeDir is missing from the properties file");
        if(debug)
            System.out.println("in FileSystemOAICatalog(): homeDir="+
                homeDir);


	File homeFile = new File(homeDir);
	int homeDirLen = homeFile.getPath().length()+1;
	loadFileMap(homeDirLen, homeFile);
// 	Iterator iterator = fileDateMap.entrySet().iterator();
// 	while (iterator.hasNext()) {
// 	    Map.Entry entry = (Map.Entry)iterator.next();
// 	    System.out.println(entry.getKey() + ":" + entry.getValue());
// 	}
    }

    private void loadFileMap(int homeDirLen, File currentDir) {
	String[] list = currentDir.list();
	for (int i=0; i<list.length; ++i) {
	    File child = new File(currentDir, list[i]);
	    if (child.isDirectory()) {
		loadFileMap(homeDirLen, child);
	    } else if (isMetadataFile(child)) {
		String localIdentifier = file2LocalIdentifier(homeDirLen, child);
                String datestamp = date2OAIDatestamp(new Date(child.lastModified()));
		fileDateMap.put(localIdentifier, datestamp);
	    }
	}
    }

    /**
     * Override this method if some files exist in the
     * filesystem that aren't metadata records.
     *
     * @param child the File to be investigated
     * @return true if it contains metadata, false otherwise
     */
    protected boolean isMetadataFile(File child) {
        return true;
    }

    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param homeDirLen the length of the home directory path
     * @param file the File object containing the native record
     * @return localIdentifier
     */
    protected String file2LocalIdentifier(int homeDirLen, File file) {
	String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
	if (hideExtension && fileName.endsWith(".xml")) {
	    fileName = fileName.substring(0, fileName.lastIndexOf(".xml"));
	}
	return fileName;
    }

    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
     * @return the File object containing the native record
     */
    protected File localIdentifier2File(String localIdentifier) {
        String fileName = localIdentifier.replace('/', File.separatorChar);
	if (hideExtension) {
	    fileName = fileName + ".xml";
	}
	return new File(homeDir, fileName);
    }

    private String date2OAIDatestamp(Date date) {
        return dateFormatter.format(date);
    }

    private HashMap getNativeHeader(String localIdentifier) {
        HashMap recordMap = null;
        if (fileDateMap.containsKey(localIdentifier)) {
            recordMap = new HashMap();
            recordMap.put("localIdentifier", localIdentifier);
            recordMap.put("lastModified", fileDateMap.get(localIdentifier));
            return recordMap;
        }
        return recordMap;
    }
    
    private HashMap getNativeRecord(String localIdentifier)
        throws IOException {
        HashMap recordMap = getNativeHeader(localIdentifier);
        if (recordMap == null) {
            return null;
        } else {
            File file = localIdentifier2File(localIdentifier);
            try {
                FileInputStream fis = new FileInputStream(file);
                BufferedInputStream bis = new BufferedInputStream(fis);
                byte[] buffer = new byte[(int)file.length()];
                bis.read(buffer, 0, (int)file.length());
                recordMap.put("recordBytes", buffer);
                return recordMap;
            } catch (FileNotFoundException e) {
                return null;
            }
        }
    }
    
    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param     oaiIdentifier the OAI identifier
     * @param     metadataPrefix the OAI metadataPrefix
     * @return    the Record object containing the result.
     * @exception CannotDisseminateFormatException signals an http status
     *                code 400 problem
     * @exception IdDoesNotExistException signals an http status code 404
     *                problem
     * @exception OAIInternalServerError signals an http status code 500
     *                problem
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
        throws IdDoesNotExistException, CannotDisseminateFormatException,
               OAIInternalServerError {
        HashMap nativeItem = null;
        try {
	    String localIdentifier
		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);

            nativeItem = getNativeRecord(localIdentifier);
            if (nativeItem == null)
                throw new IdDoesNotExistException(oaiIdentifier);
            return constructRecord(nativeItem, metadataPrefix);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
    }


    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * We get passed the ID for a record and are supposed to return a list
     * of the formats that we can deliver the record in.  Since we are assuming
     * that all the records in the directory have the same format, the
     * response to this is static;
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAINotFoundException signals an http status code 404 problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Vector getSchemaLocations(String oaiIdentifier)
      throws IdDoesNotExistException, OAIInternalServerError, NoMetadataFormatsException {
        HashMap nativeItem = null;
        try {
	    String localIdentifier
		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
            nativeItem = getNativeRecord(localIdentifier);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
        
        if (nativeItem != null) {
            return getRecordFactory().getSchemaLocations(nativeItem);
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }


    /**
     * Retrieve a list of Identifiers that satisfy the criteria parameters
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
        throws NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
	Iterator iterator = fileDateMap.entrySet().iterator();
	int numRows = fileDateMap.entrySet().size();
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            if (fileDate.compareTo(from) >= 0
                && fileDate.compareTo(until) <= 0) {
                HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
                String[] header = getRecordFactory().createHeader(nativeHeader);
                headers.add(header[0]);
                identifiers.add(header[1]);
                count++;
            }
	}

        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listIdentifiersMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listIdentifiersMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of Identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String resumptionToken)
      throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("FileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
	    iterator = fileDateMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the headers and identifiers ArrayLists. */
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
	    HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
		String[] header = getRecordFactory().createHeader(nativeHeader);
		headers.add(header[0]);
		identifiers.add(header[1]);
		count++;
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listIdentifiersMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }


    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(HashMap nativeItem, String metadataPrefix)
        throws CannotDisseminateFormatException {
        String schemaURL = null;
	Iterator setSpecs = getSetSpecs(nativeItem);
	Iterator abouts = getAbouts(nativeItem);

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(HashMap nativeItem) {
        return null;
    }

    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(HashMap nativeItem) {
        return null;
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @param metadataPrefix the OAI metadataPrefix
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listRecords(String from, String until, String set,
                                    String metadataPrefix)
      throws CannotDisseminateFormatException,
      OAIInternalServerError, NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
	Iterator iterator = fileDateMap.entrySet().iterator();
	int numRows = fileDateMap.entrySet().size();
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            if (fileDate.compareTo(from) >= 0
                && fileDate.compareTo(until) <= 0) {
                try {
                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                    count++;
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            }
	}
        
        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listRecordsMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listRecordsMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listRecords(String resumptionToken)
      throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("FileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
	    iterator = fileDateMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the records ArrayLists. */
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
	    try {
                HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
                String record = constructRecord(nativeItem, metadataPrefix);
                records.add(record);
                count++;
            } catch (CannotDisseminateFormatException e) {
                /* the client hacked the resumptionToken beyond repair */
                throw new BadResumptionTokenException();
            } catch (IOException e) {
                /* the file is probably missing */
                throw new BadResumptionTokenException();
	    }
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listRecordsMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    public Map listSets() throws NoSetHierarchyException {
	throw new NoSetHierarchyException();
//         Map listSetsMap = new HashMap();
//         listSetsMap.put("sets", setsList.iterator());
//         return listSetsMap;
    }


    public Map listSets(String resumptionToken)
      throws BadResumptionTokenException {
	throw new BadResumptionTokenException();
    }


    /**
     * close the repository
     */
    public void close() { }


    /**
     * Purge tokens that are older than the time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date      then, now = new Date();
        Iterator  keySet = resumptionResults.keySet().iterator();
        String    key;

        while (keySet.hasNext()) {
            key=(String)keySet.next();
            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }


    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a long integer version of the current time
     */
    private synchronized static String getRSName() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;

import javax.servlet.ServletContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.util.OAIUtil;


/**
 * NewFileSystemOAICatalog is an implementation of AbstractCatalog interface
 * with the data sitting in a directory on a filesystem.
 *
 * @author Jeff Young, OCLC Online Computer Library Center
 */

public class FolderOAICatalog extends AbstractCatalog {
    static final boolean debug=false;
    
    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
    protected String           homeDir;
    private HashMap          datestampMap = new HashMap();
    private HashMap identifierMap = new HashMap();
    private HashMap setMap = new HashMap();
    private HashMap          resumptionResults=new HashMap();
    private int              maxListSize;
    private ArrayList sets = null;
    private ServletContext context = null;
    
    public FolderOAICatalog(Properties properties, ServletContext context)
    throws IOException {
        this.context = context;
        String          temp;
        
        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
        temp=properties.getProperty("NewFileSystemOAICatalog.maxListSize");
        if (temp==null)
            throw new IllegalArgumentException("NewFileSystemOAICatalog."+
            "maxListSize is missing from the properties file");
        maxListSize = Integer.parseInt(temp);
        if(debug)
            System.out.println("in NewFileSystemOAICatalog(): maxListSize="+
                    maxListSize);
        
        homeDir=properties.getProperty("NewFileSystemOAICatalog.homeDir");
        if (homeDir!=null) {
            if(debug)
                System.out.println("in NewFileSystemOAICatalog(): homeDir="+
                        homeDir);
            
            
            File homeFile = new File(homeDir);
            int homeDirLen = homeFile.getPath().length()+1;
            loadFileMap(homeDirLen, homeFile);
        } else {
            /* Try looking in a known location */
            Set resourcePaths = context.getResourcePaths("/WEB-INF/DATA/");
            if (resourcePaths == null || resourcePaths.size() == 0) {
                throw new IllegalArgumentException("NewFileSystemOAICatalog."+
                "homeDir is missing from the properties file");
            }
            String earliestDatestamp = loadFileMap(context, resourcePaths);
            properties.setProperty("Identify.earliestDatestamp", earliestDatestamp);
        }
//      Iterator iterator = datestampMap.entrySet().iterator();
//      while (iterator.hasNext()) {
//      Map.Entry entry = (Map.Entry)iterator.next();
//      System.out.println(entry.getKey() + ":" + entry.getValue());
//      }
        sets = getSets(properties);
    }
    
    private static ArrayList getSets(Properties properties) {
        InputStream is = Thread.currentThread().getContextClassLoader()
        .getResourceAsStream("sets.properties");
        Properties setProps = new Properties();
        try {
            setProps.load(is);
        } catch (Exception e) {
            e.printStackTrace();
        }
        TreeMap treeMap = new TreeMap();
        Enumeration propNames = setProps.propertyNames();
        while (propNames.hasMoreElements()) {
            String propertyName = (String)propNames.nextElement();
            String s = new StringBuffer("<set><setSpec>")
            .append(OAIUtil.xmlEncode(propertyName))
            .append("</setSpec><setName>")
            .append(OAIUtil.xmlEncode((String) setProps.get(propertyName)))
            .append("</setName></set>")
            .toString();
            treeMap.put(propertyName, s);
        }
        return new ArrayList(treeMap.values());
    }
    
    private String loadFileMap(ServletContext context, Set resourcePaths)
    throws IOException {
        String earliestDatestamp = "9999-12-31";
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Iterator iter = resourcePaths.iterator();
            while (iter.hasNext()) {
                String resourcePath = (String)iter.next();
                if (!resourcePath.endsWith("/")) {
                    InputStream is = context.getResourceAsStream(resourcePath);
                    InputSource data = new InputSource(is);
                    Node doc = builder.parse(data);
                    is.close();
                    
//                    Node datestampNode = XPathAPI.selectSingleNode(doc, "/record/header/datestamp");
                    String datestamp = XPathAPI.eval(doc, "/record/header/datestamp").str();
                    if (datestamp.compareTo(earliestDatestamp) < 0) {
                        earliestDatestamp = datestamp;
                    }
                    String identifier = XPathAPI.eval(doc, "/record/header/identifier").str();
                    datestampMap.put(resourcePath,datestamp);
                    identifierMap.put(identifier, resourcePath);
                    NodeList setNodes = XPathAPI.selectNodeList(doc, "/record/header/setSpec");
                    for (int j=0; j<setNodes.getLength(); ++j) {
                        Node setSpecNode = setNodes.item(j);
                        String setSpec = XPathAPI.eval(setSpecNode,
                        "string()").str();
                        ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
                        if (setSpecList == null) {
                            setSpecList = new ArrayList();
                            setMap.put(setSpec, setSpecList);
                        }
                        setSpecList.add(resourcePath);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }
        return earliestDatestamp;
    }
    
    private void loadFileMap(int homeDirLen, File currentDir)
    throws IOException {
        try {
            String[] list = currentDir.list();
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            for (int i=0; i<list.length; ++i) {
                File child = new File(currentDir, list[i]);
                if (child.isDirectory() && !"CVS".equals(child.getName())) {
                    loadFileMap(homeDirLen, child);
                } else if (isMetadataFile(child)) {
                    String path = file2path(homeDirLen, child);
                    System.out.println("parsing " + path);
                    //                 String datestamp = date2OAIDatestamp(new Date(child.lastModified()));
                    File file = localIdentifier2File(path);
                    FileInputStream fis = new FileInputStream(file);
                    InputSource data = new InputSource(fis);
                    Node doc = builder.parse(data);
                    fis.close();
                    
                    Node datestampNode = XPathAPI.selectSingleNode(doc, "/record/header/datestamp");
                    datestampMap.put(path,
                            XPathAPI.eval(datestampNode, "string()").str());
                    NodeList setNodes = XPathAPI.selectNodeList(doc, "/record/header/setSpec");
                    for (int j=0; j<setNodes.getLength(); ++j) {
                        Node setSpecNode = setNodes.item(j);
                        String setSpec = XPathAPI.eval(setSpecNode,
                        "string()").str();
                        ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
                        if (setSpecList == null) {
                            setSpecList = new ArrayList();
                            setMap.put(setSpec, setSpecList);
                        }
                        setSpecList.add(path);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }
    }
    
    /**
     * Override this method if some files exist in the
     * filesystem that aren't metadata records.
     *
     * @param child the File to be investigated
     * @return true if it contains metadata, false otherwise
     */
    protected boolean isMetadataFile(File child) {
        return true;
    }
    
    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param homeDirLen the length of the home directory path
     * @param file the File object containing the native record
     * @return localIdentifier
     */
    protected String file2path(int homeDirLen, File file) {
        String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
        return fileName;
    }
    
    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
     * @return the File object containing the native record
     */
    protected File localIdentifier2File(String localIdentifier) {
        String fileName = localIdentifier.replace('/', File.separatorChar);
        return new File(homeDir, fileName);
    }
    
//  private String date2OAIDatestamp(Date date) {
//  return dateFormatter.format(date);
//  }
    
//    private HashMap getNativeHeader(String path) {
//        HashMap recordMap = null;
//        if (datestampMap.containsKey(path)) {
//            recordMap = new HashMap();
//            recordMap.put("localIdentifier", path.substring(0, path.lastIndexOf(".")));
//            recordMap.put("lastModified", datestampMap.get(path));
//            ArrayList setSpecs = new ArrayList();
//            Iterator keySet = setMap.keySet().iterator();
//            while (keySet.hasNext()) {
//                String key = (String)keySet.next();
//                ArrayList identifierList = (ArrayList)setMap.get(key);
//                if (identifierList.contains(path)) {
//                    setSpecs.add(key);
//                }
//            }
//            recordMap.put("setSpecs", setSpecs.iterator());
//            return recordMap;
//        }
//        return recordMap;
//    }
    
    private Document getNativeRecord(String path) {
        Document nativeRecord = null;
//        HashMap recordMap = null;
        if (datestampMap.containsKey(path)) {
//            recordMap = new HashMap();
//            File file = localIdentifier2File(path);
            try {
                InputStream is = context.getResourceAsStream(path);
//                FileInputStream fis = new FileInputStream(file);
                nativeRecord = OAIUtil.parse(is);
//                Node node = XPathAPI.selectSingleNode(doc, "/record/header");
//                String header = OAIUtil.toString(node);
//                recordMap.put("headerNode", node);
//                FileInputStream fis = new FileInputStream(file);
//                BufferedInputStream bis = new BufferedInputStream(fis);
//                byte[] buffer = new byte[(int)file.length()];
//                bis.read(buffer, 0, (int)file.length());
//                recordMap.put("recordBytes", buffer);
//                bis.close();
//                fis.close();
//                return recordMap;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        return nativeRecord;
    }
    
    private ArrayList getExtensionList(String localIdentifier) {
//      System.out.println("getExtensionList: localIdentifier="
//      + localIdentifier);
        ArrayList list = new ArrayList();
        Iterator iterator = datestampMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry)iterator.next();
//          System.out.println("entry.getKey()=" + entry.getKey());
            if (((String)entry.getKey()).startsWith(localIdentifier)) {
                list.add(((String)entry.getKey()).substring(localIdentifier.length()+1));
            }
        }
        return list;
    }
    
//    private HashMap getNativeRecord(String path)
//    throws IOException {
//        HashMap recordMap = getNativeHeader(path);
//        if (recordMap == null) {
//            return null;
//        } else {
//            File file = localIdentifier2File(path);
//            try {
//                FileInputStream fis = new FileInputStream(file);
//                BufferedInputStream bis = new BufferedInputStream(fis);
//                byte[] buffer = new byte[(int)file.length()];
//                bis.read(buffer, 0, (int)file.length());
//                recordMap.put("recordBytes", buffer);
//                bis.close();
//                fis.close();
//                return recordMap;
//            } catch (FileNotFoundException e) {
//                return null;
//            }
//        }
//    }
    
    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param     oaiIdentifier the OAI identifier
     * @param     metadataPrefix the OAI metadataPrefix
     * @return    the Record object containing the result.
     * @exception CannotDisseminateFormatException signals an http status
     *                code 400 problem
     * @exception IdDoesNotExistException signals an http status code 404
     *                problem
     * @exception OAIInternalServerError signals an http status code 500
     *                problem
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
    throws IdDoesNotExistException, CannotDisseminateFormatException {
        Document nativeItem = null;
//        try {
        String resourcePath = (String) identifierMap.get(oaiIdentifier);
            
            nativeItem = getNativeRecord(resourcePath);
            if (nativeItem == null)
                throw new IdDoesNotExistException(oaiIdentifier);
            return constructRecord(nativeItem, metadataPrefix);
//        } catch (IOException e) {
//            e.printStackTrace();
//            throw new OAIInternalServerError("Database Failure");
//        }
    }
    
    
    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * We get passed the ID for a record and are supposed to return a list
     * of the formats that we can deliver the record in.  Since we are assuming
     * that all the records in the directory have the same format, the
     * response to this is static;
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAINotFoundException signals an http status code 404 problem
     */
    public Vector getSchemaLocations(String oaiIdentifier)
    throws IdDoesNotExistException, NoMetadataFormatsException {
        ArrayList extensionList = null;
//      try {
        String localIdentifier
        = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
        extensionList = getExtensionList(localIdentifier);
//      } catch (IOException e) {
//      e.printStackTrace();
//      throw new OAIInternalServerError("Database Failure");
//      }
        
        if (extensionList != null) {
            return getRecordFactory().getSchemaLocations(extensionList);
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }
    
    
    /**
     * Retrieve a list of Identifiers that satisfy the criteria parameters
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
    throws NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        Iterator iterator = datestampMap.entrySet().iterator();
        int numRows = datestampMap.entrySet().size();
        int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
        while (count < maxListSize && iterator.hasNext()) {
            Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            String path = (String)entryDateMap.getKey();
            if (fileDate.compareTo(from) >= 0
                    && fileDate.compareTo(until) <= 0
                    && (setIdentifiers == null || setIdentifiers.contains(path))) {
                Document nativeRecord = getNativeRecord((String)entryDateMap.getKey());
                String[] header = getRecordFactory().createHeader(nativeRecord);
                headers.add(header[0]);
                identifiers.add(header[1]);
                count++;
            }
        }
        
        if (count == 0)
            throw new NoItemsMatchException();
        
        /* decide if you're done */
        if (iterator.hasNext()) {
            String resumptionId = getRSName();
            resumptionResults.put(resumptionId, iterator);
            
            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(numRows));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(set);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listIdentifiersMap.put("resumptionMap",
                    getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            0));
//          listIdentifiersMap.put("resumptionMap",
//          getResumptionMap(resumptionTokenSb.toString()));
        }
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }
    
    /**
     * Retrieve the next set of Identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
        int numRows;
        String set;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            set = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }
        
        /* Get some more records from your database */
        Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
        if (iterator == null) {
            System.out.println("NewFileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
            iterator = datestampMap.entrySet().iterator();
            for (int i = 0; i<oldCount; ++i)
                iterator.next();
        }
        
        /* load the headers and identifiers ArrayLists. */
        int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
        while (count < maxListSize && iterator.hasNext()) {
            Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String path = (String)entryDateMap.getKey();
            if (setIdentifiers == null || setIdentifiers.contains(path)) {
                Document nativeRecord = getNativeRecord((String)entryDateMap.getKey());
                String[] header = getRecordFactory().createHeader(nativeRecord);
                headers.add(header[0]);
                identifiers.add(header[1]);
                count++;
            }
        }
        
        /* decide if you're done. */
        if (iterator.hasNext()) {
            resumptionId = getRSName();
            resumptionResults.put(resumptionId, iterator);
            
            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(oldCount + count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(numRows));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(set);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                    numRows,
                    oldCount));
            //          listIdentifiersMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSb.toString()));
        }
        
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }
    
    
    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(Document nativeItem, String metadataPrefix)
    throws CannotDisseminateFormatException {
        String schemaURL = null;
        Iterator setSpecs = getSetSpecs(nativeItem);
        Iterator abouts = getAbouts(nativeItem);
        
        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }
    
    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(Document nativeItem) {
        return null;
    }
    
    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(Document nativeItem) {
        return null;
    }
    
    /**
     * Retrieve a list of records that satisfy the specified criteria
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @param metadataPrefix the OAI metadataPrefix
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listRecords(String from, String until, String set,
            String metadataPrefix)
    throws CannotDisseminateFormatException,
    NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        Iterator iterator = datestampMap.entrySet().iterator();
        int numRows = datestampMap.entrySet().size();
        int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
        while (count < maxListSize && iterator.hasNext()) {
            Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            String path = (String)entryDateMap.getKey();
//            String extension = path.substring(path.lastIndexOf(".")+1);
            if (fileDate.compareTo(from) >= 0
                    && fileDate.compareTo(until) <= 0
//                    && extension.equals(metadataPrefix)
                    && (setIdentifiers == null || setIdentifiers.contains(path))) {
//                try {
                    Document nativeItem = getNativeRecord((String)entryDateMap.getKey());
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                    count++;
//                } catch (IOException e) {
//                    e.printStackTrace();
//                    throw new OAIInternalServerError(e.getMessage());
//                }
            }
        }
        
        if (count == 0)
            throw new NoItemsMatchException();
        
        /* decide if you're done */
        if (iterator.hasNext()) {
            String resumptionId = getRSName();
            resumptionResults.put(resumptionId, iterator);
            
            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(numRows));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(set);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listRecordsMap.put("resumptionMap",
                    getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            0));
//          listRecordsMap.put("resumptionMap",
//          getResumptionMap(resumptionTokenSb.toString()));
        }
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }
    
    
    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
        int numRows;
        String set;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            set = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }
        
        /* Get some more records from your database */
        Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
        if (iterator == null) {
            System.out.println("NewFileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
            iterator = datestampMap.entrySet().iterator();
            for (int i = 0; i<oldCount; ++i)
                iterator.next();
        }
        
        /* load the records ArrayLists. */
        int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
        while (count < maxListSize && iterator.hasNext()) {
            Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String path = (String)entryDateMap.getKey();
            if (setIdentifiers == null || setIdentifiers.contains(path)) {
                try {
                    Document nativeItem = getNativeRecord((String)entryDateMap.getKey());
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                    count++;
                } catch (CannotDisseminateFormatException e) {
                    /* the client hacked the resumptionToken beyond repair */
                    throw new BadResumptionTokenException();
//                } catch (IOException e) {
//                    /* the file is probably missing */
//                    throw new BadResumptionTokenException();
                }
            }
        }
        
        /* decide if you're done. */
        if (iterator.hasNext()) {
            resumptionId = getRSName();
            resumptionResults.put(resumptionId, iterator);
            
            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(oldCount + count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(numRows));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(set);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                    numRows,
                    oldCount));
            //          listRecordsMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSb.toString()));
        }
        
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }
    
    
    public Map listSets() throws NoSetHierarchyException {
//      throw new NoSetHierarchyException();
        if (sets.size() == 0)
            throw new NoSetHierarchyException();
        Map listSetsMap = new LinkedHashMap();
        listSetsMap.put("sets", sets.iterator());
        return listSetsMap;
//      ArrayList result = new ArrayList();
//      Iterator i = sets.values().iterator();
//      while (i.hasNext()) {
//      String value = (String)i.next();
//      result.add(value);
//      }
//      Map listSetsMap = new HashMap();
//      listSetsMap.put("sets", result.iterator());
//      return listSetsMap;
    }
    
    
    public Map listSets(String resumptionToken)
    throws BadResumptionTokenException {
        throw new BadResumptionTokenException();
    }
    
    
    /**
     * close the repository
     */
    public void close() { }
    
    
    /**
     * Purge tokens that are older than the time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date      then, now = new Date();
        Iterator  keySet = resumptionResults.keySet().iterator();
        String    key;
        
        while (keySet.hasNext()) {
            key=(String)keySet.next();
            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }
    
    
    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a long integer version of the current time
     */
    private synchronized static String getRSName() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Properties;
import java.util.StringTokenizer;

import javax.xml.transform.TransformerException;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.NodeIterator;

import ORG.oclc.oai.util.OAIUtil;

/**
 * NewFileRecordFactory converts native XML "items" to "record" Strings.
 * This factory assumes the native XML item looks exactly like the <record>
 * element of an OAI GetRecord response, with the possible exception that the
 * <metadata> element contains multiple metadataFormats from which to choose.
 */
public class FolderRecordFactory extends RecordFactory {
    private String repositoryIdentifier = null;
    
    /**
     * Construct an NewFileRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public FolderRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
	repositoryIdentifier = properties.getProperty("NewFileRecordFactory.repositoryIdentifier");
	if (repositoryIdentifier == null) {
	    throw new IllegalArgumentException("NewFileRecordFactory.repositoryIdentifier is missing from the properties file");
	}
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
        try {
            StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
            tokenizer.nextToken();
            tokenizer.nextToken();
            return tokenizer.nextToken();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
        Document doc = (Document)nativeItem;
        try {
            return XPathAPI.eval(doc, "record/header/identifier").str();
        } catch (TransformerException e) {
            return e.getMessage();
        }
//	StringBuffer sb = new StringBuffer();
//	sb.append("oai:");
//	sb.append(repositoryIdentifier);
//	sb.append(":");
//	sb.append(getLocalIdentifier(nativeItem));
//	return sb.toString();
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
        return "foo";
//        return (String)((Document)nativeItem).get("localIdentifier");
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException  {
        Document doc = (Document)nativeItem;
        try {
            return XPathAPI.eval(doc, "/record/header/datestamp").str();
        } catch (TransformerException e) {
            return e.getMessage();
        }
//        return (String)((HashMap)nativeItem).get("lastModified");
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
        ArrayList list = new ArrayList();
        Document doc = (Document) nativeItem;
        try {
            NodeIterator iter = XPathAPI.selectNodeIterator(doc, "/record/header/setSpec");
            Node node = null;
            while ((node = iter.nextNode()) != null) {
                list.add(XPathAPI.eval(node, ".").str());
            }
        } catch (TransformerException e) {
            e.printStackTrace();
            return null;
        }
        return list.iterator();
//        return (Iterator)((HashMap)nativeItem).get("setSpecs");
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
        return null;
//	Document doc = (Document)nativeItem;
//            String result = new String((byte[])((HashMap)nativeItem).get("recordBytes"), "UTF-8");
//            if (result.startsWith("<?")) {
//                int offset = result.indexOf("?>");
//                result = result.substring(offset+2);
//            }
//            return result;
//        try {
//            return OAIUtil.toString(doc);
//        } catch (TransformerException e) {
//            // TODO Auto-generated catch block
//            e.printStackTrace();
//            return null;
//        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.util.Iterator;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * IMSXMLRecordFactory converts native XML items to Record objects and vice versa.
 * This factory assumes the native XML item looks exactly like the <record>
 * element of an OAI GetRecord response, with the possible exception that the
 * <metadata> element contains multiple metadataFormats from which to choose.
 */
public class IMSXMLRecordFactory extends RecordFactory {
    /**
     * Construct an IMSXMLRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     */
    public IMSXMLRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier
     * @return local identifier
     */
    public String fromOAIIdentifier(String identifier) {
	StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
	tokenizer.nextToken();
	tokenizer.nextToken();
	return tokenizer.nextToken();
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem)
	throws IllegalArgumentException  {
	throw new IllegalArgumentException("Identifier isn't available in native item");
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException  {
	throw new IllegalArgumentException("Datestamp isn't available in native item");
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
	throw new IllegalArgumentException("SetSpecs aren't available in native item");
    }

    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	throw new IllegalArgumentException("IsDeleted isn't available in native item");
    }

    public String quickCreate(Object nativeItem, String schemaLocation, String metadataFormat) {
	// Can't do quickCreates?
	return null;
    }

    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	throw new IllegalArgumentException("getAbouts isn't available in native item");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.util.OAIUtil;

/**
 * JDBCLimitedOAICatalog is an example of how to implement the AbstractCatalog interface
 * for JDBC. Pattern an implementation of the AbstractCatalog interface after this class
 * to have OAICat work with your JDBC database.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 * @deprecated Use ExtendedJDBCOAICatalog instead
 */
public class JDBCLimitedOAICatalog extends AbstractCatalog {
    private static final boolean debug = false;

    /**
     * SQL identifier query (loaded from properties)
     * \\i -> localIdentifier, \\o -> oaiIdentifier
     */
    private String identifierQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until
     */
    private String rangeQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until, \\s -> set
     */
    private String rangeSetQuery = null;

    /**
     * SQL query to get a list of available sets
     */
    private String setQuery = null;

    /**
     * SQL query to get a list of available sets that apply to a particular identifier
     */
    private String setSpecQuery = null;

    /**
     * SQL query to get a list of available abouts that apply to a particular identifier
     */
    private String aboutQuery = null;

    /**
     * SQL column labels containing the values of particular interest
     */
    private String aboutValueLabel = null;
    private String setSpecItemLabel = null;
    private String setSpecListLabel = null;
    private String setNameLabel = null;
    private String setDescriptionLabel = null;

    /**
     * maximum number of entries to return for ListRecords and ListIdentifiers
     * (loaded from properties)
     */
    private int maxListSize;

    /**
     * The format required for dates in SQL queries
     * "UTC" = YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ
     * other = YYYY/MM/DD
     */
    private String dateFormat = null;

    /**
     * Set Strings to be loaded from the properties file
     * (if they are to be loaded from properties rather than queried from the database)
     */
    ArrayList sets = new ArrayList();

    /**
     * The JDBC Connection
     */
    private boolean isPersistentConnection = true;
    private Connection persistentConnection;
    private String jdbcURL = null;
    private String jdbcLogin = null;
    private String jdbcPasswd = null;

    /**
     * pending resumption tokens
     */
    private HashMap resumptionResults = new HashMap();

    /**
     * Construct a JDBCLimitedOAICatalog object
     *
     * @param properties a properties object containing initialization parameters
     * @exception IOException an I/O error occurred during database initialization.
     */
    public JDBCLimitedOAICatalog(Properties properties) throws IOException {
        dateFormat = properties.getProperty("JDBCOAICatalog.dateFormat");
        String maxListSize = properties.getProperty("JDBCLimitedOAICatalog.maxListSize");
        if (maxListSize == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.maxListSize is missing from the properties file");
        } else {
            this.maxListSize = Integer.parseInt(maxListSize);
        }

        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
        if (paramRegex != null) {
            setParamRegex(paramRegex);
        }
        String jdbcDriverName = properties.getProperty("JDBCLimitedOAICatalog.jdbcDriverName");
        if (jdbcDriverName == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcDriverName is missing from the properties file");
        }
        jdbcURL = properties.getProperty("JDBCLimitedOAICatalog.jdbcURL");
        if (jdbcURL == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcURL is missing from the properties file");
        }

        jdbcLogin = properties.getProperty("JDBCLimitedOAICatalog.jdbcLogin");
        if (jdbcLogin == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcLogin is missing from the properties file");
        }

        jdbcPasswd = properties.getProperty("JDBCLimitedOAICatalog.jdbcPasswd");
        if (jdbcPasswd == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcPasswd is missing from the properties file");
        }

        rangeQuery = properties.getProperty("JDBCLimitedOAICatalog.rangeQuery");
        if (rangeQuery == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.rangeQuery is missing from the properties file");
        }

        rangeSetQuery = properties.getProperty("JDBCLimitedOAICatalog.rangeSetQuery");
        if (rangeSetQuery == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.rangeSetQuery is missing from the properties file");
        }

        identifierQuery = properties.getProperty("JDBCLimitedOAICatalog.identifierQuery");
        if (identifierQuery == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.identifierQuery is missing from the properties file");
        }

        aboutQuery = properties.getProperty("JDBCLimitedOAICatalog.aboutQuery");
        if (aboutQuery != null) {
            aboutValueLabel = properties.getProperty("JDBCLimitedOAICatalog.aboutValueLabel");
            if (aboutValueLabel == null) {
                throw new IllegalArgumentException("JDBCLimitedOAICatalog.aboutValueLabel is missing from the properties file");
            }
        }

        setSpecQuery = properties.getProperty("JDBCLimitedOAICatalog.setSpecQuery");
        setSpecItemLabel = properties.getProperty("JDBCLimitedOAICatalog.setSpecItemLabel");
        if (setSpecQuery != null && setSpecItemLabel == null) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.setSpecItemLabel is missing from the properties file");
        }
        setDescriptionLabel = properties.getProperty("JDBCLimitedOAICatalog.setDescriptionLabel");
//      if (setDescriptionLabel == null) {
//      throw new IllegalArgumentException("JDBCLimitedOAICatalog.setDescriptionLabel is missing from the properties file");
//      }

        // See if a setQuery exists
        setQuery = properties.getProperty("JDBCLimitedOAICatalog.setQuery");
        if (setQuery == null) {
            // if not, load the set Strings from the properties file (if present)
            String propertyPrefix = "Sets.";
            Enumeration propNames = properties.propertyNames();
            while (propNames.hasMoreElements()) {
                String propertyName = (String)propNames.nextElement();
                if (propertyName.startsWith(propertyPrefix)) {
                    sets.add(properties.get(propertyName));
                }
            }
        } else {
            setSpecListLabel = properties.getProperty("JDBCLimitedOAICatalog.setSpecListLabel");
            if (setSpecListLabel == null) {
                throw new IllegalArgumentException("JDBCLimitedOAICatalog.setSpecListLabel is missing from the properties file");
            }
            setNameLabel = properties.getProperty("JDBCLimitedOAICatalog.setNameLabel");
            if (setNameLabel == null) {
                throw new IllegalArgumentException("JDBCLimitedOAICatalog.setNameLabel is missing from the properties file");
            }
        }

        String temp = properties.getProperty("JDBCLimitedOAICatalog.isPersistentConnection");
        if ("false".equalsIgnoreCase(temp))
            isPersistentConnection = false;

        // open the connection
        try {
            Class.forName(jdbcDriverName);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("JDBCLimitedOAICatalog.jdbcDriverName is invalid: "
                    + jdbcDriverName);
        }

        if (isPersistentConnection) {
            try {
                persistentConnection = getNewConnection();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new IOException(e.getMessage());
            }
        }
    }

    private Connection getNewConnection() throws SQLException {
        // open the connection
        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
    }

    private Connection startConnection() throws SQLException {
        if (persistentConnection != null) {
            if (persistentConnection.isClosed())
                persistentConnection = getNewConnection();
            return persistentConnection;
        } else {
            return getNewConnection();
        }
    }

    private void endConnection(Connection con) throws OAIInternalServerError {
        try {
            if (persistentConnection == null)
                con.close();
        } catch (SQLException e) {
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
     * but the item is flagged as deleted and thus no schemaLocations (i.e.
     * metadataFormats) can be produced.
     */
    public Vector getSchemaLocations(String oaiIdentifier)
    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
        Connection con = null;
        try {
            con = startConnection();
            Statement stmt = con.createStatement();
            ResultSet rs =
                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
            /*
             * Let your recordFactory decide which schemaLocations
             * (i.e. metadataFormats) it can produce from the record.
             * Doing so will preserve the separation of database access
             * (which happens here) from the record content interpretation
             * (which is the responsibility of the RecordFactory implementation).
             */
            if (!rs.next()) {
                endConnection(con);
                throw new IdDoesNotExistException(oaiIdentifier);
            } else {
                /* Make sure the identifierQuery returns the columns you need
                 * (if any) to determine the supported schemaLocations for this item */
                HashMap nativeItem = getColumnValues(rs);
                endConnection(con);
                return getRecordFactory().getSchemaLocations(nativeItem);
            }
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Since the columns should only be read once, copy them into a
     * HashMap and consider that to be the "record"
     * @param rs The ResultSet row
     * @return a HashMap mapping column names with values
     */
    private HashMap getColumnValues(ResultSet rs)
    throws SQLException {
        ResultSetMetaData mdata = rs.getMetaData();
        int count = mdata.getColumnCount();
        HashMap nativeItem = new HashMap(count);
        for (int i=1; i<=count; ++i) {
            String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
            nativeItem.put(fieldName, rs.getObject(i));
            if (debug) System.out.println(fieldName + "=" + nativeItem.get(fieldName));
//          if (debug) {
//          System.out.println("JDBCLimitedOAICatalog.getColumnValues");
//          System.out.println("catalogName=" + mdata.getCatalogName(i));
//          System.out.println("columnLabel=" + mdata.getColumnLabel(i));
//          System.out.println("columnName=" + mdata.getColumnName(i));
//          System.out.println("schemaName=" + mdata.getSchemaName(i));
//          System.out.println("tableName=" + mdata.getTableName(i));
//          }
        }
        return nativeItem;
    }

    /**
     * insert actual from, until, and set parameters into the rangeQuery String
     * NOTE! This retrieves an extra record so we can decide if EOF has been reached.
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateRangeQuery(String from, String until, String set, int offset, int count)
    throws OAIInternalServerError {
        StringBuffer sb = new StringBuffer();
        StringTokenizer tokenizer;
        if (set == null || set.length() == 0)
            tokenizer = new StringTokenizer(rangeQuery, "\\");
        else
            tokenizer = new StringTokenizer(rangeSetQuery, "\\");

        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid query");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'a':
                sb.append(Integer.toString(offset));
                break;
            case 'b':
//              sb.append(Integer.toString(count));
                sb.append(Integer.toString(count+1)); // grab an extra record to decide if EOF
                break;
            case 'f':  // what are the chances someone would use this to indicate a form feed?
                sb.append(formatFromDate(from));
                break;
            case 'u':
                sb.append(formatUntilDate(until));
                break;
            case 's':
                sb.append(set);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatFromDate(String date) {
        return formatDate(date);
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatUntilDate(String date) {
        return formatDate(date);
    }

    /**
     * Change the String from UTC to SQL format
     * If this method doesn't suit your needs, extend this class and override
     * the method rather than change this code directly.
     */
    protected String formatDate(String date) {
        if ("UTC".equals(dateFormat)) {
            return date;
        } else {
            StringBuffer sb = new StringBuffer();
            sb.append(date.substring(5, 7));
            sb.append("/");
            sb.append(date.substring(8));
            sb.append("/");
            sb.append(date.substring(0, 4));
            if (debug) System.out.println("JDBCLimitedOAICatalog.formatDate: from " + date + " to " + sb.toString());
            return sb.toString();
        }
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateIdentifierQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateSetSpecQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateAboutQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Retrieve a list of identifiers that satisfy the specified criteria
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * It may seem strange for the map to include both "headers" and "identifiers"
     * since the identifiers can be obtained from the headers. This may be true, but
     * AbstractCatalog.listRecords() can operate quicker if it doesn't
     * need to parse identifiers from the XML headers itself. Better
     * still, do like I do below and override AbstractCatalog.listRecords().
     * AbstractCatalog.listRecords() is relatively inefficient because given the list
     * of identifiers, it must call getRecord() individually for each as it constructs
     * its response. It's much more efficient to construct the entire response in one fell
     * swoop by overriding listRecords() as I've done here.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
    throws NoItemsMatchException, OAIInternalServerError {
//      purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        Connection con = null;

        try {
            con = startConnection();
            /* Get some records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
//          rs.last();
//          int numRows = rs.getRow();
//          rs.beforeFirst();
            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count=0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = getColumnValues(rs);
                /* Use the RecordFactory to extract header/identifier pairs for each item */
//                RecordFactory rf = getRecordFactory();
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            if (count == 0) {
                endConnection(con);
                throw new NoItemsMatchException();
            }

            /* decide if you're done */
//          if (count == maxListSize) {
            if (rs.next()) {
//              String resumptionId = getResumptionId();
//              resumptionResults.put(resumptionId, rs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append(":");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append(":");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        0));
                //          listIdentifiersMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
                endConnection(con);
            }
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
//      purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();

        /****************************
         * parse your resumptionToken
         ****************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
//      String resumptionId;
        String from;
        String until;
        String set;
        int oldCount;
        String metadataPrefix;
//      int numRows;
        try {
//          resumptionId = tokenizer.nextToken();
            from = tokenizer.nextToken();
            until = tokenizer.nextToken();
            set = tokenizer.nextToken();
            if (set.equals("."))
                set = null;
            oldCount = Integer.parseInt(tokenizer.nextToken());
//          numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            if (debug) {
                System.out.println("JDBCLimitedOAICatalog.listIdentifiers: set=>"
                        + set + "<");
            }
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        Connection con = null;
        try {
            con = startConnection();
            /* Get some more records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
                    oldCount, maxListSize));
            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = getColumnValues(rs);
                /* Use the RecordFactory to extract header/identifier pairs for each item */
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            /* decide if you're done. */
//          if (count == maxListSize) {
            if (rs.next()) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append(":");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append(":");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        oldCount));
                //          listIdentifiersMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
                endConnection(con);
            }
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param oaiIdentifier the OAI identifier
     * @param metadataPrefix the OAI metadataPrefix
     * @return the <record/> portion of the XML response.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix is not
     * supported by the item.
     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
    throws OAIInternalServerError, CannotDisseminateFormatException,
    IdDoesNotExistException {
        Connection con = null;
        try {
            con = startConnection();
            Statement stmt = con.createStatement();
            ResultSet rs =
                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
            if (!rs.next()) {
                endConnection(con);
                throw new IdDoesNotExistException(oaiIdentifier);
            }
            HashMap nativeItem = getColumnValues(rs);
            endConnection(con);
            return constructRecord(nativeItem, metadataPrefix);
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria. Note, though,
     * that unlike the other OAI verb type methods implemented here, both of the
     * listRecords methods are already implemented in AbstractCatalog rather than
     * abstracted. This is because it is possible to implement ListRecords as a
     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
     * I suggest that you override both the AbstractCatalog.listRecords methods
     * here since it will probably improve the performance if you create the
     * response in one fell swoop rather than construct it one GetRecord at a time.
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for a "records" Iterator object
     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix isn't
     * supported by the item.
     */
    public Map listRecords(String from, String until, String set, String metadataPrefix)
    throws CannotDisseminateFormatException, NoItemsMatchException,
    OAIInternalServerError {
//      purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        Connection con = null;

        try {
            con = startConnection();
            /* Get some records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set, 0, maxListSize));
//          rs.last();
//          int numRows = rs.getRow();
//          rs.beforeFirst();
            int count;

//          if (debug) System.out.println("JDBCLimitedOAICatalog.listRecords: numRows=" + numRows);

            /* load the records ArrayList */
            for (count=0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = getColumnValues(rs);
                String record = constructRecord(nativeItem, metadataPrefix);
                records.add(record);
            }

            if (count == 0) {
                endConnection(con);
                throw new NoItemsMatchException();
            }

            /* decide if you're done */
//          if (count == maxListSize) {
            if (rs.next()) {
//              String resumptionId = getResumptionId();
//              resumptionResults.put(resumptionId, rs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append(":");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append(":");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append(":");
//              resumptionTokenSb.append(Integer.toString(numRows));
//              resumptionTokenSb.append(":");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        0));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
                endConnection(con);
            }
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception BadResumptionTokenException the value of the resumptionToken argument
     * is invalid or expired.
     */
    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
//      purge(); // clean out old resumptionTokens

        /****************************
         * parse your resumptionToken
         ****************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
//      String resumptionId;
        String from;
        String until;
        String set;
        int oldCount;
//      int numRows;
        String metadataPrefix;
        try {
//          resumptionId = tokenizer.nextToken();
            from = tokenizer.nextToken();
            until = tokenizer.nextToken();
            set = tokenizer.nextToken();
            if (set.equals("."))
                set = null;
            oldCount = Integer.parseInt(tokenizer.nextToken());
//          numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        Connection con = null;
        try {
            con = startConnection();
            /* Get some more records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set,
                    oldCount, maxListSize));

            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && rs.next(); ++count) {
                try {
                    HashMap nativeItem = getColumnValues(rs);
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                } catch (CannotDisseminateFormatException e) {
                    /* the client hacked the resumptionToken beyond repair */
                    endConnection(con);
                    throw new BadResumptionTokenException();
                }
            }

            /* decide if you're done */
//          if (count == maxListSize) {
            if (rs.next()) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
//              resumptionTokenSb.append(resumptionId);
//              resumptionTokenSb.append(":");
                resumptionTokenSb.append(from);
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(until);
                resumptionTokenSb.append(":");
                if (set == null)
                    resumptionTokenSb.append(".");
                else
                    resumptionTokenSb.append(URLEncoder.encode(set, "UTF-8"));
                resumptionTokenSb.append(":");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append(":");
//              resumptionTokenSb.append(Integer.toString(numRows));
//              resumptionTokenSb.append(":");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        -1,
                        oldCount));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
                endConnection(con);
            }
        } catch (UnsupportedEncodingException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(HashMap nativeItem, String metadataPrefix)
    throws CannotDisseminateFormatException, OAIInternalServerError {
        String schemaURL = null;
        Iterator setSpecs = getSetSpecs(nativeItem);
        Iterator abouts = getAbouts(nativeItem);

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * Retrieve a list of sets that satisfy the specified criteria
     *
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception OAIBadRequestException signals an http status code 400 problem
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
        if (setQuery == null) {
            if (sets.size() == 0)
                throw new NoSetHierarchyException();
            Map listSetsMap = new HashMap();
            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            Connection con = null;
            try {
                if (debug) System.out.println(setQuery);

                con = startConnection();
                /* Get some records from your database */
                Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
                ResultSet rs = stmt.executeQuery(setQuery);
                rs.last();
                int numRows = rs.getRow();
                rs.beforeFirst();
                int count;

//              if (debug) System.out.println("JDBCLimitedOAICatalog.listSets: numRows=" + numRows);

                /* load the sets ArrayLists. */
                for (count=0; count < maxListSize && rs.next(); ++count) {
                    /* Use the RecordFactory to extract header/set pairs for each item */
                    HashMap nativeItem = getColumnValues(rs);
                    sets.add(getSetXML(nativeItem));
                    if (debug) System.out.println("JDBCLimitedOAICatalog.listSets: adding an entry");
                }

                /* decide if you're done */
                if (count < numRows) {
                    String resumptionId = getResumptionId();
                    resumptionResults.put(resumptionId, rs);

                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append(":");
                    resumptionTokenSb.append(Integer.toString(count));
                    resumptionTokenSb.append(":");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            0));
                    //          listSetsMap.put("resumptionMap",
                    //                                 getResumptionMap(resumptionTokenSb.toString()));
                    endConnection(con);
                }
            } catch (SQLException e) {
                if (con != null)
                    endConnection(con);
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * Retrieve the next set of sets associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listSets() Map result.
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets(String resumptionToken)
    throws OAIInternalServerError, BadResumptionTokenException {
        if (setQuery == null) {
            throw new BadResumptionTokenException();
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            /****************************
             * parse your resumptionToken
             ****************************/
            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
            String resumptionId;
            int oldCount;
            int numRows;
            try {
                resumptionId = tokenizer.nextToken();
                oldCount = Integer.parseInt(tokenizer.nextToken());
                numRows = Integer.parseInt(tokenizer.nextToken());
            } catch (NoSuchElementException e) {
                throw new BadResumptionTokenException();
            }

            try {
                /* Get some more records from your database */
                ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
                if (rs == null) {
                    throw new BadResumptionTokenException();
                }

                if (rs.getRow() != oldCount) {
//                  System.out.println("JDBCLimitedOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                    rs.absolute(oldCount);
                }

                int count;

                /* load the sets ArrayLists. */
                for (count = 0; count < maxListSize && rs.next(); ++count) {
                    HashMap nativeItem = getColumnValues(rs);
                    /* Use the RecordFactory to extract set for each item */
                    sets.add(getSetXML(nativeItem));
                }

                /* decide if you're done. */
                if (oldCount+count < numRows) {
                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append(":");
                    resumptionTokenSb.append(Integer.toString(oldCount + count));
                    resumptionTokenSb.append(":");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            oldCount));
                    //          listSetsMap.put("resumptionMap",
                    //                                 getResumptionMap(resumptionTokenSb.toString()));
                }
            } catch (SQLException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(HashMap nativeItem)
    throws OAIInternalServerError {
        Connection con = null;
        try {
            ArrayList setSpecs = new ArrayList();
            if (setSpecQuery != null) {
                con = startConnection();
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                Statement stmt = con.createStatement();
                ResultSet rs = stmt.executeQuery(populateSetSpecQuery(oaiIdentifier));
                while (rs.next()) {
                    HashMap setMap = getColumnValues(rs);
                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
                }
                endConnection(con);
            }
            return setSpecs.iterator();
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(HashMap nativeItem)
    throws OAIInternalServerError {
        Connection con = null;
        try {
            ArrayList abouts = new ArrayList();
            if (aboutQuery != null) {
                con = startConnection();
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                Statement stmt = con.createStatement();
                ResultSet rs = stmt.executeQuery(populateAboutQuery(oaiIdentifier));
                while (rs.next()) {
                    HashMap aboutMap = getColumnValues(rs);
                    abouts.add(aboutMap.get(aboutValueLabel));
                }
                endConnection(con);
            }
            return abouts.iterator();
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Extract &lt;set&gt; XML string from setItem object
     *
     * @param setItem individual set instance in native format
     * @return an XML String containing the XML &lt;set&gt; content
     */
    public String getSetXML(HashMap setItem)
    throws IllegalArgumentException {
//      try {
        String setSpec = getSetSpec(setItem);
        String setName = getSetName(setItem);
        String setDescription = getSetDescription(setItem);

        StringBuffer sb = new StringBuffer();
        sb.append("<set>");
        sb.append("<setSpec>");
        sb.append(OAIUtil.xmlEncode(setSpec));
        sb.append("</setSpec>");
        sb.append("<setName>");
        sb.append(OAIUtil.xmlEncode(setName));
        sb.append("</setName>");
        if (setDescription != null) {
            sb.append("<setDescription>");
            sb.append(OAIUtil.xmlEncode(setDescription));
            sb.append("</setDescription>");
        }
        sb.append("</set>");
        return sb.toString();
//      } catch (SQLException e) {
//      e.printStackTrace();
//      throw new IllegalArgumentException(e.getMessage());
//      }
    }

    /**
     * get the setSpec XML string. Extend this class and override this method
     * if the setSpec can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setSpec&gt; content
     */
    protected String getSetSpec(HashMap setItem) {
        try {
            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return "UnsupportedEncodingException";
        }
    }

    /**
     * get the setName XML string. Extend this class and override this method
     * if the setName can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setName&gt; content
     */
    protected String getSetName(HashMap setItem) {
        return (String)setItem.get(setNameLabel);
    }

    /**
     * get the setDescription XML string. Extend this class and override this method
     * if the setDescription can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setDescription&gt; content
     */
    protected String getSetDescription(HashMap setItem) {
        if (setDescriptionLabel == null)
            return null;
        return (String)setItem.get(setDescriptionLabel);
    }

    /**
     * close the repository
     */
    public void close() {
        try {
            if (persistentConnection != null)
                persistentConnection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        persistentConnection = null;
    }

    /**
     * Purge tokens that are older than the configured time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date now = new Date();
        Iterator keySet = resumptionResults.keySet().iterator();
        while (keySet.hasNext()) {
            String key = (String)keySet.next();
            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            String key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }

    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a String version of the current time
     */
    private synchronized static String getResumptionId() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ORG.oclc.oai.server.catalog;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.util.OAIUtil;

/**
 * JDBCOAICatalog is an example of how to implement the AbstractCatalog interface
 * for JDBC. Pattern an implementation of the AbstractCatalog interface after this class
 * to have OAICat work with your JDBC database.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 * @deprecated Use ExtendedJDBCOAICatalog instead
 */
public class JDBCOAICatalog extends AbstractCatalog {
    private static final boolean debug = false;

    /**
     * SQL identifier query (loaded from properties)
     * \\i -> localIdentifier, \\o -> oaiIdentifier
     */
    private String identifierQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until
     */
    private String rangeQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until, \\s -> set
     */
    private String rangeSetQuery = null;

    /**
     * SQL query to get a list of available sets
     */
    private String setQuery = null;

    /**
     * SQL query to get a list of available sets that apply to a particular identifier
     */
    private String setSpecQuery = null;

    /**
     * SQL query to get a list of available abouts that apply to a particular identifier
     */
    private String aboutQuery = null;

    /**
     * SQL column labels containing the values of particular interest
     */
    private String aboutValueLabel = null;
    private String setSpecItemLabel = null;
    private String setSpecListLabel = null;
    private String setNameLabel = null;
    private String setDescriptionLabel = null;

    /**
     * maximum number of entries to return for ListRecords and ListIdentifiers
     * (loaded from properties)
     */
    private int maxListSize;

    /**
     * The format required for dates in SQL queries
     * "UTC" = YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ
     * other = YYYY/MM/DD
     */
    private String dateFormat = null;

    /**
     * Set Strings to be loaded from the properties file
     * (if they are to be loaded from properties rather than queried from the database)
     */
    ArrayList sets = new ArrayList();

    /**
     * Assume the connection will be persistent unless configured otherwise
     */
    private boolean isPersistentConnection = true;

    /**
     * The JDBC Connection
     */
    private Connection persistentConnection = null;

    /**
     * pending resumption tokens
     */
    private HashMap resumptionResults = new HashMap();
    private String jdbcURL = null;
    private String jdbcLogin = null;
    private String jdbcPasswd = null;

    /**
     * Construct a JDBCOAICatalog object
     *
     * @param properties a properties object containing initialization parameters
     * @exception IOException an I/O error occurred during database initialization.
     */
    public JDBCOAICatalog(Properties properties) throws IOException {
        dateFormat = properties.getProperty("JDBCOAICatalog.dateFormat");
        String maxListSize = properties.getProperty("JDBCOAICatalog.maxListSize");
        if (maxListSize == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.maxListSize is missing from the properties file");
        } else {
            this.maxListSize = Integer.parseInt(maxListSize);
        }

        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
        if (paramRegex != null) {
            setParamRegex(paramRegex);
        }
        String jdbcDriverName = properties.getProperty("JDBCOAICatalog.jdbcDriverName");
        if (jdbcDriverName == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is missing from the properties file");
        }
        jdbcURL = properties.getProperty("JDBCOAICatalog.jdbcURL");
        if (jdbcURL == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcURL is missing from the properties file");
        }

        jdbcLogin = properties.getProperty("JDBCOAICatalog.jdbcLogin");
        if (jdbcLogin == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcLogin is missing from the properties file");
        }

        jdbcPasswd = properties.getProperty("JDBCOAICatalog.jdbcPasswd");
        if (jdbcPasswd == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcPasswd is missing from the properties file");
        }

        rangeQuery = properties.getProperty("JDBCOAICatalog.rangeQuery");
        if (rangeQuery == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.rangeQuery is missing from the properties file");
        }

        rangeSetQuery = properties.getProperty("JDBCOAICatalog.rangeSetQuery");
        if (rangeSetQuery == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.rangeSetQuery is missing from the properties file");
        }

        identifierQuery = properties.getProperty("JDBCOAICatalog.identifierQuery");
        if (identifierQuery == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.identifierQuery is missing from the properties file");
        }

        aboutQuery = properties.getProperty("JDBCOAICatalog.aboutQuery");
        if (aboutQuery != null) {
            aboutValueLabel = properties.getProperty("JDBCOAICatalog.aboutValueLabel");
            if (aboutValueLabel == null) {
                throw new IllegalArgumentException("JDBCOAICatalog.aboutValueLabel is missing from the properties file");
            }
        }

        setSpecQuery = properties.getProperty("JDBCOAICatalog.setSpecQuery");
        setSpecItemLabel = properties.getProperty("JDBCOAICatalog.setSpecItemLabel");
        if (setSpecItemLabel == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.setSpecItemLabel is missing from the properties file");
        }
        setSpecListLabel = properties.getProperty("JDBCOAICatalog.setSpecListLabel");
        if (setSpecListLabel == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.setSpecListLabel is missing from the properties file");
        }
        setNameLabel = properties.getProperty("JDBCOAICatalog.setNameLabel");
        if (setNameLabel == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.setNameLabel is missing from the properties file");
        }

        String temp = properties.getProperty("JDBCOAICatalog.isPersistentConnection");
        if ("false".equalsIgnoreCase(temp)) {
            isPersistentConnection = false;
        }

        setDescriptionLabel = properties.getProperty("JDBCOAICatalog.setDescriptionLabel");
//      if (setDescriptionLabel == null) {
//      throw new IllegalArgumentException("JDBCOAICatalog.setDescriptionLabel is missing from the properties file");
//      }

        // See if a setQuery exists
        setQuery = properties.getProperty("JDBCOAICatalog.setQuery");
        if (setQuery == null) {
            // if not, load the set Strings from the properties file (if present)
            String propertyPrefix = "Sets.";
            Enumeration propNames = properties.propertyNames();
            while (propNames.hasMoreElements()) {
                String propertyName = (String)propNames.nextElement();
                if (propertyName.startsWith(propertyPrefix)) {
                    sets.add(properties.get(propertyName));
                }
            }
        }

        try {
            Class.forName(jdbcDriverName);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is invalid: "
                    + jdbcDriverName);
        }

        if (isPersistentConnection) {
            try {
                persistentConnection = getNewConnection();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new IOException(e.getMessage());
            }
        }
    }

    private Connection getNewConnection() throws SQLException {
        // open the connection
        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
    }

    private Connection startConnection() throws SQLException {
        if (persistentConnection != null) {
            if (persistentConnection.isClosed()) {
                System.out.println("Persistent connection has expired.");
                persistentConnection = getNewConnection();
            }
            return persistentConnection;
        } else {
            return getNewConnection();
        }
    }

    private void endConnection(Connection con) throws OAIInternalServerError {
        if (persistentConnection == null) {
            try {
                if (debug) System.out.println("ending connection");
                con.close();
            } catch (SQLException e) {
                throw new OAIInternalServerError(e.getMessage());
            }
        }
    }

    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
     * but the item is flagged as deleted and thus no schemaLocations (i.e.
     * metadataFormats) can be produced.
     */
    public Vector getSchemaLocations(String oaiIdentifier)
    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
        Connection con = null;
        try {
            con = startConnection();
            Statement stmt = con.createStatement();
            ResultSet rs =
                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
            /*
             * Let your recordFactory decide which schemaLocations
             * (i.e. metadataFormats) it can produce from the record.
             * Doing so will preserve the separation of database access
             * (which happens here) from the record content interpretation
             * (which is the responsibility of the RecordFactory implementation).
             */
            if (!rs.next()) {
                endConnection(con);
                throw new IdDoesNotExistException(oaiIdentifier);
            } else {
                /* Make sure the identifierQuery returns the columns you need
                 * (if any) to determine the supported schemaLocations for this item */
                HashMap nativeItem = getColumnValues(rs);
                endConnection(con);
                return getRecordFactory().getSchemaLocations(nativeItem);
            }
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Since the columns should only be read once, copy them into a
     * HashMap and consider that to be the "record"
     * @param rs The ResultSet row
     * @return a HashMap mapping column names with values
     */
    private HashMap getColumnValues(ResultSet rs)
    throws SQLException {
        ResultSetMetaData mdata = rs.getMetaData();
        int count = mdata.getColumnCount();
        HashMap nativeItem = new HashMap(count);
        for (int i=1; i<=count; ++i) {
            String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
            nativeItem.put(fieldName, rs.getObject(i));
            if (debug) System.out.println(fieldName + "=" + nativeItem.get(fieldName));
//          if (debug) {
//          System.out.println("JDBCOAICatalog.getColumnValues");
//          System.out.println("catalogName=" + mdata.getCatalogName(i));
//          System.out.println("columnLabel=" + mdata.getColumnLabel(i));
//          System.out.println("columnName=" + mdata.getColumnName(i));
//          System.out.println("schemaName=" + mdata.getSchemaName(i));
//          System.out.println("tableName=" + mdata.getTableName(i));
//          }
        }
        return nativeItem;
    }

    /**
     * insert actual from, until, and set parameters into the rangeQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateRangeQuery(String from, String until, String set)
    throws OAIInternalServerError {
        StringBuffer sb = new StringBuffer();
        StringTokenizer tokenizer;
        if (set == null || set.length() == 0)
            tokenizer = new StringTokenizer(rangeQuery, "\\");
        else
            tokenizer = new StringTokenizer(rangeSetQuery, "\\");

        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid query");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'f':  // what are the chances someone would use this to indicate a form feed?
                sb.append(formatFromDate(from));
                break;
            case 'u':
                sb.append(formatUntilDate(until));
                break;
            case 's':
                sb.append(set);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatFromDate(String date) {
        return formatDate(date);
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatUntilDate(String date) {
        return formatDate(date);
    }

    /**
     * Change the String from UTC to SQL format
     * If this method doesn't suit your needs, extend this class and override
     * the method rather than change this code directly.
     */
    protected String formatDate(String date) {
        if ("UTC".equals(dateFormat)) {
            return date;
        } else {
            StringBuffer sb = new StringBuffer();
            sb.append(date.substring(5, 7));
            sb.append("/");
            sb.append(date.substring(8));
            sb.append("/");
            sb.append(date.substring(0, 4));
            if (debug) System.out.println("JDBCOAICatalog.formatDate: from " + date + " to " + sb.toString());
            return sb.toString();
        }
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateIdentifierQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateSetSpecQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateAboutQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Retrieve a list of identifiers that satisfy the specified criteria
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * It may seem strange for the map to include both "headers" and "identifiers"
     * since the identifiers can be obtained from the headers. This may be true, but
     * AbstractCatalog.listRecords() can operate quicker if it doesn't
     * need to parse identifiers from the XML headers itself. Better
     * still, do like I do below and override AbstractCatalog.listRecords().
     * AbstractCatalog.listRecords() is relatively inefficient because given the list
     * of identifiers, it must call getRecord() individually for each as it constructs
     * its response. It's much more efficient to construct the entire response in one fell
     * swoop by overriding listRecords() as I've done here.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
    throws NoItemsMatchException, OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        Connection con = null;

        try {
            con = startConnection();
            /* Get some records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set));
            rs.last();
            int numRows = rs.getRow();
            if (numRows == 0) {
                endConnection(con);
                throw new NoItemsMatchException();
            }
            rs.beforeFirst();
            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count=0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = getColumnValues(rs);
                /* Use the RecordFactory to extract header/identifier pairs for each item */
//                RecordFactory rf = getRecordFactory();
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            /* decide if you're done */
            if (count < numRows) {
                String resumptionId = getResumptionId();
                resumptionResults.put(resumptionId, rs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        0));
                //          listIdentifiersMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
            }
            endConnection(con);
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();

        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
        int numRows;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        try {
            /* Get some more records from your database */
            ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
            if (rs == null) {
                throw new BadResumptionTokenException();
            }
            int count;

            if (rs.getRow() != oldCount) {
//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                rs.absolute(oldCount);
            }

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = getColumnValues(rs);
                /* Use the RecordFactory to extract header/identifier pairs for each item */
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            /* decide if you're done. */
            if (oldCount+count < numRows) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        oldCount));
                //          listIdentifiersMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param oaiIdentifier the OAI identifier
     * @param metadataPrefix the OAI metadataPrefix
     * @return the <record/> portion of the XML response.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix is not
     * supported by the item.
     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
    throws OAIInternalServerError, CannotDisseminateFormatException,
    IdDoesNotExistException {
        Connection con = null;
        try {
            con = startConnection();
            Statement stmt = con.createStatement();
            ResultSet rs =
                stmt.executeQuery(populateIdentifierQuery(oaiIdentifier));
            if (!rs.next()) {
                endConnection(con);
                throw new IdDoesNotExistException(oaiIdentifier);
            }
            HashMap nativeItem = getColumnValues(rs);
            endConnection(con);
            return constructRecord(nativeItem, metadataPrefix);
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria. Note, though,
     * that unlike the other OAI verb type methods implemented here, both of the
     * listRecords methods are already implemented in AbstractCatalog rather than
     * abstracted. This is because it is possible to implement ListRecords as a
     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
     * I suggest that you override both the AbstractCatalog.listRecords methods
     * here since it will probably improve the performance if you create the
     * response in one fell swoop rather than construct it one GetRecord at a time.
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for a "records" Iterator object
     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix isn't
     * supported by the item.
     */
    public Map listRecords(String from, String until, String set, String metadataPrefix)
    throws CannotDisseminateFormatException, NoItemsMatchException,
    OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        Connection con = null;

        try {
            con = startConnection();
            /* Get some records from your database */
            Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);
            ResultSet rs = stmt.executeQuery(populateRangeQuery(from, until, set));
            rs.last();
            int numRows = rs.getRow();
            if (numRows == 0) {
                endConnection(con);
                throw new NoItemsMatchException();
            }
            rs.beforeFirst();
            int count;

//          if (debug) System.out.println("JDBCOAICatalog.listRecords: numRows=" + numRows);

            /* load the records ArrayList */
            for (count=0; count < maxListSize && rs.next(); ++count) {
                HashMap nativeItem = getColumnValues(rs);
                String record = constructRecord(nativeItem, metadataPrefix);
                records.add(record);
            }

            /* decide if you're done */
            if (count < numRows) {
                String resumptionId = getResumptionId();
                resumptionResults.put(resumptionId, rs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        0));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
                endConnection(con);
            }
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception BadResumptionTokenException the value of the resumptionToken argument
     * is invalid or expired.
     */
    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        purge(); // clean out old resumptionTokens

        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
        String resumptionId;
        int oldCount;
        int numRows;
        String metadataPrefix;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        try {
            /* Get some more records from your database */
            ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
            if (rs == null) {
                throw new BadResumptionTokenException();
            }

            if (rs.getRow() != oldCount) {
//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                rs.absolute(oldCount);
            }

            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && rs.next(); ++count) {
                try {
                    HashMap nativeItem = getColumnValues(rs);
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                } catch (CannotDisseminateFormatException e) {
                    /* the client hacked the resumptionToken beyond repair */
                    throw new BadResumptionTokenException();
                }
            }

            /* decide if you're done */
            if (oldCount+count < numRows) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        oldCount));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(HashMap nativeItem, String metadataPrefix)
    throws CannotDisseminateFormatException, OAIInternalServerError {
        String schemaURL = null;
        Iterator setSpecs = getSetSpecs(nativeItem);
        Iterator abouts = getAbouts(nativeItem);

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * Retrieve a list of sets that satisfy the specified criteria
     *
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception OAIBadRequestException signals an http status code 400 problem
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
        Connection con = null;
        if (setQuery == null) {
            if (sets.size() == 0)
                throw new NoSetHierarchyException();
            Map listSetsMap = new HashMap();
            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            try {
                con = startConnection();
                if (debug) System.out.println(setQuery);

                /* Get some records from your database */
                Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
                ResultSet rs = stmt.executeQuery(setQuery);
                rs.last();
                int numRows = rs.getRow();
                rs.beforeFirst();
                int count;

//              if (debug) System.out.println("JDBCOAICatalog.listSets: numRows=" + numRows);

                /* load the sets ArrayLists. */
                for (count=0; count < maxListSize && rs.next(); ++count) {
                    /* Use the RecordFactory to extract header/set pairs for each item */
                    HashMap nativeItem = getColumnValues(rs);
                    sets.add(getSetXML(nativeItem));
                    if (debug) System.out.println("JDBCOAICatalog.listSets: adding an entry");
                }

                /* decide if you're done */
                if (count < numRows) {
                    String resumptionId = getResumptionId();
                    /*****************************************************************
                     * Note that storing the ResultSet in the resumptionResult
                     * means the token can't be reused.
                     *****************************************************************/
                    resumptionResults.put(resumptionId, rs);

                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(count));
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            0));
                    //          listSetsMap.put("resumptionMap",
                    //                                 getResumptionMap(resumptionTokenSb.toString()));
                    endConnection(con);
                }
            } catch (SQLException e) {
                if (con != null)
                    endConnection(con);
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * Retrieve the next set of sets associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listSets() Map result.
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets(String resumptionToken)
    throws OAIInternalServerError, BadResumptionTokenException {
        if (setQuery == null) {
            throw new BadResumptionTokenException();
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            /**********************************************************************
             * parse your resumptionToken and look it up in the resumptionResults,
             * if necessary
             **********************************************************************/
            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
            String resumptionId;
            int oldCount;
            int numRows;
            try {
                resumptionId = tokenizer.nextToken();
                oldCount = Integer.parseInt(tokenizer.nextToken());
                numRows = Integer.parseInt(tokenizer.nextToken());
            } catch (NoSuchElementException e) {
                throw new BadResumptionTokenException();
            }

            try {
                /* Get some more records from your database */
                ResultSet rs = (ResultSet)resumptionResults.get(resumptionId);
                if (rs == null) {
                    throw new BadResumptionTokenException();
                }

                if (rs.getRow() != oldCount) {
//                  System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                    rs.absolute(oldCount);
                }

                int count;

                /* load the sets ArrayLists. */
                for (count = 0; count < maxListSize && rs.next(); ++count) {
                    HashMap nativeItem = getColumnValues(rs);
                    /* Use the RecordFactory to extract set for each item */
                    sets.add(getSetXML(nativeItem));
                }

                /* decide if you're done. */
                if (oldCount+count < numRows) {
                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(oldCount + count));
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            oldCount));
                    //          listSetsMap.put("resumptionMap",
                    //                                 getResumptionMap(resumptionTokenSb.toString()));
                }
            } catch (SQLException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(HashMap nativeItem)
    throws OAIInternalServerError {
        Connection con = null;
        try {
            ArrayList setSpecs = new ArrayList();
            if (setSpecQuery != null) {
                con = startConnection();
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                Statement stmt = con.createStatement();
                ResultSet rs = stmt.executeQuery(populateSetSpecQuery(oaiIdentifier));
                while (rs.next()) {
                    HashMap setMap = getColumnValues(rs);
                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
                }
                endConnection(con);
            }
            return setSpecs.iterator();
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(HashMap nativeItem)
    throws OAIInternalServerError {
        Connection con = null;
        try {
            ArrayList abouts = new ArrayList();
            if (aboutQuery != null) {
                con = startConnection();
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                Statement stmt = con.createStatement();
                ResultSet rs = stmt.executeQuery(populateAboutQuery(oaiIdentifier));
                while (rs.next()) {
                    HashMap aboutMap = getColumnValues(rs);
                    abouts.add(aboutMap.get(aboutValueLabel));
                }
                endConnection(con);
            }
            return abouts.iterator();
        } catch (SQLException e) {
            if (con != null)
                endConnection(con);
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Extract &lt;set&gt; XML string from setItem object
     *
     * @param setItem individual set instance in native format
     * @return an XML String containing the XML &lt;set&gt; content
     */
    public String getSetXML(HashMap setItem)
    throws IllegalArgumentException {
//      try {
        String setSpec = getSetSpec(setItem);
        String setName = getSetName(setItem);
        String setDescription = getSetDescription(setItem);

        StringBuffer sb = new StringBuffer();
        sb.append("<set>");
        sb.append("<setSpec>");
        sb.append(OAIUtil.xmlEncode(setSpec));
        sb.append("</setSpec>");
        sb.append("<setName>");
        sb.append(OAIUtil.xmlEncode(setName));
        sb.append("</setName>");
        if (setDescription != null) {
            sb.append("<setDescription>");
            sb.append(OAIUtil.xmlEncode(setDescription));
            sb.append("</setDescription>");
        }
        sb.append("</set>");
        return sb.toString();
//      } catch (SQLException e) {
//      e.printStackTrace();
//      throw new IllegalArgumentException(e.getMessage());
//      }
    }

    /**
     * get the setSpec XML string. Extend this class and override this method
     * if the setSpec can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setSpec&gt; content
     */
    protected String getSetSpec(HashMap setItem) {
        try {
            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return "UnsupportedEncodingException";
        }
    }

    /**
     * get the setName XML string. Extend this class and override this method
     * if the setName can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setName&gt; content
     */
    protected String getSetName(HashMap setItem) {
        return (String)setItem.get(setNameLabel);
    }

    /**
     * get the setDescription XML string. Extend this class and override this method
     * if the setDescription can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setDescription&gt; content
     */
    protected String getSetDescription(HashMap setItem) {
        if (setDescriptionLabel == null)
            return null;
        return (String)setItem.get(setDescriptionLabel);
    }

    /**
     * close the repository
     */
    public void close() {
        try {
            if (persistentConnection != null)
                persistentConnection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        persistentConnection = null;
    }

    /**
     * Purge tokens that are older than the configured time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date now = new Date();
        Iterator keySet = resumptionResults.keySet().iterator();
        while (keySet.hasNext()) {
            String key = (String)keySet.next();
            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            String key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }

    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a String version of the current time
     */
    private synchronized static String getResumptionId() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.sql.Date;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * JDBCRecordFactory converts JDBC "items" to "record" Strings.
 */
public class JDBCRecordFactory extends RecordFactory {
    private String repositoryIdentifier = null;
    protected String identifierLabel = null;
    protected String datestampLabel = null;
    
    /**
     * Construct an JDBCRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public JDBCRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
	repositoryIdentifier = properties.getProperty("JDBCRecordFactory.repositoryIdentifier");
	if (repositoryIdentifier == null) {
	    throw new IllegalArgumentException("JDBCRecordFactory.repositoryIdentifier is missing from the properties file");
	}
	identifierLabel = properties.getProperty("JDBCRecordFactory.identifierLabel");
	if (identifierLabel == null) {
	    throw new IllegalArgumentException("JDBCRecordFactory.identifierLabel is missing from the properties file");
	}
	datestampLabel = properties.getProperty("JDBCRecordFactory.datestampLabel");
	if (datestampLabel == null) {
	    throw new IllegalArgumentException("JDBCRecordFactory.datestampLabel is missing from the properties file");
	}
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param oaiIdentifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String oaiIdentifier) {
	StringTokenizer tokenizer = new StringTokenizer(oaiIdentifier, ":");
	try {
	    tokenizer.nextToken();
	    tokenizer.nextToken();
	    return tokenizer.nextToken();
	} catch (java.util.NoSuchElementException e) {
	    return null;
	}
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
// 	throws IllegalArgumentException {
// 	try {
	    HashMap table = (HashMap)nativeItem;
	    Object o = table.get(identifierLabel);
	    return o.toString();
// 	} catch (SQLException e) {
// 	    e.printStackTrace();
// 	    throw new IllegalArgumentException(e.getMessage());
// 	}
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem)
	throws IllegalArgumentException {
	StringBuffer sb = new StringBuffer();
	sb.append("oai:");
	sb.append(repositoryIdentifier);
	sb.append(":");
	sb.append(getLocalIdentifier(nativeItem));
	return sb.toString();
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem) {
// 	throws IllegalArgumentException  {
// 	try {
	    HashMap table = (HashMap)nativeItem;
        Object datestamp = table.get(datestampLabel);
        if (datestamp instanceof Timestamp) { 
            return ((Timestamp)datestamp).toString().substring(0, 10);
        } else if (datestamp instanceof Date) {
            return ((Date)datestamp).toString();
        } else {
            throw new IllegalArgumentException("Unrecognized datestamp format: " + datestamp.getClass().getName());
        }
        
// 	} catch (SQLException e) {
// 	    e.printStackTrace();
// 	    throw new IllegalArgumentException(e.getMessage());
// 	}
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item. Null if setSpecs aren't
     * derived from the nativeItem.
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
	return null;
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item. Null if
     * abouts aren't derived from the nativeItem
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
	// Don't perform quick creates
	return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * NewFileRecordFactory converts native XML "items" to "record" Strings.
 * This factory assumes the native XML item looks exactly like the <record>
 * element of an OAI GetRecord response, with the possible exception that the
 * <metadata> element contains multiple metadataFormats from which to choose.
 */
public class NewFileRecordFactory extends RecordFactory {
    private String repositoryIdentifier = null;
    
    /**
     * Construct an NewFileRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public NewFileRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
	repositoryIdentifier = properties.getProperty("NewFileRecordFactory.repositoryIdentifier");
	if (repositoryIdentifier == null) {
	    throw new IllegalArgumentException("NewFileRecordFactory.repositoryIdentifier is missing from the properties file");
	}
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
        try {
            StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
            tokenizer.nextToken();
            tokenizer.nextToken();
            return tokenizer.nextToken();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
	StringBuffer sb = new StringBuffer();
	sb.append("oai:");
	sb.append(repositoryIdentifier);
	sb.append(":");
	sb.append(getLocalIdentifier(nativeItem));
	return sb.toString();
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
        return (String)((HashMap)nativeItem).get("localIdentifier");
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException  {
        return (String)((HashMap)nativeItem).get("lastModified");
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
        return (Iterator)((HashMap)nativeItem).get("setSpecs");
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
	// Don't perform quick creates
        try {
            String result = new String((byte[])((HashMap)nativeItem).get("recordBytes"), "UTF-8");
            if (result.startsWith("<?")) {
                int offset = result.indexOf("?>");
                result = result.substring(offset+2);
            }
            return result;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
// 	return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ORG.oclc.oai.server.catalog;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;


/**
 * NewFileSystemOAICatalog is an implementation of AbstractCatalog interface
 * with the data sitting in a directory on a filesystem.
 *
 * @author Jeff Young, OCLC Online Computer Library Center
 */

public class NewFileSystemOAICatalog extends AbstractCatalog {
    static final boolean debug=false;

    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
    protected String           homeDir;
    protected HashMap          fileDateMap = new HashMap();
    private HashMap setMap = new HashMap();
    private HashMap          resumptionResults=new HashMap();
    private int              maxListSize;
    private ArrayList sets = null;

    public NewFileSystemOAICatalog(Properties properties) throws IOException {
        String          temp;

        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
        temp=properties.getProperty("NewFileSystemOAICatalog.maxListSize");
        if (temp==null)
            throw new IllegalArgumentException("NewFileSystemOAICatalog."+
                "maxListSize is missing from the properties file");
        maxListSize = Integer.parseInt(temp);
        if(debug)
            System.out.println("in NewFileSystemOAICatalog(): maxListSize="+
                maxListSize);

        homeDir=properties.getProperty("NewFileSystemOAICatalog.homeDir");
        if (homeDir==null)
            throw new IllegalArgumentException("NewFileSystemOAICatalog."+
                "homeDir is missing from the properties file");
        if(debug)
            System.out.println("in NewFileSystemOAICatalog(): homeDir="+
                homeDir);


	File homeFile = new File(homeDir);
	int homeDirLen = homeFile.getPath().length()+1;
	loadFileMap(homeDirLen, homeFile);
// 	Iterator iterator = fileDateMap.entrySet().iterator();
// 	while (iterator.hasNext()) {
// 	    Map.Entry entry = (Map.Entry)iterator.next();
// 	    System.out.println(entry.getKey() + ":" + entry.getValue());
// 	}
	sets = getSets(properties);
    }

    private static ArrayList getSets(Properties properties) {
        TreeMap treeMap = new TreeMap();
	String propertyPrefix = "Sets.";
	Enumeration propNames = properties.propertyNames();
	while (propNames.hasMoreElements()) {
	    String propertyName = (String)propNames.nextElement();
	    if (propertyName.startsWith(propertyPrefix)) {
                treeMap.put(propertyName, properties.get(propertyName));
	    }
	}
 	return new ArrayList(treeMap.values());
    }
    
    private void loadFileMap(int homeDirLen, File currentDir)
        throws IOException {
        try {
            String[] list = currentDir.list();
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            for (int i=0; i<list.length; ++i) {
                File child = new File(currentDir, list[i]);
                if (child.isDirectory() && !"CVS".equals(child.getName())) {
                    loadFileMap(homeDirLen, child);
                } else if (isMetadataFile(child)) {
                    String path = file2path(homeDirLen, child);
                    System.out.println("parsing " + path);
                    //                 String datestamp = date2OAIDatestamp(new Date(child.lastModified()));
                    File file = localIdentifier2File(path);
                    FileInputStream fis = new FileInputStream(file);
                    InputSource data = new InputSource(fis);
                    Node doc = builder.parse(data);
                    fis.close();
                    
                    Node datestampNode = XPathAPI.selectSingleNode(doc, "/record/header/datestamp");
                    fileDateMap.put(path,
                                    XPathAPI.eval(datestampNode, "string()").str());
                    NodeList setNodes = XPathAPI.selectNodeList(doc, "/record/header/setSpec");
                    for (int j=0; j<setNodes.getLength(); ++j) {
                        Node setSpecNode = setNodes.item(j);
                        String setSpec = XPathAPI.eval(setSpecNode,
                                                       "string()").str();
                        ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
                        if (setSpecList == null) {
                            setSpecList = new ArrayList();
                            setMap.put(setSpec, setSpecList);
                        }
                        setSpecList.add(path);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Override this method if some files exist in the
     * filesystem that aren't metadata records.
     *
     * @param child the File to be investigated
     * @return true if it contains metadata, false otherwise
     */
    protected boolean isMetadataFile(File child) {
        return true;
    }

    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param homeDirLen the length of the home directory path
     * @param file the File object containing the native record
     * @return localIdentifier
     */
    protected String file2path(int homeDirLen, File file) {
	String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
	return fileName;
    }

    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
     * @return the File object containing the native record
     */
    protected File localIdentifier2File(String localIdentifier) {
        String fileName = localIdentifier.replace('/', File.separatorChar);
	return new File(homeDir, fileName);
    }

//     private String date2OAIDatestamp(Date date) {
//         return dateFormatter.format(date);
//     }

    private HashMap getNativeHeader(String path) {
        HashMap recordMap = null;
        if (fileDateMap.containsKey(path)) {
            recordMap = new HashMap();
            recordMap.put("localIdentifier", path.substring(0, path.lastIndexOf(".")));
            recordMap.put("lastModified", fileDateMap.get(path));
            ArrayList setSpecs = new ArrayList();
            Iterator keySet = setMap.keySet().iterator();
            while (keySet.hasNext()) {
                String key = (String)keySet.next();
                ArrayList identifierList = (ArrayList)setMap.get(key);
                if (identifierList.contains(path)) {
                    setSpecs.add(key);
                }
            }
            recordMap.put("setSpecs", setSpecs.iterator());
            return recordMap;
        }
        return recordMap;
    }

    private ArrayList getExtensionList(String localIdentifier) {
//         System.out.println("getExtensionList: localIdentifier="
//                            + localIdentifier);
        ArrayList list = new ArrayList();
        Iterator iterator = fileDateMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry)iterator.next();
//             System.out.println("entry.getKey()=" + entry.getKey());
            if (((String)entry.getKey()).startsWith(localIdentifier)) {
                list.add(((String)entry.getKey()).substring(localIdentifier.length()+1));
            }
        }
        return list;
    }
    
    private HashMap getNativeRecord(String path)
        throws IOException {
        HashMap recordMap = getNativeHeader(path);
        if (recordMap == null) {
            return null;
        } else {
            File file = localIdentifier2File(path);
            try {
                FileInputStream fis = new FileInputStream(file);
                BufferedInputStream bis = new BufferedInputStream(fis);
                byte[] buffer = new byte[(int)file.length()];
                bis.read(buffer, 0, (int)file.length());
                recordMap.put("recordBytes", buffer);
                bis.close();
                fis.close();
                return recordMap;
            } catch (FileNotFoundException e) {
                return null;
            }
        }
    }
    
    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param     oaiIdentifier the OAI identifier
     * @param     metadataPrefix the OAI metadataPrefix
     * @return    the Record object containing the result.
     * @exception CannotDisseminateFormatException signals an http status
     *                code 400 problem
     * @exception IdDoesNotExistException signals an http status code 404
     *                problem
     * @exception OAIInternalServerError signals an http status code 500
     *                problem
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
        throws IdDoesNotExistException, CannotDisseminateFormatException,
               OAIInternalServerError {
        HashMap nativeItem = null;
        try {
	    String localIdentifier
		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);

            nativeItem = getNativeRecord(localIdentifier + "." + metadataPrefix);
            if (nativeItem == null)
                throw new IdDoesNotExistException(oaiIdentifier);
            return constructRecord(nativeItem, metadataPrefix);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
    }


    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * We get passed the ID for a record and are supposed to return a list
     * of the formats that we can deliver the record in.  Since we are assuming
     * that all the records in the directory have the same format, the
     * response to this is static;
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAINotFoundException signals an http status code 404 problem
     */
    public Vector getSchemaLocations(String oaiIdentifier)
      throws IdDoesNotExistException, NoMetadataFormatsException {
        ArrayList extensionList = null;
//         try {
	    String localIdentifier
		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
            extensionList = getExtensionList(localIdentifier);
//         } catch (IOException e) {
//             e.printStackTrace();
//             throw new OAIInternalServerError("Database Failure");
//         }
        
        if (extensionList != null) {
            return getRecordFactory().getSchemaLocations(extensionList);
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }


    /**
     * Retrieve a list of Identifiers that satisfy the criteria parameters
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
        throws NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
	Iterator iterator = fileDateMap.entrySet().iterator();
	int numRows = fileDateMap.entrySet().size();
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            String path = (String)entryDateMap.getKey();
            if (fileDate.compareTo(from) >= 0
                && fileDate.compareTo(until) <= 0
                && (setIdentifiers == null || setIdentifiers.contains(path))) {
                HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
                String[] header = getRecordFactory().createHeader(nativeHeader);
                headers.add(header[0]);
                identifiers.add(header[1]);
                count++;
            }
	}

        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listIdentifiersMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listIdentifiersMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of Identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String resumptionToken)
      throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        String set;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            set = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("NewFileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
	    iterator = fileDateMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the headers and identifiers ArrayLists. */
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String path = (String)entryDateMap.getKey();
            if (setIdentifiers == null || setIdentifiers.contains(path)) {
                HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
                String[] header = getRecordFactory().createHeader(nativeHeader);
                headers.add(header[0]);
                identifiers.add(header[1]);
                count++;
            }
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listIdentifiersMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }


    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(HashMap nativeItem, String metadataPrefix)
        throws CannotDisseminateFormatException {
        String schemaURL = null;
	Iterator setSpecs = getSetSpecs(nativeItem);
	Iterator abouts = getAbouts(nativeItem);

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(HashMap nativeItem) {
        return null;
    }

    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(HashMap nativeItem) {
        return null;
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @param metadataPrefix the OAI metadataPrefix
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listRecords(String from, String until, String set,
                                    String metadataPrefix)
      throws CannotDisseminateFormatException,
      OAIInternalServerError, NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
	Iterator iterator = fileDateMap.entrySet().iterator();
	int numRows = fileDateMap.entrySet().size();
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            String path = (String)entryDateMap.getKey();
            String extension = path.substring(path.lastIndexOf(".")+1);
            if (fileDate.compareTo(from) >= 0
                && fileDate.compareTo(until) <= 0
                && extension.equals(metadataPrefix)
                && (setIdentifiers == null || setIdentifiers.contains(path))) {
                try {
                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                    count++;
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            }
	}
        
        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listRecordsMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listRecordsMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listRecords(String resumptionToken)
      throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        String set;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            set = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("NewFileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
	    iterator = fileDateMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the records ArrayLists. */
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String path = (String)entryDateMap.getKey();
            if (setIdentifiers == null || setIdentifiers.contains(path)) {
                try {
                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                    count++;
                } catch (CannotDisseminateFormatException e) {
                    /* the client hacked the resumptionToken beyond repair */
                    throw new BadResumptionTokenException();
                } catch (IOException e) {
                    /* the file is probably missing */
                    throw new BadResumptionTokenException();
                }
            }
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listRecordsMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    public Map listSets() throws NoSetHierarchyException {
// 	throw new NoSetHierarchyException();
        if (sets.size() == 0)
            throw new NoSetHierarchyException();
         Map listSetsMap = new LinkedHashMap();
         listSetsMap.put("sets", sets.iterator());
         return listSetsMap;
//         ArrayList result = new ArrayList();
//         Iterator i = sets.values().iterator();
//         while (i.hasNext()) {
//             String value = (String)i.next();
//             result.add(value);
//         }
//         Map listSetsMap = new HashMap();
//         listSetsMap.put("sets", result.iterator());
//         return listSetsMap;
    }


    public Map listSets(String resumptionToken)
      throws BadResumptionTokenException {
	throw new BadResumptionTokenException();
    }


    /**
     * close the repository
     */
    public void close() { }


    /**
     * Purge tokens that are older than the time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date      then, now = new Date();
        Iterator  keySet = resumptionResults.keySet().iterator();
        String    key;

        while (keySet.hasNext()) {
            key=(String)keySet.next();
            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }


    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a long integer version of the current time
     */
    private synchronized static String getRSName() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.util.OAIUtil;

/**
 * NewJDBCOAICatalog is like JDBCOAICatalog except it is more intelligent
 * about the coupling between Statement and ResultSet.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 * @deprecated Use ExtendedJDBCOAICatalog instead
 */
public class NewJDBCOAICatalog extends AbstractCatalog {
    /**
     * The StatementResultSet inner class is used because Statement objects
     * and ResultSet objects are tightly coupled and need to persist across
     * multiple requests.
     *
     * @author Jeffrey A. Young
     */
    private class StatementResultSet {
        private Statement stmt = null;
        private ResultSet rs = null;

        public StatementResultSet(String query) throws SQLException {
            Connection con = getConnection();
            stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            rs = stmt.executeQuery(query);
        }

        public Statement getStatement() { return stmt; }
        public ResultSet getResultSet() { return rs; }

        public void close() throws SQLException {
            if (rs != null) {
                rs.close();
                rs = null;
            }
            if (stmt != null) {
                stmt.close();
                stmt = null;
            }
        }

        /**
         * Since the columns should only be read once, copy them into a
         * HashMap and consider that to be the "record"
         * @return a HashMap mapping column names with values
         */
        public HashMap getColumnValues()
        throws SQLException {
            ResultSetMetaData mdata = rs.getMetaData();
            int count = mdata.getColumnCount();
            HashMap nativeItem = new HashMap(count);
            for (int i=1; i<=count; ++i) {
                String fieldName = new StringBuffer().append(mdata.getTableName(i)).append(".").append(mdata.getColumnName(i)).toString();
                nativeItem.put(fieldName, rs.getObject(i));
                if (debug) System.out.println(fieldName + "=" + nativeItem.get(fieldName));
            }
            return nativeItem;
        }

        public boolean next() throws SQLException {
            return rs.next();
        }

        public void last() throws SQLException {
            rs.last();
        }

        public void beforeFirst() throws SQLException {
            rs.beforeFirst();
        }

        public int getRow() throws SQLException {
            return rs.getRow();
        }

        public boolean absolute(int oldCount) throws SQLException {
            return rs.absolute(oldCount);
        }
    }

    /**
     * The JDBC Connection
     */
    private Connection persistentConnection = null;

    private static final boolean debug = false;

    /**
     * SQL identifier query (loaded from properties)
     * \\i -> localIdentifier, \\o -> oaiIdentifier
     */
    private String identifierQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until
     */
    private String rangeQuery = null;

    /**
     * SQL range query (loaded from properties)
     * \\f -> from, \\u -> until, \\s -> set
     */
    private String rangeSetQuery = null;

    /**
     * SQL query to get a list of available sets
     */
    private String setQuery = null;

    /**
     * SQL query to get a list of available sets that apply to a particular identifier
     */
    private String setSpecQuery = null;

    /**
     * SQL query to get a list of available abouts that apply to a particular identifier
     */
    private String aboutQuery = null;

    /**
     * SQL column labels containing the values of particular interest
     */
    private String aboutValueLabel = null;
    private String setSpecItemLabel = null;
    private String setSpecListLabel = null;
    private String setNameLabel = null;
    private String setDescriptionLabel = null;

    /**
     * maximum number of entries to return for ListRecords and ListIdentifiers
     * (loaded from properties)
     */
    private int maxListSize;

    /**
     * The format required for dates in SQL queries
     * "UTC" = YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ
     * other = YYYY/MM/DD
     */
    private String dateFormat = null;

    /**
     * Set Strings to be loaded from the properties file
     * (if they are to be loaded from properties rather than queried from the database)
     */
    ArrayList sets = new ArrayList();

    /**
     * pending resumption tokens
     */
    private HashMap resumptionResults = new HashMap();
    private String jdbcURL = null;
    private String jdbcLogin = null;
    private String jdbcPasswd = null;

    /**
     * Construct a JDBCOAICatalog object
     *
     * @param properties a properties object containing initialization parameters
     */
    public NewJDBCOAICatalog(Properties properties) {
        dateFormat = properties.getProperty("JDBCOAICatalog.dateFormat");
        String maxListSize = properties.getProperty("JDBCOAICatalog.maxListSize");
        if (maxListSize == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.maxListSize is missing from the properties file");
        } else {
            this.maxListSize = Integer.parseInt(maxListSize);
        }

        String paramRegex = properties.getProperty("AbstractCatalog.paramRegex");
        if (paramRegex != null) {
            setParamRegex(paramRegex);
        }
        String jdbcDriverName = properties.getProperty("JDBCOAICatalog.jdbcDriverName");
        if (jdbcDriverName == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is missing from the properties file");
        }
        jdbcURL = properties.getProperty("JDBCOAICatalog.jdbcURL");
        if (jdbcURL == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcURL is missing from the properties file");
        }

        jdbcLogin = properties.getProperty("JDBCOAICatalog.jdbcLogin");
        if (jdbcLogin == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcLogin is missing from the properties file");
        }

        jdbcPasswd = properties.getProperty("JDBCOAICatalog.jdbcPasswd");
        if (jdbcPasswd == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcPasswd is missing from the properties file");
        }

        rangeQuery = properties.getProperty("JDBCOAICatalog.rangeQuery");
        if (rangeQuery == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.rangeQuery is missing from the properties file");
        }

        rangeSetQuery = properties.getProperty("JDBCOAICatalog.rangeSetQuery");
        if (rangeSetQuery == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.rangeSetQuery is missing from the properties file");
        }

        identifierQuery = properties.getProperty("JDBCOAICatalog.identifierQuery");
        if (identifierQuery == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.identifierQuery is missing from the properties file");
        }

        aboutQuery = properties.getProperty("JDBCOAICatalog.aboutQuery");
        if (aboutQuery != null) {
            aboutValueLabel = properties.getProperty("JDBCOAICatalog.aboutValueLabel");
            if (aboutValueLabel == null) {
                throw new IllegalArgumentException("JDBCOAICatalog.aboutValueLabel is missing from the properties file");
            }
        }

        setSpecQuery = properties.getProperty("JDBCOAICatalog.setSpecQuery");
        setSpecItemLabel = properties.getProperty("JDBCOAICatalog.setSpecItemLabel");
        if (setSpecItemLabel == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.setSpecItemLabel is missing from the properties file");
        }
        setSpecListLabel = properties.getProperty("JDBCOAICatalog.setSpecListLabel");
        if (setSpecListLabel == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.setSpecListLabel is missing from the properties file");
        }
        setNameLabel = properties.getProperty("JDBCOAICatalog.setNameLabel");
        if (setNameLabel == null) {
            throw new IllegalArgumentException("JDBCOAICatalog.setNameLabel is missing from the properties file");
        }

        setDescriptionLabel = properties.getProperty("JDBCOAICatalog.setDescriptionLabel");

        // See if a setQuery exists
        setQuery = properties.getProperty("JDBCOAICatalog.setQuery");
        if (setQuery == null) {
            // if not, load the set Strings from the properties file (if present)
            String propertyPrefix = "Sets.";
            Enumeration propNames = properties.propertyNames();
            while (propNames.hasMoreElements()) {
                String propertyName = (String)propNames.nextElement();
                if (propertyName.startsWith(propertyPrefix)) {
                    sets.add(properties.get(propertyName));
                }
            }
        }

        try {
            Class.forName(jdbcDriverName);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("JDBCOAICatalog.jdbcDriverName is invalid: "
                    + jdbcDriverName);
        }
    }

    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception IdDoesNotExistException the specified oaiIdentifier can't be found
     * @exception NoMetadataFormatsException the specified oaiIdentifier was found
     * but the item is flagged as deleted and thus no schemaLocations (i.e.
     * metadataFormats) can be produced.
     */
    public Vector getSchemaLocations(String oaiIdentifier)
    throws OAIInternalServerError, IdDoesNotExistException, NoMetadataFormatsException {
        StatementResultSet stmtRs = null;
        try {
            stmtRs = new StatementResultSet(populateIdentifierQuery(oaiIdentifier));
            /*
             * Let your recordFactory decide which schemaLocations
             * (i.e. metadataFormats) it can produce from the record.
             * Doing so will preserve the separation of database access
             * (which happens here) from the record content interpretation
             * (which is the responsibility of the RecordFactory implementation).
             */
            if (!stmtRs.next()) {
                throw new IdDoesNotExistException(oaiIdentifier);
            } else {
                /* Make sure the identifierQuery returns the columns you need
                 * (if any) to determine the supported schemaLocations for this item */
                HashMap nativeItem = stmtRs.getColumnValues();
                return getRecordFactory().getSchemaLocations(nativeItem);
            }
        } catch (SQLException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } finally {
            try {
                if (stmtRs != null)
                    stmtRs.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }
        }
    }

    /**
     * insert actual from, until, and set parameters into the rangeQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateRangeQuery(String from, String until, String set)
    throws OAIInternalServerError {
        StringBuffer sb = new StringBuffer();
        StringTokenizer tokenizer;
        if (set == null || set.length() == 0)
            tokenizer = new StringTokenizer(rangeQuery, "\\");
        else
            tokenizer = new StringTokenizer(rangeSetQuery, "\\");

        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid query");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'f':  // what are the chances someone would use this to indicate a form feed?
                sb.append(formatFromDate(from));
                break;
            case 'u':
                sb.append(formatUntilDate(until));
                break;
            case 's':
                sb.append(set);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatFromDate(String date) {
        return formatDate(date);
    }

    /**
     * Extend this class and override this method if necessary.
     */
    protected String formatUntilDate(String date) {
        return formatDate(date);
    }

    /**
     * Change the String from UTC to SQL format
     * If this method doesn't suit your needs, extend this class and override
     * the method rather than change this code directly.
     */
    protected String formatDate(String date) {
        if ("UTC".equals(dateFormat)) {
            return date;
        } else {
            StringBuffer sb = new StringBuffer();
            sb.append(date.substring(5, 7));
            sb.append("/");
            sb.append(date.substring(8));
            sb.append("/");
            sb.append(date.substring(0, 4));
            if (debug) System.out.println("JDBCOAICatalog.formatDate: from " + date + " to " + sb.toString());
            return sb.toString();
        }
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateIdentifierQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(identifierQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateSetSpecQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(setSpecQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * insert actual from, until, and set parameters into the identifierQuery String
     *
     * @param from the OAI from parameter
     * @param until the OAI until paramter
     * @param set the OAI set parameter
     * @return a String containing an SQL query
     */
    private String populateAboutQuery(String oaiIdentifier)
    throws OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(aboutQuery, "\\");
        StringBuffer sb = new StringBuffer();
        if (tokenizer.hasMoreTokens())
            sb.append(tokenizer.nextToken());
        else
            throw new OAIInternalServerError("Invalid identifierQuery");

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            switch (token.charAt(0)) {
            case 'i':
                sb.append(getRecordFactory().fromOAIIdentifier(oaiIdentifier));
                break;
            case 'o':
                sb.append(oaiIdentifier);
                break;
            default: // ignore it
                sb.append("\\");
            sb.append(token.charAt(0));
            }
            sb.append(token.substring(1));
        }
        if (debug) System.out.println(sb.toString());
        return sb.toString();
    }

    /**
     * Retrieve a list of identifiers that satisfy the specified criteria
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * It may seem strange for the map to include both "headers" and "identifiers"
     * since the identifiers can be obtained from the headers. This may be true, but
     * AbstractCatalog.listRecords() can operate quicker if it doesn't
     * need to parse identifiers from the XML headers itself. Better
     * still, do like I do below and override AbstractCatalog.listRecords().
     * AbstractCatalog.listRecords() is relatively inefficient because given the list
     * of identifiers, it must call getRecord() individually for each as it constructs
     * its response. It's much more efficient to construct the entire response in one fell
     * swoop by overriding listRecords() as I've done here.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
    throws NoItemsMatchException,
    OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        StatementResultSet stmtRs = null;
        try {
            /* Get some records from your database */
            stmtRs = new StatementResultSet(populateRangeQuery(from, until, set));
            stmtRs.last();
            int numRows = stmtRs.getRow();
            if (numRows == 0) {
                throw new NoItemsMatchException();
            }
            stmtRs.beforeFirst();
            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count=0; count < maxListSize && stmtRs.next(); ++count) {
                HashMap nativeItem = stmtRs.getColumnValues();
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            /* decide if you're done */
            if (count < numRows) {
                String resumptionId = getResumptionId();
                resumptionResults.put(resumptionId, stmtRs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        0));
            } else {
                stmtRs.close();
                stmtRs = null;
            }
        } catch (SQLException e) {
            if (stmtRs != null) {
                try {
                    stmtRs.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();

        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
        int numRows;
        StatementResultSet stmtRs = null;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        try {
            /* Get some more records from your database */
            stmtRs = (StatementResultSet)resumptionResults.get(resumptionId);
            if (stmtRs == null) {
                throw new BadResumptionTokenException();
            }
            int count;

            if (stmtRs.getRow() != oldCount) {
//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                stmtRs.absolute(oldCount);
            }

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && stmtRs.next(); ++count) {
                HashMap nativeItem = stmtRs.getColumnValues();
                /* Use the RecordFactory to extract header/identifier pairs for each item */
                Iterator setSpecs = getSetSpecs(nativeItem);
                String[] header = getRecordFactory().createHeader(nativeItem, setSpecs);
                headers.add(header[0]);
                identifiers.add(header[1]);
            }

            /* decide if you're done. */
            if (oldCount+count < numRows) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        oldCount));
            } else {
                stmtRs.close();
                stmtRs = null;
            }
        } catch (SQLException e) {
            if (stmtRs != null) {
                try {
                    stmtRs.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param oaiIdentifier the OAI identifier
     * @param metadataPrefix the OAI metadataPrefix
     * @return the <record/> portion of the XML response.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix is not
     * supported by the item.
     * @exception IdDoesNotExistException the oaiIdentifier wasn't found
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
    throws OAIInternalServerError, CannotDisseminateFormatException,
    IdDoesNotExistException {
        StatementResultSet stmtRs = null;
        try {
            stmtRs = new StatementResultSet(populateIdentifierQuery(oaiIdentifier));
            if (!stmtRs.next()) {
                throw new IdDoesNotExistException(oaiIdentifier);
            }
            HashMap nativeItem = stmtRs.getColumnValues();
            return constructRecord(nativeItem, metadataPrefix);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } finally {
            try {
                if (stmtRs != null)
                    stmtRs.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }
        }
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria. Note, though,
     * that unlike the other OAI verb type methods implemented here, both of the
     * listRecords methods are already implemented in AbstractCatalog rather than
     * abstracted. This is because it is possible to implement ListRecords as a
     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
     * I suggest that you override both the AbstractCatalog.listRecords methods
     * here since it will probably improve the performance if you create the
     * response in one fell swoop rather than construct it one GetRecord at a time.
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for a "records" Iterator object
     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception CannotDisseminateFormatException the metadataPrefix isn't
     * supported by the item.
     */
    public Map listRecords(String from, String until, String set, String metadataPrefix)
    throws CannotDisseminateFormatException, NoItemsMatchException,
    OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        StatementResultSet stmtRs = null;

        try {
            stmtRs = new StatementResultSet(populateRangeQuery(from, until, set));
            stmtRs.last();
            int numRows = stmtRs.getRow();
            if (numRows == 0) {
                throw new NoItemsMatchException();
            }
            stmtRs.beforeFirst();
            int count;

//          if (debug) System.out.println("JDBCOAICatalog.listRecords: numRows=" + numRows);

            /* load the records ArrayList */
            for (count=0; count < maxListSize && stmtRs.next(); ++count) {
                HashMap nativeItem = stmtRs.getColumnValues();
                String record = constructRecord(nativeItem, metadataPrefix);
                records.add(record);
            }

            /* decide if you're done */
            if (count < numRows) {
                String resumptionId = getResumptionId();
                resumptionResults.put(resumptionId, stmtRs);

                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        0));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSbSb.toString()));
            } else {
                stmtRs.close();
                stmtRs = null;
            }
        } catch (SQLException e) {
            if (stmtRs != null) {
                try {
                    stmtRs.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception OAIInternalServerError signals an http status code 500 problem
     * @exception BadResumptionTokenException the value of the resumptionToken argument
     * is invalid or expired.
     */
    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        purge(); // clean out old resumptionTokens

        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
        String resumptionId;
        int oldCount;
        int numRows;
        String metadataPrefix;
        StatementResultSet stmtRs = null;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        try {
            /* Get some more records from your database */
            stmtRs = (StatementResultSet) resumptionResults.get(resumptionId);
            if (stmtRs == null) {
                throw new BadResumptionTokenException();
            }

            if (stmtRs.getRow() != oldCount) {
//              System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                stmtRs.absolute(oldCount);
            }

            int count;

            /* load the headers and identifiers ArrayLists. */
            for (count = 0; count < maxListSize && stmtRs.next(); ++count) {
                try {
                    HashMap nativeItem = stmtRs.getColumnValues();
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                } catch (CannotDisseminateFormatException e) {
                    /* the client hacked the resumptionToken beyond repair */
                    throw new BadResumptionTokenException();
                }
            }

            /* decide if you're done */
            if (oldCount+count < numRows) {
                /*****************************************************************
                 * Construct the resumptionToken String however you see fit.
                 *****************************************************************/
                StringBuffer resumptionTokenSb = new StringBuffer();
                resumptionTokenSb.append(resumptionId);
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(oldCount + count));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(Integer.toString(numRows));
                resumptionTokenSb.append("!");
                resumptionTokenSb.append(metadataPrefix);

                /*****************************************************************
                 * Use the following line if you wish to include the optional
                 * resumptionToken attributes in the response. Otherwise, use the
                 * line after it that I've commented out.
                 *****************************************************************/
                listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                        numRows,
                        oldCount));
                //          listRecordsMap.put("resumptionMap",
                //                                 getResumptionMap(resumptionTokenSb.toString()));
            } else {
                stmtRs.close();
                stmtRs = null;
            }
        } catch (SQLException e) {
            if (stmtRs != null) {
                try {
                    stmtRs.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(HashMap nativeItem, String metadataPrefix)
    throws CannotDisseminateFormatException, OAIInternalServerError {
        String schemaURL = null;
        Iterator setSpecs = getSetSpecs(nativeItem);
        Iterator abouts = getAbouts(nativeItem);

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * Retrieve a list of sets that satisfy the specified criteria
     *
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception OAIBadRequestException signals an http status code 400 problem
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
        StatementResultSet stmtRs = null;
        if (setQuery == null) {
            if (sets.size() == 0)
                throw new NoSetHierarchyException();
            Map listSetsMap = new HashMap();
            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            try {
                if (debug) System.out.println(setQuery);

                /* Get some records from your database */
                stmtRs = new StatementResultSet(setQuery);
                stmtRs.last();
                int numRows = stmtRs.getRow();
                stmtRs.beforeFirst();
                int count;

//              if (debug) System.out.println("JDBCOAICatalog.listSets: numRows=" + numRows);

                /* load the sets ArrayLists. */
                for (count=0; count < maxListSize && stmtRs.next(); ++count) {
                    /* Use the RecordFactory to extract header/set pairs for each item */
                    HashMap nativeItem = stmtRs.getColumnValues();
                    sets.add(getSetXML(nativeItem));
                    if (debug) System.out.println("JDBCOAICatalog.listSets: adding an entry");
                }

                /* decide if you're done */
                if (count < numRows) {
                    String resumptionId = getResumptionId();
                    /*****************************************************************
                     * Note that storing the ResultSet in the resumptionResult
                     * means the token can't be reused.
                     *****************************************************************/
                    resumptionResults.put(resumptionId, stmtRs);

                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(count));
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            0));
                } else {
                    stmtRs.close();
                    stmtRs = null;
                }
            } catch (SQLException e) {
                if (stmtRs != null) {
                    try {
                        stmtRs.close();
                    } catch (SQLException e1) {
                        e1.printStackTrace();
                    }
                }
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * Retrieve the next set of sets associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listSets() Map result.
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    public Map listSets(String resumptionToken)
    throws OAIInternalServerError, BadResumptionTokenException {
        StatementResultSet stmtRs = null;

        if (setQuery == null) {
            throw new BadResumptionTokenException();
        } else {
            purge(); // clean out old resumptionTokens
            Map listSetsMap = new HashMap();
            ArrayList sets = new ArrayList();

            /**********************************************************************
             * parse your resumptionToken and look it up in the resumptionResults,
             * if necessary
             **********************************************************************/
            StringTokenizer tokenizer = new StringTokenizer(resumptionToken, "!");
            String resumptionId;
            int oldCount;
            int numRows;
            try {
                resumptionId = tokenizer.nextToken();
                oldCount = Integer.parseInt(tokenizer.nextToken());
                numRows = Integer.parseInt(tokenizer.nextToken());
            } catch (NoSuchElementException e) {
                throw new BadResumptionTokenException();
            }

            try {
                /* Get some more records from your database */
                stmtRs = (StatementResultSet)resumptionResults.get(resumptionId);
                if (stmtRs == null) {
                    throw new BadResumptionTokenException();
                }

                if (stmtRs.getRow() != oldCount) {
//                  System.out.println("JDBCOAICatalog.listIdentifiers: reuse of old resumptionToken?");
                    stmtRs.absolute(oldCount);
                }

                int count;

                /* load the sets ArrayLists. */
                for (count = 0; count < maxListSize && stmtRs.next(); ++count) {
                    HashMap nativeItem = stmtRs.getColumnValues();
                    /* Use the RecordFactory to extract set for each item */
                    sets.add(getSetXML(nativeItem));
                }

                /* decide if you're done. */
                if (oldCount+count < numRows) {
                    /*****************************************************************
                     * Construct the resumptionToken String however you see fit.
                     *****************************************************************/
                    StringBuffer resumptionTokenSb = new StringBuffer();
                    resumptionTokenSb.append(resumptionId);
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(oldCount + count));
                    resumptionTokenSb.append("!");
                    resumptionTokenSb.append(Integer.toString(numRows));

                    /*****************************************************************
                     * Use the following line if you wish to include the optional
                     * resumptionToken attributes in the response. Otherwise, use the
                     * line after it that I've commented out.
                     *****************************************************************/
                    listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                            numRows,
                            oldCount));
                } else {
                    stmtRs.close();
                    stmtRs = null;
                }
            } catch (SQLException e) {
                if (stmtRs != null) {
                    try {
                        stmtRs.close();
                    } catch (SQLException e1) {
                        e1.printStackTrace();
                    }
                }
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }

            listSetsMap.put("sets", sets.iterator());
            return listSetsMap;
        }
    }

    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(HashMap nativeItem)
    throws OAIInternalServerError {
        StatementResultSet stmtRs = null;
        try {
            ArrayList setSpecs = new ArrayList();
            if (setSpecQuery != null) {
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                stmtRs = new StatementResultSet(populateSetSpecQuery(oaiIdentifier));
                while (stmtRs.next()) {
                    HashMap setMap = stmtRs.getColumnValues();
                    setSpecs.add(setMap.get(setSpecItemLabel).toString());
                }
            }
            return setSpecs.iterator();
        } catch (SQLException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } finally {
            try {
                stmtRs.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }
        }
    }

    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(HashMap nativeItem)
    throws OAIInternalServerError {
        StatementResultSet stmtRs = null;
        try {
            ArrayList abouts = new ArrayList();
            if (aboutQuery != null) {
                RecordFactory rf = getRecordFactory();
                String oaiIdentifier = rf.getOAIIdentifier(nativeItem);
                stmtRs = new StatementResultSet(populateAboutQuery(oaiIdentifier));
                while (stmtRs.next()) {
                    HashMap aboutMap = stmtRs.getColumnValues();
                    abouts.add(aboutMap.get(aboutValueLabel));
                }
            }
            return abouts.iterator();
        } catch (SQLException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } finally {
            try {
                stmtRs.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }
        }
    }

    /**
     * Extract &lt;set&gt; XML string from setItem object
     *
     * @param setItem individual set instance in native format
     * @return an XML String containing the XML &lt;set&gt; content
     */
    public String getSetXML(HashMap setItem)
    throws IllegalArgumentException {
        String setSpec = getSetSpec(setItem);
        String setName = getSetName(setItem);
        String setDescription = getSetDescription(setItem);

        StringBuffer sb = new StringBuffer();
        sb.append("<set>");
        sb.append("<setSpec>");
        sb.append(OAIUtil.xmlEncode(setSpec));
        sb.append("</setSpec>");
        sb.append("<setName>");
        sb.append(OAIUtil.xmlEncode(setName));
        sb.append("</setName>");
        if (setDescription != null) {
            sb.append("<setDescription>");
            sb.append(OAIUtil.xmlEncode(setDescription));
            sb.append("</setDescription>");
        }
        sb.append("</set>");
        return sb.toString();
    }

    /**
     * get the setSpec XML string. Extend this class and override this method
     * if the setSpec can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setSpec&gt; content
     */
    protected String getSetSpec(HashMap setItem) {
        try {
            return URLEncoder.encode((String)setItem.get(setSpecListLabel), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return "UnsupportedEncodingException";
        }
    }

    /**
     * get the setName XML string. Extend this class and override this method
     * if the setName can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setName&gt; content
     */
    protected String getSetName(HashMap setItem) {
        return (String)setItem.get(setNameLabel);
    }

    /**
     * get the setDescription XML string. Extend this class and override this method
     * if the setDescription can't be directly taken from the result set as a String
     *
     * @param setItem
     * @return an XML String containing the &lt;setDescription&gt; content
     */
    protected String getSetDescription(HashMap setItem) {
        if (setDescriptionLabel == null)
            return null;
        return (String)setItem.get(setDescriptionLabel);
    }

    private Connection getNewConnection() throws SQLException {
        // open the connection
        return DriverManager.getConnection(jdbcURL, jdbcLogin, jdbcPasswd);
    }

    private Connection getConnection() throws SQLException {
        if (persistentConnection != null) {
            if (persistentConnection.isClosed()) {
                System.out.println("Persistent connection has expired.");
                persistentConnection = getNewConnection();
            }
        } else {
            persistentConnection = getNewConnection();
        }
        return persistentConnection;
    }

    /**
     * close the repository
     */
    public void close() {
    }

    /**
     * Purge tokens that are older than the configured time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date now = new Date();
        Iterator keySet = resumptionResults.keySet().iterator();
        while (keySet.hasNext()) {
            String key = (String)keySet.next();
            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            String key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }

    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a String version of the current time
     */
    private synchronized static String getResumptionId() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerException;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXParseException;

import ORG.oclc.oai.server.crosswalk.CrosswalkItem;
import ORG.oclc.oai.server.crosswalk.NodePassThruCrosswalk;
import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * NodeRecordFactory converts native XML "items" to "record" Strings.
 */
public class NodeRecordFactory extends RecordFactory {
//     private static Logger logger = Logger.getLogger(NodeRecordFactory.class);
//     static {
// 	BasicConfigurator.configure();
//     }
    private static Element xmlnsEl = null;
    private static DocumentBuilderFactory factory = null;
    static {
	try {
	    factory =
		DocumentBuilderFactory.newInstance();
	    factory.setNamespaceAware(true);
	    DocumentBuilder builder = factory.newDocumentBuilder();
	    DOMImplementation impl = builder.getDOMImplementation();
	    Document xmlnsDoc =
		impl.createDocument(
				    "http://www.oclc.org/research/software/oai/harvester",
				    "harvester:xmlnsDoc",
				    null);
	    xmlnsEl = xmlnsDoc.getDocumentElement();
// 	    xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
// 				   "xmlns:mx",
// 				   "http://www.loc.gov/MARC21/slim");
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                                   "xmlns:xsd",
                                   "http://www.w3.org/2001/XMLSchema");
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                                   "xmlns:srw",
                                   "http://www.loc.gov/zing/srw/");
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
				   "xmlns:oai",
				   "http://www.openarchives.org/OAI/2.0/");
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                                   "xmlns:explain",
                                   "http://explain.z3950.org/dtd/2.0/");
	} catch (Exception e) {
	    e.printStackTrace();
	}
    }
    
    public NodeRecordFactory(Properties properties)
	throws IllegalArgumentException {
	this(properties, getCrosswalkMap(properties.getProperty("SRUOAICatalog.sruURL"), false));
    }
    
    /**
     * Construct an NodeRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public NodeRecordFactory(Properties properties, HashMap crosswalkMap)
	throws IllegalArgumentException {
	super(crosswalkMap);
    }

    private static HashMap getCrosswalkMap(String sruURL, boolean enrich)
	throws IllegalArgumentException {
	try {
	    DocumentBuilder parser = factory.newDocumentBuilder();
	    System.out.println("sruURL=" + sruURL);
	    Document explainDoc = parser.parse(sruURL);
	    System.out.println("explainDoc=" + explainDoc);
 	    NodeList schemas = XPathAPI.selectNodeList(explainDoc, "/srw:explainResponse/srw:record/srw:recordData/explain:explain/explain:schemaInfo/explain:schema", xmlnsEl);
	    
	    // Load the formats the repository supports directly
	    HashMap crosswalkMap = new HashMap();
	    for (int i=0; i<schemas.getLength(); ++i) {
		Object[] crosswalkItem = crosswalkItemFactory(schemas.item(i));
		for (int j=0; j<crosswalkItem.length; ++j) {
		    CrosswalkItem currentItem = (CrosswalkItem)crosswalkItem[j];
		    // 		logger.debug(currentItem.toString());
		    Object key = currentItem.getMetadataPrefix();
		    CrosswalkItem storedItem = (CrosswalkItem)crosswalkMap.get(key);
		    if (storedItem == null || (currentItem.getRank() < storedItem.getRank())) {
			crosswalkMap.put(key, currentItem);
		    }
		}
	    }

	    HashMap moreCrosswalkMap = new HashMap();
// 	    if (enrich) {
// 		System.out.println("enriching...");
// 		// Enrich the crosswalkMap with formats we can create w/crosswalks
// 		Iterator oldCrosswalkItems = crosswalkMap.values().iterator();
// 		while (oldCrosswalkItems.hasNext()) {
// 		    CrosswalkItem oldCrosswalkItem = (CrosswalkItem)oldCrosswalkItems.next();
// 		    String oldSchema = oldCrosswalkItem.getSchema();
// 		    String nativeRecordSchema = oldCrosswalkItem.getNativeRecordSchema();
// 		    moreCrosswalkMap.putAll(getCrosswalkMap(oldSchema,
// 							    nativeRecordSchema,
// 							    crosswalkMap,
// 							    moreCrosswalkMap));
// 		}
// 	    }
	    // Now, combine the lists with the originals taking precidence
	    moreCrosswalkMap.putAll(crosswalkMap);
	    return moreCrosswalkMap;
	} catch (Exception e) {
	    e.printStackTrace();
	    throw new IllegalArgumentException(sruURL);
	}
    }

//    private static HashMap getCrosswalkMap(String oldSchema,
//				    String nativeRecordSchema,
//				    HashMap crosswalkMap,
//				    HashMap moreCrosswalkMap)
//	throws OAIInternalServerError, SAXException, IOException,
//	       TransformerException {
//	HashMap hashMap = new HashMap();
//	return hashMap;
//    }

//     public ArrayList querySourceSchema(String nativeRecordSchema,
// 				       String sourceSchema)
// 	throws OAIInternalServerError, SAXException, IOException,
// 	       TransformerException {
// 	try {
// 	    ArrayList list = new ArrayList();
// 	    StringBuffer sb = new StringBuffer(getCentralERRoLURL());
// 	    sb.append("/schemaTrans.oclc.org.sru?query=schemaTrans.source+%3D+%22");
// 	    sb.append(URLEncoder.encode(sourceSchema, "UTF-8"));
// 	    sb.append("%22&version=1.1&operation=searchRetrieve&maximumRecords=10&startRecord=1&resultSetTTL=0&recordPacking=xml&recordXPath=&sortKeys=");
// 	    Document sruDoc = parser.parse(sb.toString());
// 	    NodeList nodeList = XPathAPI.selectNodeList(sruDoc, "/srw:searchRetrieveResponse/srw:records/srw:record/srw:recordData/mets:mets[mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:div[@LABEL='infoURI']]", xmlnsEl);

// 	    for (int i=0; i<nodeList.getLength(); ++i) {
// 		Node recordNode = nodeList.item(i);
// 		String infoURIFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:div[@LABEL='infoURI']/mets:fptr/@FILEID", xmlnsEl).str();
//  		String infoURI = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+infoURIFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();
// 		String namespaceFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:div[@LABEL='namespace']/mets:fptr/@FILEID", xmlnsEl).str();
// 		String namespace = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+namespaceFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();
// 		String schemaFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='target']/mets:div[@LABEL='Application']/mets:fptr/@FILEID", xmlnsEl).str();
// 		String schema = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+schemaFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();
// 		String crosswalkFID = XPathAPI.eval(recordNode, "mets:structMap/mets:div/mets:div[@LABEL='crosswalk']/mets:div[@LABEL='Application']/mets:fptr/@FILEID", xmlnsEl).str();
// 		String crosswalk = XPathAPI.eval(recordNode, "mets:fileSec/mets:fileGrp[@USE='Application']/mets:file[@ID='"+crosswalkFID+"']/mets:FLocat/@xlink:href", xmlnsEl).str();

// 		String metadataPrefix = infoURI.substring("info:ofi/fmt:xml:xsd:".length());
// 		list.add(new CrosswalkItem(nativeRecordSchema,
// 					   metadataPrefix,
// 					   schema,
// 					   namespace,
// 					   XSLCrosswalk.class,
// 					   crosswalk
// 					   ));
// 	    }
// 	    return list;
// 	} catch (UnsupportedEncodingException e) {
// 	    e.printStackTrace();
// 	    throw new OAIInternalServerError(e.getMessage());
// 	}
//     }
    
    public static Object[] crosswalkItemFactory(Node explainSchemaNode)
	throws TransformerException, OAIInternalServerError {
	ArrayList crosswalkItemList = new ArrayList();
 	String nativeRecordSchema = XPathAPI.eval(explainSchemaNode, "@identifier", xmlnsEl).str();
 	String metadataPrefix = XPathAPI.eval(explainSchemaNode, "@name", xmlnsEl).str();
	String schema = XPathAPI.eval(explainSchemaNode, "@location", xmlnsEl).str();
	String metadataNamespace = null;
	try {
	    DocumentBuilder parser = factory.newDocumentBuilder();
	    Document schemaDoc = parser.parse(schema);
	    metadataNamespace = XPathAPI.eval(schemaDoc, "/xsd:schema/@targetNamespace", xmlnsEl).str();
	} catch (SAXParseException e) {
// 	    logger.debug("assume unreadable schema is for empty namespace", e);
	    metadataNamespace = "";
	} catch (Exception e) {
// 	    logger.warn("Problem obtaining namespace", e);
	    System.out.println("Failed to get schema: " + schema);
	    e.printStackTrace();
	    metadataNamespace = "";
	}
	CrosswalkItem crosswalkItem = new CrosswalkItem(nativeRecordSchema, metadataPrefix, schema, metadataNamespace, NodePassThruCrosswalk.class);
	crosswalkItemList.add(crosswalkItem);
// 	logger.debug("Service_srw2oai.crosswalkItemFactory: metadataPrefix=" + metadataPrefix);
// 	logger.debug("Service_srw2oai.crosswalkItemFactory: nativeRecordSchema=" + crosswalkItem.getNativeRecordSchema());
// 	if (nativeRecordSchema.equals("info:srw/schema/1/marcxml-v1.1")) {
// 	}
	return crosswalkItemList.toArray();
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
	return identifier;
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
	return getLocalIdentifier(nativeItem);
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
//  	logger.debug("getLocalIdentifier: nativeItem=" + nativeItem);
	HashMap hashMap = (HashMap)nativeItem;
	Node dataNode = (Node) hashMap.get("header");
// 	MessageElement messageElement = dataNode.get_any()[0];
//	StringBuffer sb = new StringBuffer();
	try {
// 	    Element recordEl = messageElement.getAsDOM();
	    Element recordEl = (Element)dataNode;
// 	    logger.debug("NodeRecordFactory.getLocalIdentifier: recordEl=" + recordEl);
	    Node identifierNode =
		XPathAPI.selectSingleNode(
					  recordEl,
					  "/oai:header/oai:identifier",
					  xmlnsEl);
// 	    logger.debug("NodeRecordFactory.getLocalIdentifier: identifierNode=" + identifierNode);
	    if (identifierNode != null) {
		return XPathAPI.eval(identifierNode, "string()").str();
	    }
	} catch (Exception e) {
	    e.printStackTrace();
	}
	return null;
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException {
	HashMap hashMap = (HashMap)nativeItem;
	Node dataNode = (Node) hashMap.get("header");
// 	MessageElement messageElement = dataNode.get_any()[0];
	try {
// 	    Element recordEl = messageElement.getAsDOM();
	    Element recordEl = (Element)dataNode;
	    Node datestampNode =
		XPathAPI.selectSingleNode(
					  recordEl,
					  "/oai:header/oai:datestamp",
					  xmlnsEl);
	    if (datestampNode != null) {
		return XPathAPI.eval(datestampNode, "string()").str();
	    }
	} catch (Exception e) {
	    e.printStackTrace();
	}
	return null;
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException {
	//         List setSpecs = (List)((HashMap)nativeItem).get("setSpecs");
	//         if (setSpecs != null) 
	//             return setSpecs.iterator();
	//         else
	return null;
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem)
	throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(
			      Object nativeItem,
			      String schemaLocation,
			      String metadataPrefix) {
	return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

// import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;

import ORG.oclc.oai.server.crosswalk.Crosswalk;
import ORG.oclc.oai.server.crosswalk.CrosswalkItem;
import ORG.oclc.oai.server.crosswalk.Crosswalks;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.util.OAIUtil;

/**
 * RecordFactory is responsible for pulling various pieces of information from native records
 * such as the identifier, datestamp, and sets. Note that the Crosswalk implementations
 * are responsible for the creating the &lt;metadata&gt; part of the response from
 * native records.
 */
public abstract class RecordFactory {
    public static final boolean debug=false;
//     private boolean encodeSetSpec = true;
    
    /**
     * Container for the crosswalk(s) supported by this factory
     */
    private Crosswalks crosswalks;

    /**
     * Construct a RecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     *
     * @param properties Contains information to configure the factory;
     *                   specifically, the names of crosswalk(s) supported
     */
    public RecordFactory(Properties properties) {
//         if ("false".equals(properties.getProperty("RecordFactory.encodeSetSpec"))) {
//             encodeSetSpec = false;
//         }
	crosswalks = new Crosswalks(properties);
    }

    public RecordFactory(HashMap crosswalkMap) {
	crosswalks = new Crosswalks(crosswalkMap);
    }

    /**
     * Retrieve the crosswalk(s) container
     *
     * @return Crosswalk(s) container
     */
    public Crosswalks getCrosswalks() { return crosswalks; }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier
     * @return local identifier
     */
    public abstract String fromOAIIdentifier(String identifier);

    /**
     * Get a list of supported schemaLocations for the specified native record.
     * @param nativeItem native database record
     * @return A Vector containing all the schemaLocations this record can support.
     * @exception NoMetadatdaFormatsException This record doesn't support any of the
     * available schemaLocations for this repository.
     */
    public Vector getSchemaLocations(Object nativeItem)
	throws NoMetadataFormatsException {
	if (isDeleted(nativeItem)) {
	    throw new NoMetadataFormatsException();
	}
	Vector v = new Vector();
	Iterator iterator = getCrosswalks().iterator();
	while (iterator.hasNext()) {
	    Map.Entry entry = (Map.Entry)iterator.next();
	    CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
	    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
	    if (crosswalk.isAvailableFor(nativeItem)) {
		v.add(crosswalk.getSchemaLocation());
	    }
	}
	return v;
    }

    /**
     * Convert a native "item" to a "record" String. Use this version of createHeader
     * if the setSpecs are derived from the nativeItem itself.
     *
     * @param nativeItem the native record.
     * @return String[0] = "header" XML string String[1] = oai-identifier.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     */
    public String[] createHeader(Object nativeItem)
	throws IllegalArgumentException {
	    return createHeader(
				getOAIIdentifier(nativeItem),
				getDatestamp(nativeItem),
				getSetSpecs(nativeItem),
				isDeleted(nativeItem));
	}
    
    /**
     * Convert a native "item" to a "record" String. Use this version of createHeader
     * if the setSpecs are supplied from a source other than the nativeItem itself.
     *
     * @param nativeItem the native record.
     * @param setSpecs a setSpec iterator
     * @return String[0] = "header" XML string String[1] = oai-identifier.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     */
    public String[] createHeader(Object nativeItem, Iterator setSpecs)
	throws IllegalArgumentException {
	    if (setSpecs == null) {
		return createHeader(nativeItem);
	    } else {
		return createHeader(
				    getOAIIdentifier(nativeItem),
				    getDatestamp(nativeItem),
				    setSpecs,
				    isDeleted(nativeItem));
	    }
	}
    
    /**
     * Create a "record" string from the specified components.
     *
     * @param identifier the OAI identifier
     * @param datestamp the datestamp
     * @param setSpecs 
     * @param isDeleted a flag indicating the status of the record.
     * @return String[0] = "header" XML string String[1] = oai-identifier.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     */
    public static String[] createHeader(String identifier,
				 String datestamp, Iterator setSpecs, boolean isDeleted)
	throws IllegalArgumentException {
	    StringBuffer xmlHeader = new StringBuffer();
	    xmlHeader.append("<header");
	    if (isDeleted) {
		xmlHeader.append(" status=\"deleted\"");
	    }
	    xmlHeader.append("><identifier>");
	    xmlHeader.append(OAIUtil.xmlEncode(identifier));
	    xmlHeader.append("</identifier><datestamp>");
	    xmlHeader.append(datestamp);
	    xmlHeader.append("</datestamp>");
	    if (setSpecs != null) {
		while (setSpecs.hasNext()) {
		    xmlHeader.append("<setSpec>");
//                     try {
//                         if (encodeSetSpec) {
//                             xmlHeader.append(URLEncoder.encode((String)setSpecs.next(), "UTF-8"));
//                         } else {
                            xmlHeader.append((String)setSpecs.next());
//                         }
                        
//                     } catch (UnsupportedEncodingException e) {
//                         e.printStackTrace();
//                         throw new OAIInternalServerError(e.getMessage());
//                     }
		    xmlHeader.append("</setSpec>");
		}
	    }
	    xmlHeader.append("</header>");
	    return new String[] {
		xmlHeader.toString(),
		identifier
	    };
	}

    /**
     * Create the &lt;record&gt; for the given record and metadataFormat selection. Use this
     * version of create if the setSpecs can be derived from the nativeItem itself.
     *
     * @param nativeItem the native record
     * @param schemaURL the schemaURL desired for the response
     * @param metadataPrefix 
     * @return a String containing the OAI record response.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @throws CannotDisseminateFormatException 
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public String create(Object nativeItem, String schemaURL, String metadataPrefix)
	throws IllegalArgumentException, CannotDisseminateFormatException {
	return create(nativeItem, schemaURL, metadataPrefix, (Iterator)null, (Iterator)null);
    }

    /**
     * Create the &lt;record&gt; for the given record and metadataFormat selection.
     * Use this version of create if the setSpecs are derived from a source other
     * other than the nativeItem itself.
     *
     * @param nativeItem the native record
     * @param schemaURL the schemaURL desired for the response
     * @param metadataPrefix 
     * @param setSpecs 
     * @param abouts 
     * @return a String containing the OAI record response.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @throws CannotDisseminateFormatException 
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public String create(Object nativeItem, String schemaURL, String metadataPrefix,
			 Iterator setSpecs, Iterator abouts)
	throws IllegalArgumentException, CannotDisseminateFormatException {
        if (isDeleted(nativeItem)) {
            StringBuffer sb = new StringBuffer("<record>");
            sb.append(createHeader(nativeItem)[0]);
            sb.append("</record>");
            return sb.toString();
        }
        String result = quickCreate(nativeItem, schemaURL, metadataPrefix);
        if (result == null) {
            if (setSpecs == null) {
                setSpecs = getSetSpecs(nativeItem);
            }
            if (abouts == null) {
                abouts = getAbouts(nativeItem);
            }
            result = create(nativeItem, schemaURL, metadataPrefix,
                            getOAIIdentifier(nativeItem),
                            getDatestamp(nativeItem),
                            setSpecs,
                            abouts,
                            isDeleted(nativeItem));
        }
      
        return result;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record.
     * @param nativeItem the native record
     * @param schemaURL the schemaURL desired for the response
     * @param metadataPrefix 
     * @return a String containing the OAI record response.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @throws CannotDisseminateFormatException 
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public abstract String quickCreate(Object nativeItem, String schemaURL, String metadataPrefix)
	throws IllegalArgumentException, CannotDisseminateFormatException;

    public String create(Object nativeItem, String schemaURL, String identifier,
            String datestamp, Iterator setSpecs, Iterator abouts, boolean isDeleted)
    throws IllegalArgumentException, CannotDisseminateFormatException {
        return create(nativeItem, schemaURL, null, identifier, datestamp,
                setSpecs, abouts, isDeleted);
    }
    
    /**
     * Convert a native "item" to a "record" object.
     *
     * @param nativeItem native "Item"
     * @param schemaURL the schemaURL of the desired metadataFormat
     *                  or NULL for a Record object containing all
     *                  the supported Crosswalk(s)
     * @return "record" String
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public String create(Object nativeItem, String schemaURL,
            String metadataPrefix, String identifier,
            String datestamp, Iterator setSpecs, Iterator abouts, boolean isDeleted)
    throws IllegalArgumentException, CannotDisseminateFormatException {
        if (debug) System.out.println("RecordFactory.create");
        StringBuffer xmlRec = new StringBuffer();
        xmlRec.append("<record><header");
        if (isDeleted) {
            xmlRec.append(" status=\"deleted\"");
        }
        xmlRec.append("><identifier>");
        xmlRec.append(OAIUtil.xmlEncode(identifier));
        xmlRec.append("</identifier><datestamp>");
        xmlRec.append(datestamp);
        xmlRec.append("</datestamp>");
        if (setSpecs != null) {
            while (setSpecs.hasNext()) {
                xmlRec.append("<setSpec>");
//              try {
//              if (encodeSetSpec) {
//              xmlRec.append(URLEncoder.encode((String)setSpecs.next(), "UTF-8"));
//              } else {
                xmlRec.append((String)setSpecs.next());
//              }
//              } catch (UnsupportedEncodingException e) {
//              e.printStackTrace();
//              throw new OAIInternalServerError(e.getMessage());
//              }
                xmlRec.append("</setSpec>");
            }
        }
        xmlRec.append("</header>");
        if (debug) System.out.println("RecordFactory.create: header finished");
        if (!isDeleted) {
            if (debug) System.out.println("RecordFactory.create: starting metadata");
            xmlRec.append("<metadata>");
            Iterator iterator = getCrosswalks().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry)iterator.next();
                String itemPrefix = (String) entry.getKey();
                CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
                Crosswalk crosswalk = crosswalkItem.getCrosswalk();
                if (debug) System.out.println("RecordFactory.create: crosswalk=" + crosswalk);
                if (schemaURL == null
                        || (metadataPrefix == null && crosswalk.getSchemaURL().equals(schemaURL))
                        || (metadataPrefix != null && itemPrefix.equals(metadataPrefix))) {
                    xmlRec.append(crosswalk.createMetadata(nativeItem));
                    break;
                }
            }
            xmlRec.append("</metadata>");
            if (debug) System.out.println("RecordFactory.create: finished metadata");
            if (abouts != null) {
                while (abouts.hasNext()) {
                    xmlRec.append("<about>");
                    xmlRec.append((String)abouts.next());
                    xmlRec.append("</about>");
                }
            }
        }
        xmlRec.append("</record>");
        if (debug) System.out.println("RecordFactory.create: return=" + xmlRec.toString());
        return xmlRec.toString();
    }

    /**
     * Create the &lt;record&gt; for the given record and metadataFormat selection. Use this
     * version of create if the setSpecs can be derived from the nativeItem itself.
     *
     * @param nativeItem the native record
     * @param schemaURL the schemaURL desired for the response
     * @param metadataPrefix 
     * @return a String containing the OAI record response.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @throws CannotDisseminateFormatException 
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public String createMetadata(Object nativeItem, String schemaURL, String metadataPrefix)
	throws IllegalArgumentException, CannotDisseminateFormatException {
	return createMetadata(nativeItem, schemaURL, metadataPrefix, (Iterator)null, (Iterator)null);
    }

    /**
     * Create the &lt;record&gt; for the given record and metadataFormat selection.
     * Use this version of create if the setSpecs are derived from a source other
     * other than the nativeItem itself.
     *
     * @param nativeItem the native record
     * @param schemaURL the schemaURL desired for the response
     * @param metadataPrefix 
     * @param setSpecs 
     * @param abouts 
     * @return a String containing the OAI record response.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @throws CannotDisseminateFormatException 
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public String createMetadata(Object nativeItem, String schemaURL, String metadataPrefix,
			 Iterator setSpecs, Iterator abouts)
	throws IllegalArgumentException, CannotDisseminateFormatException {
        if (isDeleted(nativeItem)) {
            throw new CannotDisseminateFormatException("Record is deleted.");
        }
        String result = quickCreateMetadata(nativeItem, schemaURL, metadataPrefix);
        if (result == null) {
            if (setSpecs == null) {
                setSpecs = getSetSpecs(nativeItem);
            }
            if (abouts == null) {
                abouts = getAbouts(nativeItem);
            }
            result = createMetadata(nativeItem, schemaURL,
                            isDeleted(nativeItem));
        }
      
        return result;
    }

    /**
     * Allows classes that implement RecordFactory to override the default createMetadata() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record.
     * @param nativeItem the native record
     * @param schemaURL the schemaURL desired for the response
     * @param metadataPrefix 
     * @return a String containing the OAI record response.
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public String quickCreateMetadata(Object nativeItem, String schemaURL, String metadataPrefix)
	throws IllegalArgumentException {
        return null;
    }

    /**
     * Convert a native "item" to a "record" object.
     *
     * @param nativeItem native "Item"
     * @param schemaURL the schemaURL of the desired metadataFormat
     *                  or NULL for a Record object containing all
     *                  the supported Crosswalk(s)
     * @return "record" String
     * @exception IllegalArgumentException One of the header components for this record is bad.
     * @exception This nativeItem doesn't support the specified metadataPrefix
     */
    public String createMetadata(Object nativeItem, String schemaURL, boolean isDeleted)
	throws IllegalArgumentException, CannotDisseminateFormatException {
	StringBuffer xmlRec = new StringBuffer();
	if (isDeleted) {
            throw new CannotDisseminateFormatException("Record is deleted");
	}
        Iterator iterator = getCrosswalks().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry)iterator.next();
            CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
	    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
            if (schemaURL == null
                || crosswalk.getSchemaURL().equals(schemaURL)) {
                xmlRec.append(crosswalk.createMetadata(nativeItem));
            }
        }
	return xmlRec.toString();
    }

    /**
     * Construct an OAI identifier from the native record
     *
     * Caveat: This brazenly assumes the record's local identifier
     * exists somewhere within the native, or can be
     * generated on the fly.
     *
     * @param nativeItem the native record
     * @return String OAI identifier
     */
    public abstract String getOAIIdentifier(Object nativeItem);

    /**
     * Extract the local identifier from the native record.
     * Since I forgot to add this originally, it is a roundabout
     * way to get this. It's a good idea to override this method
     * to do it more directly.
     *
     * @param nativeItem the native record
     * @return String local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
	return fromOAIIdentifier(getOAIIdentifier(nativeItem));
    }
    
    /**
     * Construct an OAI datestamp from the native record
     *
     * Caveat: This brazenly assumes the record's datestamp
     * exists somewhere within the native record, or can be
     * generated on the fly.
     *
     * @param nativeItem the native record
     * @return String OAI identifier
     */
    public abstract String getDatestamp(Object nativeItem);

    /**
     * Get an Iterator containing all the setSpecs for this native record.
     * Return null if the setSpecs are derived from
     * a source other than the nativeItem itself. In this case, the AbstractCatalog
     * is responsible for obtaining the setSpecs which can be passed to the
     * create() and createHeader() methods along with the nativeItem.
     *
     * @param nativeItem the native record
     * @return an Iterator containing setSpec String
     */
    public abstract Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException;
    /**
     * Get a boolean indicating if this native record is deleted
     *
     * @param nativeItem the native record
     * @return true if record is deleted, false otherwise.
     */
    public abstract boolean isDeleted(Object nativeItem);
    /**
     * Get an Iterator containing all the &lt;about&gt; entries for this native record
     * Return null if the abouts are derived from a source outside the native record itself.
     * In this case, the AbstractCatalog is responsible for obtaining the abouts which
     * can be passed to the create() method along with the nativeItem.
     *
     * @param nativeItem the native record
     * @return an Iterator containing &lt;about&gt; XML String
     */
    public abstract Iterator getAbouts(Object nativeItem);
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import ORG.oclc.oai.server.verb.BadArgumentException;
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;

public class SRUOAICatalog extends AbstractCatalog {
    private static final boolean debug = false;
    private String sruURL;
    private String sortKeys = "";
    protected int maxListSize;
    private TreeMap sets = null;
    private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
    private static Transformer transformer = null;
    private static HashMap builderMap = new HashMap();
    private static Element xmlnsEl = null;
    private static DocumentBuilderFactory factory =
        DocumentBuilderFactory.newInstance();
    
    static {
        try {
            transformer = transformerFactory.newTransformer();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = getBuilder();
            DOMImplementation impl = builder.getDOMImplementation();
            Document xmlnsDoc
            = impl.createDocument("http://www.oclc.org/research/software/oai/harvester",
                    "harvester:xmlnsDoc", null);
            xmlnsEl = xmlnsDoc.getDocumentElement();
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                    "xmlns:srw",
            "http://www.loc.gov/zing/srw/");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public SRUOAICatalog(Properties properties) {
        this(properties, properties.getProperty("SRUOAICatalog.sruURL"));
    }
    
    public SRUOAICatalog(Properties properties, String sruURL) {
        sortKeys = properties.getProperty("SRUOAICatalog.sortKeys");
        if (sortKeys == null) sortKeys = "";
        
        String maxListSize =
            properties.getProperty("SRUOAICatalog.maxListSize");
        
        if (maxListSize == null) {
            throw new IllegalArgumentException("SRUOAICatalog.maxListSize is missing from the properties file");
        } else {
            this.maxListSize = Integer.parseInt(maxListSize);
        }
        
        this.sruURL = sruURL;
        sets = getSets(properties);
    }
    
    private static TreeMap getSets(Properties properties) {
        TreeMap treeMap = new TreeMap();
        return treeMap;
    }
    
    private static DocumentBuilder getBuilder() throws ParserConfigurationException {
        Thread currentThread = Thread.currentThread();
        DocumentBuilder builder = (DocumentBuilder) builderMap.get(currentThread);
        if (builder == null) {
            builder = factory.newDocumentBuilder();
            builderMap.put(currentThread, builder);
        }
        return builder;
    }
    
    private String normalizeTerm(String term) {
        return term;
    }
    
    public Map listSets() throws NoSetHierarchyException {
        if (sets.size() == 0)
            throw new NoSetHierarchyException();
        Map listSetsMap = new LinkedHashMap();
        try {
            Object[] keys = sets.keySet().toArray();
            Object[] values = sets.values().toArray();
            ArrayList newList = new ArrayList();
            for (int i = 0; i < keys.length; ++i) {
                newList.add(values[i]);
            }
            listSetsMap.put("sets", newList.iterator());
        } catch (Throwable e) {
            System.err.println("SRUOAICatalog.listSets: browse failed");
            e.printStackTrace();
        }
        
        return listSetsMap;
    }
    
    public Map listSets(String resumptionToken)
    throws BadResumptionTokenException {
        throw new BadResumptionTokenException();
    }
    
    /**
     * get a map containing a key="headers" value=Iterator and
     * a key="resumptionToken" value=String. The "headers" Map
     * contains Map.Entrys where key="header" value="deleted" or null.
     */
    public Map listIdentifiers(String from,
            String until,
            String set,
            String metadataPrefix)
    throws
    BadArgumentException,
    NoItemsMatchException,
    OAIInternalServerError {
        if (set != null
                && set.length() > 0
                && from.equals(toFinestFrom("0000-00-00"))
                && until.equals(toFinestUntil("9999-99-99"))) {
            from = null;
            until = null;
        }
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        Document srResponse;
        try {
            srResponse =
                getSearchRetrieveResponse(sruURL,
                        from,
                        until,
                        set,
                        "http://www.openarchives.org/OAI/2.0/#header",
                        1,
                        maxListSize,
                "xml");
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
        try {
            NodeList nodeList = getRecords(srResponse);
            if (nodeList != null) {
                RecordFactory recordFactory = getRecordFactory();
                for (int i = 0; i < nodeList.getLength(); ++i) {
                    Object rec = getRecordData(nodeList.item(i));
                    HashMap hashMap = new HashMap();
                    hashMap.put("header", rec);
                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
                    try {
                        hashMap.put("metadata", getNativeMetadata(localIdentifier,
                                metadataPrefix));
                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (TransformerException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (ParserConfigurationException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (SAXException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    }
                    String[] header = recordFactory.createHeader(hashMap);
                    headers.add(header[0]);
                    identifiers.add(header[1]);
                }
                try {
                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
                    String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
                    if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                        StringBuffer resumptionToken = new StringBuffer();
                        resumptionToken.append(resultSetId);
                        resumptionToken.append(":");
                        resumptionToken.append(nextRecordPosition);
                        resumptionToken.append(":");
                        resumptionToken.append(metadataPrefix);
                        listIdentifiersMap.put(
                                "resumptionMap",
                                getResumptionMap(resumptionToken.toString()));
                    }
                } catch (TransformerException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            } else {
                throw new NoItemsMatchException();
            }
            listIdentifiersMap.put("headers", headers.iterator());
            listIdentifiersMap.put("identifiers", identifiers.iterator());
            return listIdentifiersMap;
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * get a map containing a key="headers" value=Iterator and
     * a key="resumptionToken" value=String. The "headers" Iterator
     * contains Map.Entrys where key="header" value="deleted" or null.
     */
    public Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resultSetId;
        String nextRecordPosition;
        String metadataPrefix;
        try {
            resultSetId = tokenizer.nextToken();
            nextRecordPosition = tokenizer.nextToken();
            metadataPrefix = tokenizer.nextToken();
            if (metadataPrefix.equals("null"))
                metadataPrefix = null;
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        try {
            Document srResponse;
            try {
                srResponse =
                    getSearchRetrieveResponse(sruURL,
                            resultSetId,
                            nextRecordPosition,
                            "http://www.openarchives.org/OAI/2.0/#header",
                            maxListSize,
                    "xml");
            } catch (IOException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            } catch (SAXException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            } catch (ParserConfigurationException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }
            NodeList nodeList = getRecords(srResponse);
            if (nodeList != null) {
                RecordFactory recordFactory = getRecordFactory();
                for (int i = 0; i < nodeList.getLength(); ++i) {
                    
                    Object rec = getRecordData(nodeList.item(i));
                    HashMap hashMap = new HashMap();
                    hashMap.put("header", rec);
                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
                    hashMap.put("metadata", getNativeMetadata(localIdentifier,
                            metadataPrefix));
                    String[] header = recordFactory.createHeader(hashMap);
                    headers.add(header[0]);
                    identifiers.add(header[1]);
                }
                nextRecordPosition = XPathAPI.eval(srResponse,
                        "/srw:searchRetrieveResponse/srw:nextRecordPosition",
                        xmlnsEl).str();
                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                    StringBuffer newResumptionToken = new StringBuffer();
                    newResumptionToken.append(resultSetId);
                    newResumptionToken.append(":");
                    newResumptionToken.append(nextRecordPosition);
                    newResumptionToken.append(":");
                    newResumptionToken.append(metadataPrefix);
                    listIdentifiersMap.put(
                            "resumptionMap",
                            getResumptionMap(newResumptionToken.toString()));
                }
            }
            listIdentifiersMap.put("headers", headers.iterator());
            listIdentifiersMap.put("identifiers", identifiers.iterator());
        } catch (Throwable e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
        return listIdentifiersMap;
    }
    
    /**
     * get an Iterator containing Map.Entrys where key=metadataPrefix
     * and value=schema.
     */
    public Vector getSchemaLocations(String oaiIdentifier)
    throws
    IdDoesNotExistException,
    NoMetadataFormatsException,
    OAIInternalServerError {
        Vector result = new Vector();
        NodeList nodeList = null;
        try {
            nodeList = getIdentifierRecords(sruURL, oaiIdentifier, (String) null);
        } catch (Throwable e) {
            throw new OAIInternalServerError("Database failure");
        }
        if (nodeList != null) {
            for (int i = 0; i < nodeList.getLength(); ++i) {
                try {
                    Object rec = getRecordData(nodeList.item(i));
                    if (rec != null) {
                        Vector schemaLocations =
                            getRecordFactory().getSchemaLocations(rec);
                        for (int j = 0; j < schemaLocations.size(); ++j) {
                            result.add(schemaLocations.get(j));
                        }
                    } else {
                        throw new OAIInternalServerError("Null Record");
                    }
                } catch (TransformerException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                } catch (SAXException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                } catch (ParserConfigurationException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            }
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
        return result;
    }
    
    /**
     * get a DocumentFragment containing the specified record
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
    throws
    IdDoesNotExistException,
    IdDoesNotExistException,
    CannotDisseminateFormatException,
    OAIInternalServerError {
        Object nativeObject;
        try {
            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
        
        
        if (nativeObject != null) {
            String schemaURL = null;
            
            if (metadataPrefix != null) {
                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                        == null) {
                    throw new CannotDisseminateFormatException(metadataPrefix);
                }
            }
            try {
                String s =
                    getRecordFactory().create(
                            nativeObject,
                            schemaURL,
                            metadataPrefix);
                return s;
            } catch (CannotDisseminateFormatException e) {
                e.printStackTrace();
                throw e;
            }
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }
    
    /**
     * get a DocumentFragment containing the specified record
     */
    public String getMetadata(String oaiIdentifier, String metadataPrefix)
    throws
    IdDoesNotExistException,
    IdDoesNotExistException,
    CannotDisseminateFormatException,
    OAIInternalServerError {
        Object nativeObject;
        try {
            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
        
        if (debug) {
            System.out.println(nativeObject);
        }
        if (nativeObject != null) {
            String schemaURL = null;
            
            if (metadataPrefix != null) {
                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                        == null) {
                    if (debug) {
                        System.out.println(
                        "SRUOAICatalog.getRecord: metadataPrefix not found");
                    }
                    throw new CannotDisseminateFormatException(metadataPrefix);
                }
            }
            try {
                return getRecordFactory().createMetadata(
                        nativeObject,
                        schemaURL,
                        metadataPrefix);
            } catch (CannotDisseminateFormatException e) {
                e.printStackTrace();
                throw e;
            }
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }
    
    private NodeList getIdentifierRecords(String sruURL,
            String oaiIdentifier,
            String metadataPrefix)
    throws TransformerException, SAXException, IOException,
    ParserConfigurationException {
        Document srResponse;
        String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
        srResponse =
            getSearchRetrieveResponse(sruURL,
                    localIdentifier,
                    "http://www.openarchives.org/OAI/2.0/#header",
            "xml");
        return getRecords(srResponse);
    }
    
    private Object getFullRecord(String sruURL,
            String oaiIdentifier,
            String metadataPrefix)
    throws SAXException, TransformerException, IOException,
    ParserConfigurationException {
        NodeList nodeList = getIdentifierRecords(sruURL, oaiIdentifier, metadataPrefix);
        if (nodeList != null) {
            HashMap hashMap = new HashMap();
            hashMap.put("header", getRecordData(nodeList.item(0)));
            String nativeRecordSchema = getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
            String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
            Document srResponse =
                getSearchRetrieveResponse(sruURL,
                        localIdentifier,
                        nativeRecordSchema,
                "xml");
            nodeList = getRecords(srResponse);
            hashMap.put("metadata", getRecordData(nodeList.item(0)));
            return hashMap;
        }
        return null;
    }
    
    private Object getNativeMetadata(String localIdentifier, String metadataPrefix)
    throws TransformerException, SAXException, IOException,
    ParserConfigurationException {
        String nativeRecordSchema = getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
        Document srResponse;
        srResponse =
            getSearchRetrieveResponse(sruURL,
                    localIdentifier,
                    nativeRecordSchema,
            "xml");
        NodeList nodeList = getRecords(srResponse);
        return getRecordData(nodeList.item(0));
    }
    
    public Map listRecords(String from,
            String until,
            String set,
            String metadataPrefix)
    throws
    BadArgumentException,
    CannotDisseminateFormatException,
    NoItemsMatchException,
    OAIInternalServerError {
        if (set != null
                && set.length() > 0
                && from.equals(toFinestFrom("0000-00-00"))
                && until.equals(toFinestUntil("9999-99-99"))) {
            from = null;
            until = null;
        }
        if (toFinestFrom("0000-00-00").equals(from)) {
            from = null;
        }
        if (toFinestUntil("9999-12-31").compareTo(until) <= 0) {
            until = null;
        }
        Map listRecordsMap = new HashMap();
        ArrayList recordsList = new ArrayList();
        Document srResponse;
        try {
            srResponse =
                getSearchRetrieveResponse(sruURL,
                        from,
                        until,
                        set,
                        "http://www.openarchives.org/OAI/2.0/#header",
                        1,
                        maxListSize,
                "xml");
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
        try {
            NodeList nodeList = getRecords(srResponse);
            if (nodeList != null) {
                RecordFactory recordFactory = getRecordFactory();
                String schemaURL = null;
                if (metadataPrefix != null) {
                    if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                            == null)
                        throw new CannotDisseminateFormatException(metadataPrefix);
                }
                for (int i = 0; i < nodeList.getLength(); ++i) {
                    Object rec = getRecordData(nodeList.item(i));
                    HashMap hashMap = new HashMap();
                    hashMap.put("header", rec);
                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
                    try {
                        hashMap.put("metadata", getNativeMetadata(localIdentifier,
                                metadataPrefix));
                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (ParserConfigurationException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (SAXException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    }
                    recordsList.add(
                            recordFactory.create(hashMap,
                                    schemaURL,
                                    metadataPrefix));
                }
                String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
                    StringBuffer resumptionToken = new StringBuffer();
                    resumptionToken.append(resultSetId);
                    resumptionToken.append(":");
                    resumptionToken.append(nextRecordPosition);
                    resumptionToken.append(":");
                    resumptionToken.append(metadataPrefix);
                    listRecordsMap.put(
                            "resumptionMap",
                            getResumptionMap(resumptionToken.toString()));
                }
            } else {
                throw new NoItemsMatchException();
            }
            listRecordsMap.put("records", recordsList.iterator());
            return listRecordsMap;
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resultSetId;
        String nextRecordPosition;
        String metadataPrefix;
        
        try {
            resultSetId = tokenizer.nextToken();
            nextRecordPosition = tokenizer.nextToken();
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            e.printStackTrace();
            throw new BadResumptionTokenException();
        }
        Map listRecordsMap = new HashMap();
        ArrayList recordsList = new ArrayList();
        try {
            Document srResponse =
                getSearchRetrieveResponse(sruURL,
                        resultSetId,
                        nextRecordPosition,
                        "http://www.openarchives.org/OAI/2.0/#header",
                        maxListSize,
                "xml");
            NodeList nodeList = getRecords(srResponse);
            RecordFactory recordFactory = getRecordFactory();
            String schemaURL = null;
            if (metadataPrefix != null) {
                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                        == null)
                    throw new CannotDisseminateFormatException(metadataPrefix);
            }
            for (int i = 0; i < nodeList.getLength(); ++i) {
                Object rec = getRecordData(nodeList.item(i));
                HashMap hashMap = new HashMap();
                hashMap.put("header", rec);
                String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
                hashMap.put("metadata", getNativeMetadata(localIdentifier,
                        metadataPrefix));
                recordsList.add(
                        recordFactory.create(hashMap,
                                schemaURL,
                                metadataPrefix));
            }
            nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
            if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
                StringBuffer newResumptionToken = new StringBuffer();
                newResumptionToken.append(resultSetId);
                newResumptionToken.append(":");
                newResumptionToken.append(nextRecordPosition);
                newResumptionToken.append(":");
                newResumptionToken.append(metadataPrefix);
                listRecordsMap.put(
                        "resumptionMap",
                        getResumptionMap(newResumptionToken.toString()));
            }
            listRecordsMap.put("records", recordsList.iterator());
        } catch (Throwable e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
        return listRecordsMap;
    }
    
    private Document
    getSearchRetrieveResponse(String sruURL,
            String from,
            String until,
            String set,
            String recordSchema,
            int startRecord,
            int maximumRecords,
            String recordPacking)
    throws SAXException, ParserConfigurationException,
    UnsupportedEncodingException, IOException {
        StringBuffer query = new StringBuffer();
        if ((from != null && from.length() > 0)
                || (until != null && until.length() > 0)) {
            query.append("(");
            if (from != null && from.length() > 0) {
                query.append("oai.datestamp>=\"");
                query.append(normalizeTerm(from));
                query.append("\"");
            }
            if (until != null && from.length() > 0) {
                if (query.length() > 0)
                    query.append(" and ");
                query.append("oai.datestamp<=\"");
                query.append(normalizeTerm(until));
                query.append("\"");
            }
            query.append(")");
        }
        if (set != null && set.length() > 0) {
            if (query.length() > 0)
                query.append(" and ");
            query.append("oai.set=\"");
            query.append(normalizeTerm(set));
            query.append("\"");
        }
        StringBuffer request = new StringBuffer(sruURL);
        request.append("?operation=searchRetrieve&version=1.1&resultSetTTL=600&query=");
        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
        request.append("&startRecord=").append(Integer.toString(startRecord));
        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
        request.append("&recordPacking=").append(recordPacking);
        request.append("&sortKeys=").append(URLEncoder.encode(sortKeys, "UTF-8"));
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(request.toString());
    }
    
    private Document
    getSearchRetrieveResponse(String sruURL,
            String resultSetId,
            String nextRecordPosition,
            String recordSchema,
            int maximumRecords,
            String recordPacking)
    throws SAXException, ParserConfigurationException,
    UnsupportedEncodingException, IOException {
        StringBuffer query = new StringBuffer();
        query.append("cql.resultSetId=");
        query.append(resultSetId);
        StringBuffer request = new StringBuffer(sruURL);
        request.append("?operation=searchRetrieve&resultSetTTL=600&version=1.1&query=");
        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
        request.append("&startRecord=").append(nextRecordPosition);
        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
        request.append("&recordPacking=").append(recordPacking);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(request.toString());
    }
    
    private Document
    getSearchRetrieveResponse(String sruURL,
            String localIdentifier,
            String recordSchema,
            String recordPacking)
    throws SAXException, IOException, ParserConfigurationException,
    UnsupportedEncodingException {
        StringBuffer query = new StringBuffer();
        query.append("oai.identifier exact \"");
        query.append(localIdentifier);
        query.append("\"");
        
        StringBuffer request = new StringBuffer(sruURL);
        request.append("?operation=searchRetrieve&resultSetTTL=0&version=1.1&query=");
        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
        request.append("&startRecord=1&maximumRecords=1");
        request.append("&recordPacking=").append(recordPacking);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(request.toString());
    }
    
    private Element getRecordData(Node record)
    throws TransformerException, SAXException, IOException, ParserConfigurationException {
        Node result = XPathAPI.selectSingleNode(record,
                "srw:recordData/*[1]",
                xmlnsEl);
        return toDocument((Element)result).getDocumentElement();
    }
    
    private Document toDocument(Element el)
    throws TransformerException, SAXException, IOException, ParserConfigurationException {
        DOMSource source = new DOMSource(el);
        StringWriter sw = new StringWriter();
        StreamResult result = new StreamResult(sw);
        synchronized (transformer) {
            transformer.transform(source, result);
        }
        DocumentBuilder builder = getBuilder();
        Document doc = builder.parse(new InputSource(new StringReader(sw.toString())));
        Element docEl = doc.getDocumentElement();
        if (docEl.getNamespaceURI() == null) {
            docEl.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "");
        }
        return doc;
    }
    
    private NodeList getRecords(Document srResponse)
    throws TransformerException {
        return XPathAPI.selectNodeList(srResponse, "/srw:searchRetrieveResponse/srw:records/srw:record", xmlnsEl);
    }

    public void close() {
    }
}

/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.xml.sax.SAXException;

import ORG.oclc.oai.server.catalog.helpers.RecordStringHandler;
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;


/**
 * XMLFileOAICatalog is an implementation of AbstractCatalog interface
 * with the data sitting in a directory on a filesystem.
 *
 * @author Jeff Young, OCLC Online Computer Library Center
 */

public class XMLFileOAICatalog extends AbstractCatalog {
    private static boolean debug=false;

    private SortedMap nativeMap = null;
    private HashMap          resumptionResults=new HashMap();
    private int              maxListSize;
    private ArrayList sets = null;
    private Transformer getMetadataTransformer = null;
    private boolean schemaLocationIndexed = false;

    public XMLFileOAICatalog(Properties properties) throws IOException {
        try {
            String          temp;

            temp=properties.getProperty("XMLFileOAICatalog.debug");
            if ("true".equals(temp)) debug = true;
            temp=properties.getProperty("XMLFileOAICatalog.schemaLocationIndexed");
            if ("true".equals(temp)) schemaLocationIndexed = true;
            temp=properties.getProperty("XMLFileOAICatalog.maxListSize");
            if (temp==null)
                throw new IllegalArgumentException("XMLFileOAICatalog."+
                                                   "maxListSize is missing from the properties file");
            maxListSize = Integer.parseInt(temp);
            if(debug)
                System.out.println("in XMLFileOAICatalog(): maxListSize="+
                                   maxListSize);
            
            String sourceFile=properties.getProperty("XMLFileOAICatalog.sourceFile");
            if (sourceFile==null)
                throw new IllegalArgumentException("XMLFileOAICatalog."+
                                                   "sourceFile is missing from the properties file");
            if(debug)
                System.out.println("in XMLFileOAICatalog(): sourceFile="+
                                   sourceFile);
            String getMetadataXSLTName = properties.getProperty("XMLFileOAICatalog.getMetadataXSLTName");
            if (getMetadataXSLTName != null) {
                try {
                    InputStream is;
                    try {
                        is = new FileInputStream(getMetadataXSLTName);
                    } catch (FileNotFoundException e) {
                        is = Thread.currentThread().getContextClassLoader().getResourceAsStream(getMetadataXSLTName);
                    }
                    StreamSource xslSource = new StreamSource(is);
                    TransformerFactory tFactory = TransformerFactory.newInstance();
                    getMetadataTransformer = tFactory.newTransformer(xslSource);
                } catch (TransformerConfigurationException e) {
                    e.printStackTrace();
                    throw new IOException(e.getMessage());
                }
            }
            
            RecordStringHandler rsh = new RecordStringHandler();
            SAXParserFactory factory = SAXParserFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
            SAXParser saxParser = factory.newSAXParser();
            InputStream in;
            try {
                in = new FileInputStream(sourceFile);
            } catch (FileNotFoundException e) {
                in = Thread.currentThread().getContextClassLoader().getResourceAsStream(sourceFile);
            }
            saxParser.parse(in, rsh);
//            saxParser.parse(new File(sourceFile), rsh);
            
            // build the indexes
            nativeMap = rsh.getNativeRecords();
        } catch (SAXException e) {
            e.printStackTrace();
            throw new IOException(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }
	sets = getSets(properties);
    }

    private static ArrayList getSets(Properties properties) {
        TreeMap treeMap = new TreeMap();
	String propertyPrefix = "Sets.";
	Enumeration propNames = properties.propertyNames();
	while (propNames.hasMoreElements()) {
	    String propertyName = (String)propNames.nextElement();
	    if (propertyName.startsWith(propertyPrefix)) {
                treeMap.put(propertyName, properties.get(propertyName));
	    }
	}
 	return new ArrayList(treeMap.values());
    }
    
    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param     oaiIdentifier the OAI identifier
     * @param     metadataPrefix the OAI metadataPrefix
     * @return    the Record object containing the result.
     * @exception CannotDisseminateFormatException signals an http status
     *                code 400 problem
     * @exception IdDoesNotExistException signals an http status code 404
     *                problem
     * @exception OAIInternalServerError signals an http status code 500
     *                problem
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
        throws IdDoesNotExistException, CannotDisseminateFormatException,
               OAIInternalServerError {
        String localIdentifier
            = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
        String recordid = localIdentifier;
        if (schemaLocationIndexed) {
            recordid=recordid + "/" + metadataPrefix;
        }
        if (debug) System.out.println("XMLFileOAICatalog.getRecord: recordid=" + recordid);
        Object nativeRecord = nativeMap.get(recordid.toLowerCase());
        if (nativeRecord == null)
            throw new IdDoesNotExistException(oaiIdentifier);
        return constructRecord(nativeRecord, metadataPrefix);
    }


    /**
     * get a DocumentFragment containing the specified record
     */
    public String getMetadata(String oaiIdentifier, String metadataPrefix)
        throws IdDoesNotExistException, IdDoesNotExistException, OAIInternalServerError {
        String localIdentifier
            = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
        String recordid = localIdentifier;
        if (schemaLocationIndexed) {
            recordid=recordid + "/" + metadataPrefix;
        }
        if (debug) System.out.println("XMLFileOAICatalog.getRecord: recordid=" + recordid);
        HashMap nativeRecord = (HashMap)nativeMap.get(recordid.toLowerCase());
        if (nativeRecord == null)
            throw new IdDoesNotExistException(oaiIdentifier);
        if (debug) {
            Iterator keys = nativeRecord.keySet().iterator();
            while (keys.hasNext())
                System.out.println(keys.next());
        }
        String result = (String)nativeRecord.get("recordString");
        if (getMetadataTransformer != null) {
            StringReader stringReader = new StringReader(result);
            StreamSource streamSource = new StreamSource(stringReader);
            StringWriter stringWriter = new StringWriter();
            try {
                synchronized (getMetadataTransformer) {
                    getMetadataTransformer.transform(streamSource, new StreamResult(stringWriter));
                }
            } catch (TransformerException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }
            result = stringWriter.toString();
        }
        return result;
    }

    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * We get passed the ID for a record and are supposed to return a list
     * of the formats that we can deliver the record in.  Since we are assuming
     * that all the records in the directory have the same format, the
     * response to this is static;
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAINotFoundException signals an http status code 404 problem
     */
    public Vector getSchemaLocations(String oaiIdentifier)
      throws IdDoesNotExistException, NoMetadataFormatsException {
        Vector v = new Vector();
//        String localIdentifier
//            = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
        Iterator iterator = nativeMap.entrySet().iterator();
        int numRows = nativeMap.entrySet().size();
        for (int i=0; i<numRows; ++i) {
            Map.Entry entryNativeMap = (Map.Entry)iterator.next();
            HashMap nativeRecord = (HashMap)entryNativeMap.getValue();
	    if (getRecordFactory().getOAIIdentifier(nativeRecord).equals(oaiIdentifier)) {
		Vector schemaLocations = getRecordFactory().getSchemaLocations(nativeRecord);
		Iterator itemIterator = schemaLocations.iterator();
		while (itemIterator.hasNext())
		    v.add(itemIterator.next());
	    }
        }
        if (v.size() > 0) {
            return v;
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }


    /**
     * Retrieve a list of Identifiers that satisfy the criteria parameters
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
        throws NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
	Iterator iterator = nativeMap.entrySet().iterator();
	int numRows = nativeMap.entrySet().size();
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
            HashMap nativeRecord = (HashMap)entryNativeMap.getValue();
            String recordDate = getRecordFactory().getDatestamp(nativeRecord);
            String schemaLocation = (String)nativeRecord.get("schemaLocation");
	    List setSpecs = (List)nativeRecord.get("setSpecs");
            if (recordDate.compareTo(from) >= 0
                && recordDate.compareTo(until) <= 0
                && (!schemaLocationIndexed || schemaLocation.equals(getCrosswalks().getSchemaLocation(metadataPrefix)))
		&& (set==null || setSpecs.contains(set))) {
                String[] header = getRecordFactory().createHeader(nativeRecord);
                headers.add(header[0]);
                identifiers.add(header[1]);
                count++;
            }
	}

        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listIdentifiersMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listIdentifiersMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of Identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     */
    public Map listIdentifiers(String resumptionToken)
      throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("XMLFileOAICatalog.listIdentifiers: reuse of old resumptionToken?");
	    iterator = nativeMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the headers and identifiers ArrayLists. */
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
            String key = (String)entryNativeMap.getKey();
            Object nativeRecord = nativeMap.get(key);
            String[] header = getRecordFactory().createHeader(nativeRecord);
            headers.add(header[0]);
            identifiers.add(header[1]);
            count++;
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listIdentifiersMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }


    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeRecord native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(Object nativeRecord, String metadataPrefix)
        throws CannotDisseminateFormatException, OAIInternalServerError {
        String schemaURL = null;
	Iterator setSpecs = getSetSpecs(nativeRecord);
	Iterator abouts = getAbouts(nativeRecord);

        if (metadataPrefix != null) {
            if (debug) {
                System.out.println(getCrosswalks());
            }
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeRecord, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * get an Iterator containing the setSpecs for the nativeRecord
     *
     * @param rs ResultSet containing the nativeRecord
     * @return an Iterator containing the list of setSpec values for this nativeRecord
     */
    private Iterator getSetSpecs(Object nativeRecord)
	throws OAIInternalServerError {
	try {
	    return getRecordFactory().getSetSpecs(nativeRecord);
	} catch (Exception e) {
	    e.printStackTrace();
	    throw new OAIInternalServerError(e.getMessage());
	}
    }

    /**
     * get an Iterator containing the abouts for the nativeRecord
     *
     * @param rs ResultSet containing the nativeRecord
     * @return an Iterator containing the list of about values for this nativeRecord
     */
    private Iterator getAbouts(Object nativeRecord) {
        return null;
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @param metadataPrefix the OAI metadataPrefix
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listRecords(String from, String until, String set,
                                    String metadataPrefix)
      throws CannotDisseminateFormatException,
      OAIInternalServerError, NoItemsMatchException {
        String requestedSchemaLocation = getCrosswalks().getSchemaLocation(metadataPrefix);
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        LinkedList records = new LinkedList();
	Iterator iterator = nativeMap.entrySet().iterator();
	int numRows = nativeMap.entrySet().size();
        if (debug) {
            System.out.println("XMLFileOAICatalog.listRecords: numRows=" + numRows);
        }
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
            HashMap nativeRecord = (HashMap)entryNativeMap.getValue();
            String recordDate = getRecordFactory().getDatestamp(nativeRecord);
            String schemaLocation = (String)nativeRecord.get("schemaLocation");
	    List setSpecs = (List)nativeRecord.get("setSpecs");
            if (debug) {
                System.out.println("XMLFileOAICatalog.listRecord: recordDate=" + recordDate);
                System.out.println("XMLFileOAICatalog.listRecord: requestedSchemaLocation=" + requestedSchemaLocation);
                System.out.println("XMLFileOAICatalog.listRecord: schemaLocation=" + schemaLocation);
            }
            if (recordDate.compareTo(from) >= 0
                && recordDate.compareTo(until) <= 0
                && (!schemaLocationIndexed || requestedSchemaLocation.equals(schemaLocation))
		&& (set==null || setSpecs.contains(set))) {
                String record = constructRecord(nativeRecord, metadataPrefix);
                if (debug) {
                    System.out.println("XMLFileOAICatalog.listRecords: record=" + record);
                }
                records.add(record);
                count++;
            }
	}
        
        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listRecordsMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listRecordsMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listRecords(String resumptionToken)
      throws BadResumptionTokenException,
      OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        LinkedList records = new LinkedList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("XMLFileOAICatalog.listRecords: reuse of old resumptionToken?");
	    iterator = nativeMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the records ArrayLists. */
	int count = 0;
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryNativeMap = (Map.Entry)iterator.next();
	    try {
                String key = (String)entryNativeMap.getKey();
                Object nativeRecord = nativeMap.get(key);
                String record = constructRecord(nativeRecord, metadataPrefix);
                records.add(record);
                count++;
            } catch (CannotDisseminateFormatException e) {
                /* the client hacked the resumptionToken beyond repair */
                throw new BadResumptionTokenException();
	    }
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listRecordsMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    public Map listSets() throws NoSetHierarchyException {
        if (sets.size() == 0)
            throw new NoSetHierarchyException();
         Map listSetsMap = new LinkedHashMap();
         listSetsMap.put("sets", sets.iterator());
         return listSetsMap;
    }


    public Map listSets(String resumptionToken)
      throws BadResumptionTokenException {
	throw new BadResumptionTokenException();
    }


    /**
     * close the repository
     */
    public void close() { }


    /**
     * Purge tokens that are older than the time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date      then, now = new Date();
        Iterator  keySet = resumptionResults.keySet().iterator();
        String    key;

        while (keySet.hasNext()) {
            key=(String)keySet.next();
            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }


    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a long integer version of the current time
     */
    private synchronized static String getRSName() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

/**
 * XMLFileRecordFactory converts native XML "items" to "record" Strings.
 * This factory assumes the native XML item looks exactly like the <record>
 * element of an OAI GetRecord response, with the possible exception that the
 * <metadata> element contains multiple metadataFormats from which to choose.
 */
public class XMLFileRecordFactory extends RecordFactory {
//     private String repositoryIdentifier = null;
    
    /**
     * Construct an XMLFileRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public XMLFileRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
// 	repositoryIdentifier = properties.getProperty("XMLFileRecordFactory.repositoryIdentifier");
// 	if (repositoryIdentifier == null) {
// 	    throw new IllegalArgumentException("XMLFileRecordFactory.repositoryIdentifier is missing from the properties file");
// 	}
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
        try {
//             StringTokenizer tokenizer = new StringTokenizer(identifier, ":");
//             tokenizer.nextToken();
//             tokenizer.nextToken();
//             return tokenizer.nextToken();
            return identifier;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
	StringBuffer sb = new StringBuffer();
// 	sb.append("oai:");
// 	sb.append(repositoryIdentifier);
// 	sb.append(":");
	sb.append(getLocalIdentifier(nativeItem));
	return sb.toString();
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
        return (String)((HashMap)nativeItem).get("localIdentifier");
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException  {
        return (String)((HashMap)nativeItem).get("datestamp");
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
        List setSpecs = (List)((HashMap)nativeItem).get("setSpecs");
        if (setSpecs != null) 
            return setSpecs.iterator();
        else
            return null;
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
	// Don't perform quick creates
	return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Properties;

/**
 * XMLRecordFactory converts native XML "items" to "record" Strings.
 * This factory assumes the native XML item looks exactly like the <record>
 * element of an OAI GetRecord response, with the possible exception that the
 * <metadata> element contains multiple metadataFormats from which to choose.
 */
public class XMLRecordFactory extends RecordFactory {
    private static final String identifierStart = "<identifier>";
    private static final String identifierEnd = "</identifier>";
    private static final String datestampStart = "<datestamp>";
    private static final String datestampEnd = "</datestamp>";
    private static final String setSpecStart = "<setSpec>";
    private static final String setSpecEnd = "</setSpec>";
    private static final String aboutStart = "<about>";
    private static final String aboutEnd = "</about>";
    
    /**
     * Construct an XMLRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public XMLRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
	return identifier;
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
	String xmlRec = (String)nativeItem;
	int startOffset = xmlRec.indexOf(identifierStart);
	int endOffset = xmlRec.indexOf(identifierEnd);
	return xmlRec.substring(startOffset + identifierStart.length(), endOffset);
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException  {
	String xmlRec = (String)nativeItem;
	int startOffset = xmlRec.indexOf(datestampStart);
	int endOffset = xmlRec.indexOf(datestampEnd);
	return xmlRec.substring(startOffset + datestampStart.length(), endOffset);
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
	ArrayList list = new ArrayList();
	String xmlRec = (String)nativeItem;
	for (int startOffset = xmlRec.indexOf(setSpecStart);
	     startOffset >= 0;
	     startOffset = xmlRec.indexOf(setSpecStart, startOffset + 1)) {
	    int endOffset = xmlRec.indexOf(setSpecEnd, startOffset + 1);
	    list.add(xmlRec.substring(startOffset + setSpecStart.length(), endOffset));
	}
	return list.iterator();
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	ArrayList list = new ArrayList();
	String xmlRec = (String)nativeItem;
	for (int startOffset = xmlRec.indexOf(aboutStart);
	     startOffset >= 0;
	     startOffset = xmlRec.indexOf(aboutStart, startOffset + 1)) {
	    int endOffset = xmlRec.indexOf(aboutEnd, startOffset + 1);
	    list.add(xmlRec.substring(startOffset + aboutStart.length(), endOffset));
	}
	return list.iterator();
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	String xmlRec = (String)nativeItem;
	return xmlRec.indexOf("<header status=\"deleted\"") != -1;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
	// Don't perform quick creates
	return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import ORG.oclc.oai.server.verb.BadArgumentException;
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;

public class XerSRUOAICatalog extends AbstractCatalog {
    private static final boolean debug = false;
    private String sruURL;
    private String sortKeys = "";
    protected int maxListSize;
    private TreeMap sets = null;
    private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
    private static Transformer transformer = null;
    private static HashMap builderMap = new HashMap();
    private static Element xmlnsEl = null;
    private static DocumentBuilderFactory factory =
        DocumentBuilderFactory.newInstance();

    static {
        try {
            transformer = transformerFactory.newTransformer();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = getBuilder();
            DOMImplementation impl = builder.getDOMImplementation();
            Document xmlnsDoc
            = impl.createDocument("http://www.oclc.org/research/software/oai/harvester",
                    "harvester:xmlnsDoc", null);
            xmlnsEl = xmlnsDoc.getDocumentElement();
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                    "xmlns:srw",
            "http://www.loc.gov/zing/srw/");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public XerSRUOAICatalog(Properties properties) {
        this(properties, properties.getProperty("SRUOAICatalog.sruURL"));
    }

    public XerSRUOAICatalog(Properties properties, String sruURL) {
        sortKeys = properties.getProperty("SRUOAICatalog.sortKeys");
        if (sortKeys == null) sortKeys = "";

        String maxListSize =
            properties.getProperty("SRUOAICatalog.maxListSize");

        if (maxListSize == null) {
            throw new IllegalArgumentException("SRUOAICatalog.maxListSize is missing from the properties file");
        } else {
            this.maxListSize = Integer.parseInt(maxListSize);
        }

        this.sruURL = sruURL;
        sets = getSets(properties);
    }

    private static TreeMap getSets(Properties properties) {
        TreeMap treeMap = new TreeMap();
        return treeMap;
    }

    private static DocumentBuilder getBuilder() throws ParserConfigurationException {
        Thread currentThread = Thread.currentThread();
        DocumentBuilder builder = (DocumentBuilder) builderMap.get(currentThread);
        if (builder == null) {
            builder = factory.newDocumentBuilder();
            builderMap.put(currentThread, builder);
        }
        return builder;
    }

    private String normalizeTerm(String term) {
        return term;
    }

    public Map listSets() throws NoSetHierarchyException {
        if (sets.size() == 0)
            throw new NoSetHierarchyException();
        Map listSetsMap = new LinkedHashMap();
        try {
            Object[] keys = sets.keySet().toArray();
            Object[] values = sets.values().toArray();
            ArrayList newList = new ArrayList();
            for (int i = 0; i < keys.length; ++i) {
                newList.add(values[i]);
            }
            listSetsMap.put("sets", newList.iterator());
        } catch (Throwable e) {
            System.err.println("SRUOAICatalog.listSets: browse failed");
            e.printStackTrace();
        }

        return listSetsMap;
    }

    public Map listSets(String resumptionToken)
    throws BadResumptionTokenException {
        throw new BadResumptionTokenException();
    }

    /**
     * get a map containing a key="headers" value=Iterator and
     * a key="resumptionToken" value=String. The "headers" Map
     * contains Map.Entrys where key="header" value="deleted" or null.
     */
    public Map listIdentifiers(String from,
            String until,
            String set,
            String metadataPrefix)
    throws
    BadArgumentException,
    NoItemsMatchException,
    OAIInternalServerError {
        if (set != null
                && set.length() > 0
                && from.equals(toFinestFrom("0000-00-00"))
                && until.equals(toFinestUntil("9999-99-99"))) {
            from = null;
            until = null;
        }
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        Document srResponse;
        try {
            srResponse =
                getSearchRetrieveResponse(sruURL,
                        from,
                        until,
                        set,
                        "http://www.openarchives.org/OAI/2.0/#header",
                        1,
                        maxListSize,
                "xml");
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
        try {
            NodeList nodeList = getRecords(srResponse);
            if (nodeList != null) {
                RecordFactory recordFactory = getRecordFactory();
                for (int i = 0; i < nodeList.getLength(); ++i) {
                    Object rec = getRecordData(nodeList.item(i));
                    HashMap hashMap = new HashMap();
                    hashMap.put("header", rec);
                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
                    try {
                        hashMap.put("metadata", getNativeMetadata(localIdentifier,
                                metadataPrefix));
                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (TransformerException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (ParserConfigurationException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    } catch (SAXException e) {
                        e.printStackTrace();
                        throw new OAIInternalServerError(e.getMessage());
                    }
                    String[] header = recordFactory.createHeader(hashMap);
                    headers.add(header[0]);
                    identifiers.add(header[1]);
                }
                try {
                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
                    String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
                    if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                        StringBuffer resumptionToken = new StringBuffer();
                        resumptionToken.append(resultSetId);
                        resumptionToken.append(":");
                        resumptionToken.append(nextRecordPosition);
                        resumptionToken.append(":");
                        resumptionToken.append(metadataPrefix);
                        listIdentifiersMap.put(
                                "resumptionMap",
                                getResumptionMap(resumptionToken.toString()));
                    }
                } catch (TransformerException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            } else {
                throw new NoItemsMatchException();
            }
            listIdentifiersMap.put("headers", headers.iterator());
            listIdentifiersMap.put("identifiers", identifiers.iterator());
            return listIdentifiersMap;
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * get a map containing a key="headers" value=Iterator and
     * a key="resumptionToken" value=String. The "headers" Iterator
     * contains Map.Entrys where key="header" value="deleted" or null.
     */
    public Map listIdentifiers(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resultSetId;
        String nextRecordPosition;
        String metadataPrefix;
        try {
            resultSetId = tokenizer.nextToken();
            nextRecordPosition = tokenizer.nextToken();
            metadataPrefix = tokenizer.nextToken();
            if (metadataPrefix.equals("null"))
                metadataPrefix = null;
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        try {
            Document srResponse;
            try {
                srResponse =
                    getSearchRetrieveResponse(sruURL,
                            resultSetId,
                            nextRecordPosition,
                            "http://www.openarchives.org/OAI/2.0/#header",
                            maxListSize,
                    "xml");
            } catch (IOException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            } catch (SAXException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            } catch (ParserConfigurationException e) {
                e.printStackTrace();
                throw new OAIInternalServerError(e.getMessage());
            }
            NodeList nodeList = getRecords(srResponse);
            if (nodeList != null) {
                RecordFactory recordFactory = getRecordFactory();
                for (int i = 0; i < nodeList.getLength(); ++i) {

                    Object rec = getRecordData(nodeList.item(i));
                    HashMap hashMap = new HashMap();
                    hashMap.put("header", rec);
                    String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
                    hashMap.put("metadata", getNativeMetadata(localIdentifier,
                            metadataPrefix));
                    String[] header = recordFactory.createHeader(hashMap);
                    headers.add(header[0]);
                    identifiers.add(header[1]);
                }
                nextRecordPosition = XPathAPI.eval(srResponse,
                        "/srw:searchRetrieveResponse/srw:nextRecordPosition",
                        xmlnsEl).str();
                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                    StringBuffer newResumptionToken = new StringBuffer();
                    newResumptionToken.append(resultSetId);
                    newResumptionToken.append(":");
                    newResumptionToken.append(nextRecordPosition);
                    newResumptionToken.append(":");
                    newResumptionToken.append(metadataPrefix);
                    listIdentifiersMap.put(
                            "resumptionMap",
                            getResumptionMap(newResumptionToken.toString()));
                }
            }
            listIdentifiersMap.put("headers", headers.iterator());
            listIdentifiersMap.put("identifiers", identifiers.iterator());
        } catch (Throwable e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
        return listIdentifiersMap;
    }

    /**
     * get an Iterator containing Map.Entrys where key=metadataPrefix
     * and value=schema.
     */
    public Vector getSchemaLocations(String oaiIdentifier)
    throws
    IdDoesNotExistException,
    NoMetadataFormatsException,
    OAIInternalServerError {
        Vector result = new Vector();
        NodeList nodeList = null;
        try {
            nodeList = getIdentifierRecords(sruURL, oaiIdentifier, (String) null);
        } catch (Throwable e) {
            throw new OAIInternalServerError("Database failure");
        }
        if (nodeList != null) {
            for (int i = 0; i < nodeList.getLength(); ++i) {
                try {
                    Object rec = getRecordData(nodeList.item(i));
                    if (rec != null) {
                        Vector schemaLocations =
                            getRecordFactory().getSchemaLocations(rec);
                        for (int j = 0; j < schemaLocations.size(); ++j) {
                            result.add(schemaLocations.get(j));
                        }
                    } else {
                        throw new OAIInternalServerError("Null Record");
                    }
                } catch (TransformerException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                } catch (SAXException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                } catch (ParserConfigurationException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            }
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
        return result;
    }

    /**
     * get a DocumentFragment containing the specified record
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
    throws
    IdDoesNotExistException,
    IdDoesNotExistException,
    CannotDisseminateFormatException,
    OAIInternalServerError {
        Object nativeObject;
        try {
            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }


        if (nativeObject != null) {
            String schemaURL = null;

            if (metadataPrefix != null) {
                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                        == null) {
                    throw new CannotDisseminateFormatException(metadataPrefix);
                }
            }
            try {
                String s =
                    getRecordFactory().create(
                            nativeObject,
                            schemaURL,
                            metadataPrefix);
                return s;
            } catch (CannotDisseminateFormatException e) {
                e.printStackTrace();
                throw e;
            }
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }

    /**
     * get a DocumentFragment containing the specified record
     */
    public String getMetadata(String oaiIdentifier, String metadataPrefix)
    throws
    IdDoesNotExistException,
    IdDoesNotExistException,
    CannotDisseminateFormatException,
    OAIInternalServerError {
        Object nativeObject;
        try {
            nativeObject = getFullRecord(sruURL, oaiIdentifier, metadataPrefix);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }

        if (debug) {
            System.out.println(nativeObject);
        }
        if (nativeObject != null) {
            String schemaURL = null;

            if (metadataPrefix != null) {
                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                        == null) {
                    if (debug) {
                        System.out.println(
                                "SRUOAICatalog.getRecord: metadataPrefix not found");
                    }
                    throw new CannotDisseminateFormatException(metadataPrefix);
                }
            }
            try {
                return getRecordFactory().createMetadata(
                        nativeObject,
                        schemaURL,
                        metadataPrefix);
            } catch (CannotDisseminateFormatException e) {
                e.printStackTrace();
                throw e;
            }
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }

    private NodeList getIdentifierRecords(String sruURL,
            String oaiIdentifier,
            String metadataPrefix)
    throws TransformerException, SAXException, IOException,
    ParserConfigurationException {
        Document srResponse;
        String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
        srResponse =
            getSearchRetrieveResponse(sruURL,
                    localIdentifier,
                    "http://www.openarchives.org/OAI/2.0/#header",
            "xml");
        return getRecords(srResponse);
    }

    private Object getFullRecord(String sruURL,
            String oaiIdentifier,
            String metadataPrefix)
    throws SAXException, TransformerException, IOException,
    ParserConfigurationException {
        NodeList nodeList = getIdentifierRecords(sruURL, oaiIdentifier, metadataPrefix);
        if (nodeList != null) {
            HashMap hashMap = new HashMap();
            hashMap.put("header", getRecordData(nodeList.item(0)));
            String nativeRecordSchema = getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
            String localIdentifier = getRecordFactory().fromOAIIdentifier(oaiIdentifier);
            Document srResponse =
                getSearchRetrieveResponse(sruURL,
                        localIdentifier,
                        nativeRecordSchema,
                "xml");
            nodeList = getRecords(srResponse);
            hashMap.put("metadata", getRecordData(nodeList.item(0)));
            return hashMap;
        }
        return null;
    }

    private Object getNativeMetadata(String localIdentifier, String metadataPrefix)
    throws TransformerException, SAXException, IOException,
    ParserConfigurationException {
        String nativeRecordSchema = "info:srw/schema/1/xer";
//            getRecordFactory().getCrosswalks().getNativeRecordSchema(metadataPrefix);
        Document srResponse;
        srResponse =
            getSearchRetrieveResponse(sruURL,
                    localIdentifier,
                    nativeRecordSchema,
            "xml");
        NodeList nodeList = getRecords(srResponse);
        return getRecordData(nodeList.item(0));
    }

    public Map listRecords(String from,
            String until,
            String set,
            String metadataPrefix)
    throws
    BadArgumentException,
    CannotDisseminateFormatException,
    NoItemsMatchException,
    OAIInternalServerError {
        if (set != null
                && set.length() > 0
                && from.equals(toFinestFrom("0000-00-00"))
                && until.equals(toFinestUntil("9999-99-99"))) {
            from = null;
            until = null;
        }
        if (toFinestFrom("0000-00-00").equals(from)) {
            from = null;
        }
        if (toFinestUntil("9999-12-31").compareTo(until) <= 0) {
            until = null;
        }
        Map listRecordsMap = new HashMap();
        ArrayList recordsList = new ArrayList();
        Document srResponse;
        try {
            srResponse =
                getSearchRetrieveResponse(sruURL,
                        from,
                        until,
                        set,
                        "info:srw/schema/1/xer",
                        1,
                        maxListSize,
                "xml");
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
        try {
            NodeList nodeList = getRecords(srResponse);
            if (nodeList != null) {
                RecordFactory recordFactory = getRecordFactory();
                String schemaURL = null;
                if (metadataPrefix != null) {
                    if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                            == null)
                        throw new CannotDisseminateFormatException(metadataPrefix);
                }
                for (int i = 0; i < nodeList.getLength(); ++i) {
                    Element recEl = getRecordData(nodeList.item(i));
                    recordsList.add(
                            recordFactory.create(recEl,
                                    schemaURL,
                                    metadataPrefix));
                }
                String nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
                if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                    String resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
                    StringBuffer resumptionToken = new StringBuffer();
                    resumptionToken.append(resultSetId);
                    resumptionToken.append(":");
                    resumptionToken.append(nextRecordPosition);
                    resumptionToken.append(":");
                    resumptionToken.append(metadataPrefix);
                    listRecordsMap.put(
                            "resumptionMap",
                            getResumptionMap(resumptionToken.toString()));
                }
            } else {
                throw new NoItemsMatchException();
            }
            listRecordsMap.put("records", recordsList.iterator());
            return listRecordsMap;
        } catch (TransformerException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (SAXException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    public Map listRecords(String resumptionToken)
    throws BadResumptionTokenException, OAIInternalServerError {
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resultSetId;
        String nextRecordPosition;
        String metadataPrefix;

        try {
            resultSetId = tokenizer.nextToken();
            nextRecordPosition = tokenizer.nextToken();
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            e.printStackTrace();
            throw new BadResumptionTokenException();
        }
        Map listRecordsMap = new HashMap();
        ArrayList recordsList = new ArrayList();
        try {
            Document srResponse =
                getSearchRetrieveResponse(sruURL,
                        resultSetId,
                        nextRecordPosition,
                        "http://www.openarchives.org/OAI/2.0/#header",
                        maxListSize,
                "xml");
            NodeList nodeList = getRecords(srResponse);
            RecordFactory recordFactory = getRecordFactory();
            String schemaURL = null;
            if (metadataPrefix != null) {
                if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix))
                        == null)
                    throw new CannotDisseminateFormatException(metadataPrefix);
            }
            for (int i = 0; i < nodeList.getLength(); ++i) {
                Object rec = getRecordData(nodeList.item(i));
                HashMap hashMap = new HashMap();
                hashMap.put("header", rec);
                String localIdentifier = getRecordFactory().getLocalIdentifier(hashMap);
                hashMap.put("metadata", getNativeMetadata(localIdentifier,
                        metadataPrefix));
                recordsList.add(
                        recordFactory.create(hashMap,
                                schemaURL,
                                metadataPrefix));
            }
            nextRecordPosition = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:nextRecordPosition", xmlnsEl).str();
            if (nextRecordPosition != null && nextRecordPosition.length() > 0) {
                resultSetId = XPathAPI.eval(srResponse, "/srw:searchRetrieveResponse/srw:resultSetId", xmlnsEl).str();
                StringBuffer newResumptionToken = new StringBuffer();
                newResumptionToken.append(resultSetId);
                newResumptionToken.append(":");
                newResumptionToken.append(nextRecordPosition);
                newResumptionToken.append(":");
                newResumptionToken.append(metadataPrefix);
                listRecordsMap.put(
                        "resumptionMap",
                        getResumptionMap(newResumptionToken.toString()));
            }
            listRecordsMap.put("records", recordsList.iterator());
        } catch (Throwable e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
        return listRecordsMap;
    }

    private Document
    getSearchRetrieveResponse(String sruURL,
            String from,
            String until,
            String set,
            String recordSchema,
            int startRecord,
            int maximumRecords,
            String recordPacking)
    throws SAXException, ParserConfigurationException,
    UnsupportedEncodingException, IOException {
        StringBuffer query = new StringBuffer();
        if ((from != null && from.length() > 0)
                || (until != null && until.length() > 0)) {
            query.append("(");
            if (from != null && from.length() > 0) {
                query.append("oai.datestamp>=\"");
                query.append(normalizeTerm(from));
                query.append("\"");
            }
            if (until != null && from.length() > 0) {
                if (query.length() > 0)
                    query.append(" and ");
                query.append("oai.datestamp<=\"");
                query.append(normalizeTerm(until));
                query.append("\"");
            }
            query.append(")");
        }
        if (set != null && set.length() > 0) {
            if (query.length() > 0)
                query.append(" and ");
            query.append("oai.set=\"");
            query.append(normalizeTerm(set));
            query.append("\"");
        }
        StringBuffer request = new StringBuffer(sruURL);
        request.append("?operation=searchRetrieve&version=1.1&resultSetTTL=600&query=");
        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
        request.append("&startRecord=").append(Integer.toString(startRecord));
        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
        request.append("&recordPacking=").append(recordPacking);
        request.append("&sortKeys=").append(URLEncoder.encode(sortKeys, "UTF-8"));
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(request.toString());
    }

    private Document
    getSearchRetrieveResponse(String sruURL,
            String resultSetId,
            String nextRecordPosition,
            String recordSchema,
            int maximumRecords,
            String recordPacking)
    throws SAXException, ParserConfigurationException,
    UnsupportedEncodingException, IOException {
        StringBuffer query = new StringBuffer();
        query.append("cql.resultSetId=");
        query.append(resultSetId);
        StringBuffer request = new StringBuffer(sruURL);
        request.append("?operation=searchRetrieve&resultSetTTL=600&version=1.1&query=");
        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
        request.append("&startRecord=").append(nextRecordPosition);
        request.append("&maximumRecords=").append(Integer.toString(maximumRecords));
        request.append("&recordPacking=").append(recordPacking);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(request.toString());
    }

    private Document
    getSearchRetrieveResponse(String sruURL,
            String localIdentifier,
            String recordSchema,
            String recordPacking)
    throws SAXException, IOException, ParserConfigurationException,
    UnsupportedEncodingException {
        StringBuffer query = new StringBuffer();
        query.append("oai.identifier exact \"");
        query.append(localIdentifier);
        query.append("\"");

        StringBuffer request = new StringBuffer(sruURL);
        request.append("?operation=searchRetrieve&resultSetTTL=0&version=1.1&query=");
        request.append(URLEncoder.encode(query.toString(), "UTF-8"));
        request.append("&recordSchema=").append(URLEncoder.encode(recordSchema, "UTF-8"));
        request.append("&startRecord=1&maximumRecords=1");
        request.append("&recordPacking=").append(recordPacking);
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(request.toString());
    }

    private Element getRecordData(Node record)
    throws TransformerException, SAXException, IOException, ParserConfigurationException {
        Node result = XPathAPI.selectSingleNode(record,
                "srw:recordData/*[1]",
                xmlnsEl);
        return toDocument((Element)result).getDocumentElement();
    }

    private Document toDocument(Element el)
    throws TransformerException, SAXException, IOException, ParserConfigurationException {
        DOMSource source = new DOMSource(el);
        StringWriter sw = new StringWriter();
        StreamResult result = new StreamResult(sw);
        synchronized (transformer) {
            transformer.transform(source, result);
        }
        DocumentBuilder builder = getBuilder();
        Document doc = builder.parse(new InputSource(new StringReader(sw.toString())));
        Element docEl = doc.getDocumentElement();
        if (docEl.getNamespaceURI() == null) {
            docEl.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "");
        }
        return doc;
    }

    private NodeList getRecords(Document srResponse)
    throws TransformerException {
        return XPathAPI.selectNodeList(srResponse, "/srw:searchRetrieveResponse/srw:records/srw:record", xmlnsEl);
    }

    public void close() {
    }
}

/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerException;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * NodeRecordFactory converts native XML "items" to "record" Strings.
 */
public class XerSRURecordFactory extends RecordFactory {

    private static Element xmlnsEl = null;
    private static DocumentBuilderFactory factory = null;
    static {
        try {
            factory =
                DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            DOMImplementation impl = builder.getDOMImplementation();
            Document xmlnsDoc =
                impl.createDocument(
                        "http://www.oclc.org/research/software/oai/harvester",
                        "harvester:xmlnsDoc",
                        null);
            xmlnsEl = xmlnsDoc.getDocumentElement();
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                    "xmlns:xsd",
            "http://www.w3.org/2001/XMLSchema");
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                    "xmlns:srw",
            "http://www.loc.gov/zing/srw/");
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                    "xmlns:oai",
            "http://www.openarchives.org/OAI/2.0/");
            xmlnsEl.setAttributeNS("http://www.w3.org/2000/xmlns/",
                    "xmlns:explain",
            "http://explain.z3950.org/dtd/2.0/");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public XerSRURecordFactory(Properties properties)
    throws IllegalArgumentException {
        super(properties);
//        this(properties, getCrosswalkMap(properties.getProperty("SRUOAICatalog.sruURL")));
    }

    /**
     * Construct an NodeRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public XerSRURecordFactory(Properties properties, HashMap crosswalkMap)
    throws IllegalArgumentException {
        super(crosswalkMap);
    }

//    private static HashMap getCrosswalkMap(String sruURL)
//    throws IllegalArgumentException {
//        try {
//            DocumentBuilder parser = factory.newDocumentBuilder();
//            Document explainDoc = parser.parse(sruURL);
//            NodeList schemas = XPathAPI.selectNodeList(explainDoc, "/srw:explainResponse/srw:record/srw:recordData/explain:explain/explain:schemaInfo/explain:schema", xmlnsEl);
//
//            // Load the formats the repository supports directly
//            HashMap crosswalkMap = new HashMap();
//            for (int i=0; i<schemas.getLength(); ++i) {
//                Object[] crosswalkItem = crosswalkItemFactory(schemas.item(i));
//                for (int j=0; j<crosswalkItem.length; ++j) {
//                    CrosswalkItem currentItem = (CrosswalkItem)crosswalkItem[j];
//                    // 		logger.debug(currentItem.toString());
//                    Object key = currentItem.getMetadataPrefix();
//                    CrosswalkItem storedItem = (CrosswalkItem)crosswalkMap.get(key);
//                    if (storedItem == null || (currentItem.getRank() < storedItem.getRank())) {
//                        crosswalkMap.put(key, currentItem);
//                    }
//                }
//            }
//
//            HashMap moreCrosswalkMap = new HashMap();
//
//            // Now, combine the lists with the originals taking precidence
//            moreCrosswalkMap.putAll(crosswalkMap);
//            return moreCrosswalkMap;
//        } catch (Exception e) {
//            e.printStackTrace();
//            throw new IllegalArgumentException(sruURL);
//        }
//    }

//    public static Object[] crosswalkItemFactory(Node explainSchemaNode)
//    throws TransformerException, OAIInternalServerError {
//        ArrayList crosswalkItemList = new ArrayList();
//        String nativeRecordSchema = XPathAPI.eval(explainSchemaNode, "@identifier", xmlnsEl).str();
//        String metadataPrefix = XPathAPI.eval(explainSchemaNode, "@name", xmlnsEl).str();
//        String schema = XPathAPI.eval(explainSchemaNode, "@location", xmlnsEl).str();
//        String metadataNamespace = null;
//        try {
//            DocumentBuilder parser = factory.newDocumentBuilder();
//            Document schemaDoc = parser.parse(schema);
//            metadataNamespace = XPathAPI.eval(schemaDoc, "/xsd:schema/@targetNamespace", xmlnsEl).str();
//        } catch (SAXParseException e) {
//            metadataNamespace = "";
//        } catch (Exception e) {
//            System.out.println("Failed to get schema: " + schema);
//            e.printStackTrace();
//            metadataNamespace = "";
//        }
//        CrosswalkItem crosswalkItem = new CrosswalkItem(nativeRecordSchema, metadataPrefix, schema, metadataNamespace, NodePassThruCrosswalk.class);
//        crosswalkItemList.add(crosswalkItem);
//        return crosswalkItemList.toArray();
//    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
        return identifier;
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
        return getLocalIdentifier(nativeItem);
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
        try {
            Element recordEl = (Element)nativeItem;
//            Node identifierNode =
//                XPathAPI.selectSingleNode(
//                        recordEl,
//                        "/oai:header/oai:identifier",
//                        xmlnsEl);
//            if (identifierNode != null) {
//                return XPathAPI.eval(identifierNode, "string()").str();
//            }
            return "foo";
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
    throws IllegalArgumentException {
        Element recordData = (Element) nativeItem;
        try {
            String datetime = XPathAPI.eval(recordData,
                    "/ber/tag0/tag0[@class='private'/tag0]/",
                    xmlnsEl)
                    .str();
            return datetime;
        } catch (TransformerException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
    throws IllegalArgumentException {
        //         List setSpecs = (List)((HashMap)nativeItem).get("setSpecs");
        //         if (setSpecs != null) 
        //             return setSpecs.iterator();
        //         else
        return null;
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem)
    throws IllegalArgumentException {
        return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
    throws IllegalArgumentException {
        return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(
            Object nativeItem,
            String schemaLocation,
            String metadataPrefix) {
        return null;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.catalog.helpers;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.SortedMap;
import java.util.TreeMap;

import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

import ORG.oclc.oai.util.OAIUtil;

public class RecordStringHandler extends DefaultHandler {
    private static final boolean debug = false;
    private static final String OAI_NS = "http://www.openarchives.org/OAI/2.0/";
    private static final String DATABASE_NS = "http://www.oclc.org/pears/";
//     private static final String OAI_DC_NS = "http://www.openarchives.org/OAI/2.0/oai_dc/";
//     private static final String MARC21_NS = "http://www.loc.gov/MARC21/slim";
//     private static final String REG_NS = "http://info-uri.info/registry";
//     private static final String MTX_NS = "http://www.w3.org/1999/xhtml";
//     private static final String PRO_NS = "info:ofi/pro";
//     private static final String XSD_NS = "http://www.w3.org/2001/XMLSchema";
//     private static final String XSL_NS = "http://www.w3.org/1999/XSL/Transform";
    private static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance";
    private SortedMap nativeRecords = new TreeMap();
    private int recordFlag = 0;
    private int metadataFlag = 0;
    private StringWriter metadata = null;
    private int recordidFlag = 0;
    private StringBuffer recordid = null;
    private String schemaLocation = null;
    private int identifierFlag = 0;
    private StringBuffer identifier = null;
    private int datestampFlag = 0;
    private StringBuffer datestamp = null;
    private ArrayList setSpecs = null;
    private int setSpecFlag = 0;
    private StringBuffer setSpec = null;
//     private static Logger logger = Logger.getLogger(RecordStringHandler.class);
//     static {
//         BasicConfigurator.configure();
//     }

    public SortedMap getNativeRecords() { return nativeRecords; }
    
    public void startElement(String uri, String localName, String qName,
			     Attributes attrs) {
        if (debug) {
            System.out.println("startElement: " + uri + ", " + localName + ", "
                               + qName + ", ");
        }
	if (OAI_NS.equals(uri) && "record".equals(localName)) {
	    setSpecs = new ArrayList();
	    recordFlag++;
	}
	if (metadataFlag > 0) {
	    metadata.write("<" + getName(localName, qName));
	    if (attrs != null) {
		for (int i=0; i<attrs.getLength(); ++i) {
		    String attributeName = getName(attrs.getLocalName(i),
						   attrs.getQName(i));
		    
		    // modified by Colin DOig, 6 September 2006
		    // xmlEncode ",&,< etc within attributes
		    // previously invalid XML was being produced.
		    metadata.write(" " + attributeName + "=\"" +
				  OAIUtil.xmlEncode(attrs.getValue(i)) + "\"");
		}
	    }
	    metadata.write(">");
	}
	if (schemaLocation == null
            && metadataFlag == 1) {
// 	    && ((OAI_DC_NS.equals(uri) && "dc".equals(localName))
//             || (XSD_NS.equals(uri) && "schema".equals(localName))
//             || (XSL_NS.equals(uri) && "stylesheet".equals(localName))
//             || (MARC21_NS.equals(uri) && "record".equals(localName))
//             || (REG_NS.equals(uri) && "info-registry-entry".equals(localName))
//             || (MTX_NS.equals(uri) && "html".equals(localName))
//             || (PRO_NS.equals(uri) && "profile".equals(localName)))) {
            schemaLocation = attrs.getValue(XSI_NS, "schemaLocation");
	}
	if (OAI_NS.equals(uri) && "metadata".equals(localName)) {
	    if (metadata == null) {
		metadata = new StringWriter();
	    }
	    metadataFlag++;
	}
	if (OAI_NS.equals(uri) && "identifier".equals(localName)) {
	    if (identifier == null) {
		identifier = new StringBuffer();
	    }
	    identifierFlag++;
	}
	if (DATABASE_NS.equals(uri) && "recordid".equals(localName)) {
	    if (recordid == null) {
		recordid = new StringBuffer();
	    }
	    recordidFlag++;
	}
	if (OAI_NS.equals(uri) && "datestamp".equals(localName)) {
	    if (datestamp == null) {
		datestamp = new StringBuffer();
	    }
	    datestampFlag++;
	}
	if (OAI_NS.equals(uri) && "setSpec".equals(localName)) {
	    if (setSpec == null) {
		setSpec = new StringBuffer();
	    }
	    setSpecFlag++;
	}
    }

    public void endElement(String uri, String localName, String qName) {
	if (OAI_NS.equals(uri) && "identifier".equals(localName)) {
	    identifierFlag--;
	}
	if (DATABASE_NS.equals(uri) && "recordid".equals(localName)) {
	    recordidFlag--;
	}
	if (OAI_NS.equals(uri) && "datestamp".equals(localName)) {
	    datestampFlag--;
	}
	if (OAI_NS.equals(uri) && "setSpec".equals(localName)) {
	    setSpecs.add(setSpec.toString());
	    setSpec = null;
	    setSpecFlag--;
	}
	if (OAI_NS.equals(uri) && "record".equals(localName)) {
	    recordFlag--;
	    if (recordFlag == 0) {
		HashMap nativeRecord = new HashMap();
		nativeRecord.put("recordString", metadata.toString());
//                 logger.debug(metadata.toString());
                if (debug) {
                    System.out.println("metadata: " + metadata.toString());
                }
                nativeRecord.put("localIdentifier", identifier.toString());
                if (debug) {
                    System.out.println("localIdentifier=" + identifier.toString());
                }
                nativeRecord.put("recordid", recordid.toString());
                if (debug) {
                    System.out.println("recordid=" + recordid.toString());
                }
                nativeRecord.put("schemaLocation", schemaLocation);
                if (debug) {
                    System.out.println("schemaLocation=" + schemaLocation);
                }
                nativeRecord.put("datestamp", datestamp.toString());
                if (debug) {
                    System.out.println("datestamp=" + datestamp.toString());
                }
 		nativeRecord.put("setSpecs", setSpecs);
                nativeRecords.put(recordid.toString().toLowerCase(), nativeRecord);
		setSpecs = null;
                identifier = null;
                metadata = null;
                recordid = null;
                schemaLocation = null;
                datestamp = null;
	    }
	}
	if (OAI_NS.equals(uri) && "metadata".equals(localName)) {
	    metadataFlag--;
	}
	if (metadataFlag > 0) {
	    metadata.write("</" + getName(localName, qName) + ">");
	}
    }

    public void characters(char[] ch, int start, int length) {
        String s = new String(ch, start, length);
	if (metadataFlag > 0) {
	    metadata.write(OAIUtil.xmlEncode(s));
	}
	if (identifierFlag > 0) {
	    identifier.append(s);
	}
	if (recordidFlag > 0) {
	    recordid.append(s);
	}
	if (datestampFlag > 0) {
	    datestamp.append(s);
	}
	if (setSpecFlag > 0) {
	    setSpec.append(s);
	}
    }

    private String getName(String s1, String s2) {
        if (s2==null || "".equals(s2))
            return s1;
        else
            return s2;
    }

//     public InputSource resolveEntity(String publicId, String systemId) {
//         System.out.println("RecordStringHandler.resolveEntity:publicId=" + publicId);
//         System.out.println("RecordStringHandler.resolveEntity:systemId=" + systemId);
//         return null;
//     }

//     public void skippedEntity(String name) {
//         System.out.println("RecordStringHandler.skippedEntity:name=" + name);
//     }

//     public void unparsedEntityDecl(String name, String publicId, String systemId,
//                                    String notationName) {
//         System.out.println("RecordStringHandler.unparsedEntityDecl: name=" + name);
//         System.out.println("RecordStringHandler.unparsedEntityDecl: publicId=" + publicId);
//         System.out.println("RecordStringHandler.unparsedEntityDecl: systemId=" + systemId);
//         System.out.println("RecordStringHandler.unparsedEntityDecl: notationName=" + notationName);
//     }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to xsd. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copycdwalite extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copycdwalite(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.getty.edu/CDWA/CDWALite http://www.getty.edu/CDWA/CDWALite/CDWALite-xsd-public-v1-1.xsd", null);
        try {
            String xsltName = properties.getProperty("Copycdwalite.xsltName");
            String classpathXSL = properties.getProperty("Copycdwalite.classpathXSL");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            } else if (classpathXSL != null) {
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classpathXSL);
                StreamSource xslSource = new StreamSource(is);
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("cdwalite");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to desc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copydesc extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copydesc(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://errol.oclc.org/oai:xmlregistry.oclc.org:sts/desc http://errol.oclc.org/oai:xmlregistry.oclc.org:sts/desc.xsd", null);
        try {
            String xsltName = properties.getProperty("Copydesc.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("desc");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to mets. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copymets extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copymets(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.loc.gov/METS/ http://www.loc.gov/standards/mets/mets.xsd", null);
        try {
            String xsltName = properties.getProperty("Copymets.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("mets");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copyoai_dc extends XSLTCrosswalk {
    private boolean debug=false;
//     private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copyoai_dc(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd", (String)null);
        try {
            String xsltName = properties.getProperty("Copyoai_dc.xsltName");
            String classpathXSL = properties.getProperty("Copyoai_dc.classpathXSL");
            if (debug) System.out.println("Copyoai_dc.Copyoai_dc: xsltName="
                                          + xsltName);
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            } else if (classpathXSL != null) {
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classpathXSL);
                StreamSource xslSource = new StreamSource(is);
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("oai_dc");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to mtx. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copyxhtml extends XSLTCrosswalk {
//     private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copyxhtml(Properties properties)
        throws OAIInternalServerError {
  	super(properties, "http://www.w3.org/1999/xhtml http://www.w3.org/2002/08/xhtml/xhtml1-transitional.xsd", "text/html; charset=UTF-8", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
        try {
            String xsltName = properties.getProperty("Copyxhtml.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
                System.out.println("Copyxhtml.Copyxhtml: transformer="
                                   + this.transformer);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("xhtml");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to xoaiconfig. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copyxoaiconfig extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copyxoaiconfig(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://errol.oclc.org/oai:xmlregistry.oclc.org:xoai/config http://errol.oclc.org/oai:xmlregistry.oclc.org:xoai/config.xsd", null);
        try {
            String xsltName = properties.getProperty("Copyxoaiconfig.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("xoaiconfig");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to xsd. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copyxsd extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copyxsd(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd", null);
        try {
            String xsltName = properties.getProperty("Copyxsd.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("xsd");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to xsl. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class Copyxsl extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public Copyxsl(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.w3.org/1999/XSL/Transform http://www.w3.org/1999/XSL/Transform.xsd", null);
        try {
            String xsltName = properties.getProperty("Copyxsl.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        ArrayList list = (ArrayList)nativeItem;
        return list.contains("xsl");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;

/**
 * Converts a native "item" to an OAI metadataFormat.
 */
public abstract class Crosswalk {
    /**
     * The schemaLocation supported by this crosswalk
     */
    private String schemaLocation;
    private String contentType;
    private String docType;
    private String encoding;

    /**
     * Constructor
     *
     * @param schemaLocation the schemaLocation supported by this crosswalk
     */
    public Crosswalk(String schemaLocation) {
        this(schemaLocation, (String)null);
    }

    /**
     * Constructor
     *
     * @param schemaLocation the schemaLocation supported by this crosswalk
     */
    public Crosswalk(String schemaLocation, String contentType) {
        this(schemaLocation, contentType, (String)null);
    }

    public Crosswalk(String schemaLocation, String contentType, String docType) {
        this(schemaLocation, contentType, docType, (String)null);
    }
    /**
     * Constructor
     *
     * @param schemaLocation the schemaLocation supported by this crosswalk
     */
    public Crosswalk(String schemaLocation, String contentType, String docType, String encoding) {
        this.schemaLocation = schemaLocation;
        if (contentType == null)
            contentType = "text/xml; charset=UTF-8";
        this.contentType = contentType;
        this.docType = docType;
        this.encoding = encoding;
    }

    /**
     * returns the schemaLocation
     *
     * @return the schemaLocation
     */
    public String getSchemaLocation() { return schemaLocation; }

    public String getContentType() { return contentType; }

    public String getDocType() { return docType; }

    public String getEncoding() { return encoding; }

    /**
     * parse the schemaURL from the schemaLocation
     *
     * @return the schemaURL portion of the schemaLocation
     */
    public String getSchemaURL() {
	StringTokenizer tokenizer = new StringTokenizer(schemaLocation, " ");
	String temp = tokenizer.nextToken();
	try {
	    return tokenizer.nextToken();
	} catch (NoSuchElementException e) {
	    // no namespace for the format
	    return temp;
	}
    }

    /**
     * parse the namespaceURL from the schemaLocation
     *
     * @return the namespaceURL portion of the schemaLocation
     */
    public String getNamespaceURL() {
	StringTokenizer tokenizer = new StringTokenizer(schemaLocation, " ");
	return tokenizer.nextToken();
    }

    /**
     * Can this nativeItem be represented in ETDMS format?
     * @param nativeItem a record in native format
     * @return true if ETDMS format is possible, false otherwise.
     */
    public abstract boolean isAvailableFor(Object nativeItem);
    
    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the XML to be stored within the <metadata> element.
     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
     */
    public abstract String createMetadata(Object nativeItem)
	throws CannotDisseminateFormatException;

    /**
     * returns the schemaLocation for this crosswalk.
     * @return a String containing the schemaLocation.
     */
    public String toString() {
	return schemaLocation;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

public class CrosswalkItem {
    private String contentType = null;
    private String docType = null;
    private String encoding = null;
    private String nativeRecordSchema = null;
    private String metadataPrefix = null;
    private String schema = null;
    private String metadataNamespace = null;
    private String xsltName = null;
    private Crosswalk crosswalk = null;
    private int rank = -1;
    
    public static final int RANK_DIRECTLY_AVAILABLE = 0;
    public static final int RANK_DERIVED = 1;

    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
			 String schema, String metadataNamespace,
			 Crosswalk crosswalk, int rank) {
	this(nativeRecordSchema, metadataPrefix, schema, metadataNamespace,
	     rank);
	this.crosswalk = crosswalk;
    }
    
    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
			 String schema, String metadataNamespace,
			 int rank) {
	this.nativeRecordSchema = nativeRecordSchema;
	this.metadataPrefix = metadataPrefix;
	this.schema = schema;
	this.metadataNamespace = metadataNamespace;
	this.rank = rank;
    }
    
    public CrosswalkItem(String metadataPrefix, String schema,
			 String metadataNamespace, Crosswalk crosswalk) {
	this(metadataPrefix, metadataPrefix, schema, metadataNamespace,
	     RANK_DIRECTLY_AVAILABLE);
	this.crosswalk = crosswalk;
    }
    
    public CrosswalkItem(String metadataPrefix, String schema,
			 String metadataNamespace, Class crosswalkClass)
	throws OAIInternalServerError {
	this(metadataPrefix, metadataPrefix, schema, metadataNamespace,
	     RANK_DIRECTLY_AVAILABLE);
	try {
	    this.crosswalk = getCrosswalk(crosswalkClass);
	} catch (Exception e) {
	    e.printStackTrace();
	    throw new OAIInternalServerError(e.getMessage());
	}
    }
    
    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
			 String schema, String metadataNamespace,
			 Class crosswalkClass, String xsltName)
	throws OAIInternalServerError {
	this(nativeRecordSchema, metadataPrefix, schema, metadataNamespace,
	     RANK_DERIVED);
	this.xsltName = xsltName;
	try {
	    this.crosswalk = getCrosswalk(crosswalkClass);
	} catch (Exception e) {
	    e.printStackTrace();
	    throw new OAIInternalServerError(e.getMessage());
	}
    }
    
    public CrosswalkItem(String nativeRecordSchema, String metadataPrefix,
			 String schema, String metadataNamespace,
			 Class crosswalkClass)
	throws OAIInternalServerError {
	this(nativeRecordSchema, metadataPrefix, schema, metadataNamespace,
	     RANK_DIRECTLY_AVAILABLE);
	try {
	    this.crosswalk = getCrosswalk(crosswalkClass);
	} catch (Exception e) {
	    e.printStackTrace();
	    throw new OAIInternalServerError(e.getMessage());
	}
    }

    private Crosswalk getCrosswalk(Class crosswalkClass)
	throws NoSuchMethodException, InstantiationException,
	       IllegalAccessException, InvocationTargetException {
	Constructor constructor = crosswalkClass.getConstructor(new Class[] {CrosswalkItem.class});
	return (Crosswalk)constructor.newInstance(new Object[] {this});
    }
	
    public String getNativeRecordSchema() { return nativeRecordSchema; }
    public String getMetadataPrefix() { return metadataPrefix; }
    public String getMetadataNamespace() { return metadataNamespace; }
    public String getSchema() { return schema; }
    public Crosswalk getCrosswalk() { return crosswalk; }
    public String getContentType() { return contentType; }
    public String getDocType() { return docType; }
    public String getEncoding() { return encoding; }
    public String getXSLTName() { return xsltName; }
    
    public int getRank() { return rank; }

    public String toString() {
	StringBuffer sb = new StringBuffer();
	sb.append("CrosswalkItem: " );
	sb.append(getNativeRecordSchema());
	sb.append(":");
 	sb.append(getMetadataPrefix());
	sb.append(":");
	sb.append(getMetadataNamespace());
	sb.append(":");
	sb.append(getSchema());
	sb.append(":");
	sb.append(getCrosswalk());
	sb.append(":");
	sb.append(getRank());
	return sb.toString();
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.lang.reflect.Constructor;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * Crosswalks manages all the Crosswalk objects this
 * repository supports.
 *
 * @author Jeffrey A. Young
 */
public class Crosswalks {
    private static final boolean debug = false;

    // map of metadataPrefix/CrosswalkItem
    private Map crosswalksMap = new HashMap();

    /**
     * Find out which metadataFormats this repository supports and create
     * the corresponding Crosswalk objects for each.
     *
     * @param properties a properties object containing Crosswalks entries
     */
    public Crosswalks(Properties properties) {
        String propertyPrefix = "Crosswalks.";
        Enumeration propNames = properties.propertyNames();
        while (propNames.hasMoreElements()) {
            String propertyName = (String)propNames.nextElement();
            if (propertyName.startsWith(propertyPrefix)) {
                String schemaLabel = propertyName.substring(propertyPrefix.length());
		String formatClassName = (String)properties.get(propertyName);
		try {
		    Class crosswalkClass = Class.forName(formatClassName);
		    Crosswalk crosswalk = null;
		    try {
			Constructor crosswalkConstructor = crosswalkClass.getConstructor(new Class[] {String.class, Properties.class});
			crosswalk = (Crosswalk)crosswalkConstructor.newInstance(new Object[] {schemaLabel, properties});
		    } catch (NoSuchMethodException e) {
			Constructor crosswalkConstructor = crosswalkClass.getConstructor(new Class[] {Properties.class});
			crosswalk = (Crosswalk)crosswalkConstructor.newInstance(new Object[] {properties});
		    }
		    CrosswalkItem crosswalkItem = new CrosswalkItem(schemaLabel, crosswalk.getSchemaURL(), crosswalk.getNamespaceURL(), crosswalk);
		    crosswalksMap.put(schemaLabel, crosswalkItem);
                    if (debug) {
                        System.out.println("Crosswalks.Crosswalks: " + schemaLabel + "=" + crosswalk);
                    }
		} catch (Exception e) {
		    System.err.println("Crosswalks: couldn't construct: " + formatClassName);
		    e.printStackTrace();
		}
            }
        }
        if (crosswalksMap.size() == 0) {
            System.err.println("Crosswalks entries are missing from properties file");
        }
    }

    public Crosswalks(Map crosswalkItemMap) {
	Iterator iter = crosswalkItemMap.values().iterator();
	while (iter.hasNext()) {
	    CrosswalkItem crosswalkItem = (CrosswalkItem)iter.next();
	    String schemaLabel = crosswalkItem.getMetadataPrefix();
// 	    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
	    crosswalksMap.put(schemaLabel, crosswalkItem);
	}
	
	if (crosswalksMap.size() == 0) {
            System.err.println("Crosswalks entries are missing from properties file");
	}
    }

    public String toString() {
        StringBuffer sb = new StringBuffer();
        Iterator iterator = iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry)iterator.next();
            sb.append(entry.getKey());
	    sb.append("=");
	    CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
	    sb.append(crosswalkItem.getCrosswalk().toString());
	    sb.append("\n");
        }
        return sb.toString();
    }

    /**
     * Get the metadataPrefix associated with the specified namespace/schema
     *
     * @param namespaceURI the namespaceURI portion of the format specifier
     * @param schemaURL the schemaURL portion of the format specifier
     * @return a String containing the metadataPrefix value associated with this pair
     */
    public String getMetadataPrefix(String namespaceURI, String schemaURL) {
        StringBuffer sb = new StringBuffer();
        if (namespaceURI != null) {
        	sb.append(namespaceURI);
        }
        sb.append(" ")
        .append(schemaURL);
	Iterator iterator = crosswalksMap.entrySet().iterator();
	while (iterator.hasNext()) {
	    Map.Entry entry = (Map.Entry)iterator.next();
	    if (((CrosswalkItem)entry.getValue()).getCrosswalk().getSchemaLocation().equals(sb.toString())) {
		return (String)entry.getKey();
	    }
	}
        return null;
    }

    /**
     * Get the schemaURL associated with the specified metadataPrefix
     *
     * @param metadataPrefix the prefix desired
     * @return a String containing the schemaURL associated with the metadataPrefix
     */
    public String getSchemaURL(String metadataPrefix) {
        String schemaLocation = getSchemaLocation(metadataPrefix);
        StringTokenizer tokenizer = new StringTokenizer(schemaLocation);
        String temp = tokenizer.nextToken();
	try {
	    return tokenizer.nextToken();
	} catch (NoSuchElementException e) {
	    return temp;
	}
    }
    
    /**
     * Get the namespaceURI associated with the specified metadataPrefix
     *
     * @param metadataPrefix the prefix desired
     * @return a String containing the namespaceURI associated with the metadataPrefix
     */
    public String getNamespaceURI(String metadataPrefix) {
        String schemaLocation = getSchemaLocation(metadataPrefix);
        StringTokenizer tokenizer = new StringTokenizer(schemaLocation);
        return tokenizer.nextToken();
    }

    public String getNativeRecordSchema(String metadataPrefix) {
	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
	if (crosswalkItem == null) {
	    return null;
	} else {
	    return crosswalkItem.getNativeRecordSchema();
	}
    }
    
    /**
     * Get the namespaceURI/schemaURL associated with the specified metadataPrefix
     *
     * @param metadataPrefix the prefix desired
     * @return a String containing the namespaceURI/schemaURL associated with the metadataPrefix
     */
    public String getSchemaLocation(String metadataPrefix) {
	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
	if (crosswalkItem != null)
	    return crosswalkItem.getCrosswalk().getSchemaLocation();
	else
	    return null;
    }

    /**
     * Get the namespaceURI/schemaURL associated with the specified metadataPrefix
     *
     * @param metadataPrefix the prefix desired
     * @return a String containing the namespaceURI/schemaURL associated with the metadataPrefix
     */
    public String getContentType(String metadataPrefix) {
	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
	if (crosswalkItem != null)
	    return crosswalkItem.getCrosswalk().getContentType();
	else
	    return null;
    }

    /**
     * Get the DOCTYPE associated with the specified metadataPrefix
     *
     * @param metadataPrefix the prefix desired
     * @return a String containing the DOCTYPE associated with the metadataPrefix
     */
    public String getDocType(String metadataPrefix) {
	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
	if (crosswalkItem != null)
	    return crosswalkItem.getCrosswalk().getDocType();
	else
	    return null;
    }

    public String getEncoding(String metadataPrefix) {
	CrosswalkItem crosswalkItem = (CrosswalkItem)crosswalksMap.get(metadataPrefix);
	if (crosswalkItem != null)
	    return crosswalkItem.getCrosswalk().getEncoding();
	else
	    return null;
    }

    /**
     * Does the specified metadataPrefix appears in the list of supportedFormats?
     *
     * @param metadataPrefix the prefix desired
     * @return true if prefix is supported, false otherwise.
     */
    public boolean containsValue(String metadataPrefix) {
        return (crosswalksMap.get(metadataPrefix) != null);
    }

    /**
     * Get an iterator containing Map.Entry's for the supported formats.
     *
     * @return an Iterator containing Map.Entry's for each supported format.
     */
    public Iterator iterator() {
 	return crosswalksMap.entrySet().iterator();
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.util.HashMap;
import java.util.Properties;

import ORG.oclc.oai.util.OAIUtil;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class ExtendedJDBC2oai_dc extends Crosswalk {
    private String dcTitleLabel = null;
    private String dcCreatorLabel = null;
    private String dcSubjectLabel = null;
    private String dcDescriptionLabel = null;
    private String dcPublisherLabel = null;
    private String dcContributorLabel = null;
    private String dcDateLabel = null;
    private String dcTypeLabel = null;
    private String dcFormatLabel = null;
    private String dcIdentifierLabel = null;
    private String dcSourceLabel = null;
    private String dcLanguageLabel = null;
    private String dcRelationLabel = null;
    private String dcCoverageLabel = null;
    private String dcRightsLabel = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public ExtendedJDBC2oai_dc(Properties properties) {
	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
	dcTitleLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcTitleLabel");
	dcCreatorLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcCreatorLabel");
	dcSubjectLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcSubjectLabel");
	dcDescriptionLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcDescriptionLabel");
	dcPublisherLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcPublisherLabel");
	dcContributorLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcContributorLabel");
	dcDateLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcDateLabel");
	dcTypeLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcTypeLabel");
	dcFormatLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcFormatLabel");
	dcIdentifierLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcIdentifierLabel");
	dcSourceLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcSourceLabel");
	dcLanguageLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcLanguageLabel");
	dcRelationLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcRelationLabel");
	dcCoverageLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcCoverageLabel");
	dcRightsLabel = (String)properties.get("ExtendedJDBC2oai_dc.dcRightsLabel");
    }

    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
	return true; // all records must support oai_dc according to the OAI spec.
    }

    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the XML to be stored within the <metadata> element.
     */
    public String createMetadata(Object nativeItem) {
	Object dcTitle = null;
	Object dcCreator = null;
	Object dcSubject = null;
	Object dcDescription = null;
	Object dcPublisher = null;
	Object dcContributor = null;
	Object dcDate = null;
	Object dcType = null;
	Object dcFormat = null;
	Object dcIdentifier = null;
	Object dcSource = null;
	Object dcLanguage = null;
	Object dcRelation = null;
	Object dcCoverage = null;
	Object dcRights = null;
	HashMap coreTable = (HashMap)((HashMap)nativeItem).get("coreResult");
	StringBuffer sb = new StringBuffer();
	sb.append("<oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\""
		  + getSchemaLocation()
		  + "\">");


	if (dcTitleLabel != null
	    && (dcTitle = coreTable.get(dcTitleLabel)) != null) {
	    sb.append("<dc:title>");
	    sb.append(OAIUtil.xmlEncode(dcTitle.toString()));
	    sb.append("</dc:title>");
	}
	
	if (dcCreatorLabel != null
	    && (dcCreator = coreTable.get(dcCreatorLabel)) != null) {
	    sb.append("<dc:creator>");
	    sb.append(OAIUtil.xmlEncode(dcCreator.toString()));
	    sb.append("</dc:creator>");
	}
	
	if (dcSubjectLabel != null
	    && (dcSubject = coreTable.get(dcSubjectLabel)) != null) {
	    sb.append("<dc:subject>");
	    sb.append(OAIUtil.xmlEncode(dcSubject.toString()));
	    sb.append("</dc:subject>");
	}
	
	if (dcDescriptionLabel != null
	    && (dcDescription = coreTable.get(dcDescriptionLabel)) != null) {
	    sb.append("<dc:description>");
	    sb.append(OAIUtil.xmlEncode(dcDescription.toString()));
	    sb.append("</dc:description>");
	}
	
	if (dcPublisherLabel != null
	    && (dcPublisher = coreTable.get(dcPublisherLabel)) != null) {
	    sb.append("<dc:publisher>");
	    sb.append(OAIUtil.xmlEncode(dcPublisher.toString()));
	    sb.append("</dc:publisher>");
	}
	
	if (dcContributorLabel != null
	    && (dcContributor = coreTable.get(dcContributorLabel)) != null) {
	    sb.append("<dc:contributor>");
	    sb.append(OAIUtil.xmlEncode(dcContributor.toString()));
	    sb.append("</dc:contributor>");
	}
	
	if (dcDateLabel != null
	    && (dcDate = coreTable.get(dcDateLabel)) != null) {
	    sb.append("<dc:date>");
	    sb.append(OAIUtil.xmlEncode(dcDate.toString()));
	    sb.append("</dc:date>");
	}
	
	if (dcTypeLabel != null
	    && (dcType = coreTable.get(dcTypeLabel)) != null) {
	    sb.append("<dc:type>");
	    sb.append(OAIUtil.xmlEncode(dcType.toString()));
	    sb.append("</dc:type>");
	}
	
	if (dcFormatLabel != null
	    && (dcFormat = coreTable.get(dcFormatLabel)) != null) {
	    sb.append("<dc:format>");
	    sb.append(OAIUtil.xmlEncode(dcFormat.toString()));
	    sb.append("</dc:format>");
	}
	
	if (dcIdentifierLabel != null
	    && (dcIdentifier = coreTable.get(dcIdentifierLabel)) != null) {
	    sb.append("<dc:identifier>");
	    sb.append(OAIUtil.xmlEncode(dcIdentifier.toString()));
	    sb.append("</dc:identifier>");
	}
	
	if (dcSourceLabel != null
	    && (dcSource = coreTable.get(dcSourceLabel)) != null) {
	    sb.append("<dc:source>");
	    sb.append(OAIUtil.xmlEncode(dcSource.toString()));
	    sb.append("</dc:source>");
	}
	
	if (dcLanguageLabel != null
	    && (dcLanguage = coreTable.get(dcLanguageLabel)) != null) {
	    sb.append("<dc:language>");
	    sb.append(OAIUtil.xmlEncode(dcLanguage.toString()));
	    sb.append("</dc:language>");
	}
	
	if (dcRelationLabel != null
	    && (dcRelation = coreTable.get(dcRelationLabel)) != null) {
	    sb.append("<dc:relation>");
	    sb.append(OAIUtil.xmlEncode(dcRelation.toString()));
	    sb.append("</dc:relation>");
	}
	
	if (dcCoverageLabel != null
	    && (dcCoverage = coreTable.get(dcCoverageLabel)) != null) {
	    sb.append("<dc:coverage>");
	    sb.append(OAIUtil.xmlEncode(dcCoverage.toString()));
	    sb.append("</dc:coverage>");
	}
	
	if (dcRightsLabel != null
	    && (dcRights = coreTable.get(dcRightsLabel)) != null) {
	    sb.append("<dc:rights>");
	    sb.append(OAIUtil.xmlEncode(dcRights.toString()));
	    sb.append("</dc:rights>");
	}
	
	sb.append("</oai_dc:dc>");
	return sb.toString();
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Properties;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class FileMap2oai_dc extends Crosswalk {
    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public FileMap2oai_dc(Properties properties)
        throws OAIInternalServerError {
	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
        try {
            String xsltName = properties.getProperty("FileMap2oai_dc.xsltName");
	    TransformerFactory tFactory = TransformerFactory.newInstance();
	    if (xsltName != null) {
		StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
		this.transformer = tFactory.newTransformer(xslSource);
	    } else {
		this.transformer = tFactory.newTransformer();
                this.transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
	    }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        return true;
    }

    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the FileMap to be stored within the <metadata> element.
     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
     */
    public String createMetadata(Object nativeItem)
	throws CannotDisseminateFormatException {
	HashMap recordMap = (HashMap)nativeItem;
        try {
            String xmlRec = (new String((byte[])recordMap.get("recordBytes"), "UTF-8")).trim();
            if (xmlRec.startsWith("<?")) {
                int offset = xmlRec.indexOf("?>");
                xmlRec = xmlRec.substring(offset+2);
            }
            StringReader stringReader = new StringReader(xmlRec);
            StreamSource streamSource = new StreamSource(stringReader);
            StringWriter stringWriter = new StringWriter();
            synchronized (transformer) {
                transformer.transform(streamSource, new StreamResult(stringWriter));
            }
            return stringWriter.toString();
        } catch (Exception e) {
            throw new CannotDisseminateFormatException(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Properties;

import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;

/**
 * Convert native "item" to oai_etdms. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class FileMap2oai_etdms extends Crosswalk {
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public FileMap2oai_etdms(Properties properties) {
	super("http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd");
    }

    /**
     * Can this nativeItem be represented in ETDMS format?
     * @param nativeItem a record in native format
     * @return true if ETDMS format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        return true;
    }
    
    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the FileMap to be stored within the <metadata> element.
     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
     */
    public String createMetadata(Object nativeItem)
	throws CannotDisseminateFormatException {
	HashMap recordMap = (HashMap)nativeItem;
        try {
            String xmlRec = (new String((byte[])recordMap.get("recordBytes"), "UTF-8")).trim();
            if (xmlRec.startsWith("<?")) {
                int offset = xmlRec.indexOf("?>");
                xmlRec = xmlRec.substring(offset+2);
            }
            return xmlRec;
        } catch (UnsupportedEncodingException e) {
            throw new CannotDisseminateFormatException("An error occurred");
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.util.HashMap;
import java.util.Properties;

import ORG.oclc.oai.util.OAIUtil;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class JDBC2oai_dc extends Crosswalk {
    private String separator = null;
    private String dcTitleLabel = null;
    private String dcCreatorLabel = null;
    private String dcSubjectLabel = null;
    private String dcDescriptionLabel = null;
    private String dcPublisherLabel = null;
    private String dcContributorLabel = null;
    private String dcDateLabel = null;
    private String dcTypeLabel = null;
    private String dcFormatLabel = null;
    private String dcIdentifierLabel = null;
    private String dcSourceLabel = null;
    private String dcLanguageLabel = null;
    private String dcRelationLabel = null;
    private String dcCoverageLabel = null;
    private String dcRightsLabel = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public JDBC2oai_dc(Properties properties) {
        super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
        dcTitleLabel = (String)properties.get("JDBC2oai_dc.dcTitleLabel");
        dcCreatorLabel = (String)properties.get("JDBC2oai_dc.dcCreatorLabel");
        dcSubjectLabel = (String)properties.get("JDBC2oai_dc.dcSubjectLabel");
        dcDescriptionLabel = (String)properties.get("JDBC2oai_dc.dcDescriptionLabel");
        dcPublisherLabel = (String)properties.get("JDBC2oai_dc.dcPublisherLabel");
        dcContributorLabel = (String)properties.get("JDBC2oai_dc.dcContributorLabel");
        dcDateLabel = (String)properties.get("JDBC2oai_dc.dcDateLabel");
        dcTypeLabel = (String)properties.get("JDBC2oai_dc.dcTypeLabel");
        dcFormatLabel = (String)properties.get("JDBC2oai_dc.dcFormatLabel");
        dcIdentifierLabel = (String)properties.get("JDBC2oai_dc.dcIdentifierLabel");
        dcSourceLabel = (String)properties.get("JDBC2oai_dc.dcSourceLabel");
        dcLanguageLabel = (String)properties.get("JDBC2oai_dc.dcLanguageLabel");
        dcRelationLabel = (String)properties.get("JDBC2oai_dc.dcRelationLabel");
        dcCoverageLabel = (String)properties.get("JDBC2oai_dc.dcCoverageLabel");
        dcRightsLabel = (String)properties.get("JDBC2oai_dc.dcRightsLabel");
        separator = (String)properties.get("JDBC2oai_dc.separator");
    }
    
    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        return true; // all records must support oai_dc according to the OAI spec.
    }
    
    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the XML to be stored within the <metadata> element.
     */
    public String createMetadata(Object nativeItem) {
        HashMap table = (HashMap)nativeItem;
        StringBuffer sb = new StringBuffer();
        sb.append("<oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\""
                + getSchemaLocation()
                + "\">\n");
        
        sb.append(getElements(table, dcTitleLabel, "dc:title"));
        sb.append(getElements(table, dcCreatorLabel, "dc:creator"));
        sb.append(getElements(table, dcSubjectLabel, "dc:subject"));
        sb.append(getElements(table, dcDescriptionLabel, "dc:description"));
        sb.append(getElements(table, dcPublisherLabel, "dc:publisher"));
        sb.append(getElements(table, dcContributorLabel, "dc:contributor"));
        sb.append(getElements(table, dcDateLabel, "dc:date"));
        sb.append(getElements(table, dcTypeLabel, "dc:type"));
        sb.append(getElements(table, dcFormatLabel, "dc:format"));
        sb.append(getElements(table, dcIdentifierLabel, "dc:identifier"));
        sb.append(getElements(table, dcSourceLabel, "dc:source"));
        sb.append(getElements(table, dcLanguageLabel, "dc:language"));
        sb.append(getElements(table, dcRelationLabel, "dc:relation"));
        sb.append(getElements(table, dcCoverageLabel, "dc:coverage"));
        sb.append(getElements(table, dcRightsLabel, "dc:rights"));
        
        sb.append("</oai_dc:dc>\n");
        return sb.toString();
    }
    
    private String getElements(HashMap table, String jdbcLabel, String elementLabel) {
        StringBuffer sb = new StringBuffer();
        Object jdbcObject;
        if (jdbcLabel != null
                && (jdbcObject = table.get(jdbcLabel)) != null
                && jdbcObject.toString().length() > 0) {
            if (separator != null && separator.length() > 0) {
                String[] values = jdbcObject.toString().split(separator);
                for (int i=0; i<values.length; ++i) {
                    sb.append("<").append(elementLabel).append(">");
                    sb.append(OAIUtil.xmlEncode(values[i]));
                    sb.append("</").append(elementLabel).append(">\n");
                }
            } else {
                sb.append("<").append(elementLabel).append(">");
                sb.append(OAIUtil.xmlEncode(jdbcObject.toString()));
                sb.append("</").append(elementLabel).append(">\n");
            }
        }
        return sb.toString();
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.StringWriter;
import java.util.HashMap;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Element;

// import gov.loc.www.zing.srw.StringOrXmlFragment;
// import org.apache.axis.message.MessageElement;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class NodePassThruCrosswalk extends Crosswalk {
//     private static Logger logger = Logger.getLogger(NodePassThruCrosswalk.class);
    private static Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     * @param crosswalkItem 
     */
    public NodePassThruCrosswalk(CrosswalkItem crosswalkItem) {
	super(crosswalkItem.getMetadataNamespace() + " " + crosswalkItem.getSchema());
// 	BasicConfigurator.configure();
	TransformerFactory transformerFactory = TransformerFactory.newInstance();
	try {
	    transformer = transformerFactory.newTransformer();
	    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
	    transformer.setOutputProperty(OutputKeys.STANDALONE, "no");
	    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
	} catch (Exception e) {
	    e.printStackTrace();
// 	    logger.fatal("failed to create transformer", e);
	}
    }

    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
	return true;
    }

    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the XML to be stored within the <metadata> element.
     */
    public String createMetadata(Object nativeItem) {
	try {
	    HashMap hashMap = (HashMap)nativeItem;
	    Element dataNode = (Element)hashMap.get("metadata");
	    DOMSource source = new DOMSource(dataNode);
	    StringWriter sw = new StringWriter();
	    StreamResult result = new StreamResult(sw);
	    synchronized (transformer) {
		transformer.transform(source, result);
	    }
	    return sw.toString();
	} catch (Exception e) {
// 	    logger.warn("NodePassThruCrosswalk.createMetadata failed", e);
	    e.printStackTrace();
	    return e.getMessage();
	}
// 	MessageElement[] messageElement = stringOrXmlFragment.get_any();
// 	StringBuffer sb = new StringBuffer();
// 	for (int i=0; i<messageElement.length; ++i) {
// 	    sb.append(messageElement[i].toString());
// 	}
// 	return sb.toString();
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.util.Properties;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XML2oai_dc extends Crosswalk {
    private static final String elementName = "oai_dc:dc";
    private static final String elementStart = "<" + elementName;
    private static final String elementEnd = elementName + ">";
	
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XML2oai_dc(Properties properties) {
	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
    }

    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
	String fullItem = (String)nativeItem;
	if ((fullItem.indexOf(elementStart)) >= 0)
	    return true;
	return false;
    }

    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the XML to be stored within the <metadata> element.
     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
     */
    public String createMetadata(Object nativeItem)
	throws CannotDisseminateFormatException {
	String fullItem = (String)nativeItem;

	int startOffset = fullItem.indexOf(elementStart);
	if (startOffset == -1) {
	    throw new CannotDisseminateFormatException(getSchemaLocation());
	}
	int endOffset = fullItem.indexOf(elementEnd) + elementEnd.length();
	return fullItem.substring(startOffset, endOffset);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.util.Properties;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;

/**
 * Convert native "item" to oai_etdms. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XML2oai_etdms extends Crosswalk {
    private static final String elementName = "oai_etdms:thesis";
    private static final String elementStart = "<" + elementName;
    private static final String elementEnd = elementName + ">";
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XML2oai_etdms(Properties properties) {
	super("http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd");
    }

    /**
     * Can this nativeItem be represented in ETDMS format?
     * @param nativeItem a record in native format
     * @return true if ETDMS format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
	String fullItem = (String)nativeItem;
	if (fullItem.indexOf(elementStart) > 0) {
	    return true;
	}
	return false;
    }
    
    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the XML to be stored within the <metadata> element.
     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
     */
    public String createMetadata(Object nativeItem)
	throws CannotDisseminateFormatException {
	String fullItem = (String)nativeItem;
	int startOffset = fullItem.indexOf(elementStart);
	if (startOffset == -1) {
	    throw new CannotDisseminateFormatException(getSchemaLocation());
	}
	int endOffset = fullItem.indexOf(elementEnd) + elementEnd.length();
	return fullItem.substring(startOffset, endOffset);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Properties;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;

import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.util.OAIUtil;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTCrosswalk extends Crosswalk {
    private boolean debug = false;
    protected Transformer transformer = null;

    public XSLTCrosswalk(Properties properties)
        throws OAIInternalServerError {
        this(properties, properties.getProperty("XSLTCrosswalk.schemaLocation"), (String)null);
    }

    public XSLTCrosswalk(Properties properties, String schemaLocation, String contentType)
        throws OAIInternalServerError {
        this(properties, schemaLocation, contentType, (String)null);
    }
    public XSLTCrosswalk(Properties properties, String schemaLocation, String contentType, String docType)
        throws OAIInternalServerError {
        this(properties, schemaLocation, contentType, docType, (String)null);
    }
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTCrosswalk(Properties properties, String schemaLocation, String contentType, String docType, String encoding)
        throws OAIInternalServerError {
// 	super("http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
        super(schemaLocation, contentType, docType, encoding);
        if ("true".equals(properties.getProperty("XSLTCrosswalk.debug"))) debug = true;
        try {
            String xsltName = properties.getProperty("XSLTCrosswalk.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
		this.transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		this.transformer.setOutputProperty(OutputKeys.STANDALONE, "no");
		this.transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }

    /**
     * Can this nativeItem be represented in DC format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public boolean isAvailableFor(Object nativeItem) {
        return true;
    }

    /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the FileMap to be stored within the <metadata> element.
     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
     */
    public String createMetadata(Object nativeItem)
	throws CannotDisseminateFormatException {
        try {
            String xmlRec = null;
            if (nativeItem instanceof HashMap) {
                HashMap recordMap = (HashMap)nativeItem;
                xmlRec = (String)recordMap.get("recordString");
                if (xmlRec == null) {
                    xmlRec = new String((byte[])recordMap.get("recordBytes"), "UTF-8");
                }
                xmlRec = xmlRec.trim();
            } else if (nativeItem instanceof String) {
                xmlRec = (String)nativeItem;
            } else if (nativeItem instanceof Document) {
                xmlRec = OAIUtil.toString((Document)nativeItem);
            } else {
                throw new Exception("Unrecognized nativeItem");
            }
        
            if (debug) {
                System.out.println("XSLTCrosswalk.createMetadata: xmlRec=" + xmlRec);
            }
            if (xmlRec.startsWith("<?")) {
                int offset = xmlRec.indexOf("?>");
                xmlRec = xmlRec.substring(offset+2);
            }
            if (debug) {
                System.out.println("XSLTCrosswalk.createMetadata: transformer=" + transformer);
            }
            if (transformer != null) {
                StringReader stringReader = new StringReader(xmlRec);
                StreamSource streamSource = new StreamSource(stringReader);
                StringWriter stringWriter = new StringWriter();
                synchronized (this) {
                    transformer.transform(streamSource, new StreamResult(stringWriter));
                }
                if (debug) {
                    System.out.println("XSLTCrosswalk.createMetadata: return=" + stringWriter.toString());
                }
                return stringWriter.toString();
            } else {
                return xmlRec;
            }
        } catch (Exception e) {
            if (debug) {
                e.printStackTrace();
            }
            throw new CannotDisseminateFormatException(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to marc21. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTmarc21Crosswalk extends XSLTCrosswalk {
    private boolean debug = false;
//     private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTmarc21Crosswalk(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd", null);
        String temp = properties.getProperty("XSLTmarc21Crosswalk.debug");
        if ("true".equals(temp)) debug = true;
        try {
            String xsltName = properties.getProperty("XSLTmarc21Crosswalk.xsltName");
            if (debug) System.out.println("XSLTmarc21Crosswalk.xsltName=" + xsltName);
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to mtx. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTmtxCrosswalk extends XSLTCrosswalk {
//     private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTmtxCrosswalk(Properties properties)
        throws OAIInternalServerError {
  	super(properties, "http://www.w3.org/1999/xhtml http://www.w3.org/2002/08/xhtml/xhtml1-transitional.xsd", "text/html; charset=UTF-8", "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
        try {
            String xsltName = properties.getProperty("XSLTmtxCrosswalk.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
                System.out.println("XSLTmtxCrosswalk.XSLTmtxCrosswalk: transformer="
                                   + this.transformer);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to oai_dc. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLToai_dcCrosswalk extends XSLTCrosswalk {
    private boolean debug=false;
//     private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLToai_dcCrosswalk(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd", (String)null);
        try {
            String xsltName = properties.getProperty("XSLToai_dcCrosswalk.xsltName");
            if (debug) System.out.println("XSLToai_dcCrosswalk.XSLToai_dcCrosswalk: xsltName="
                                          + xsltName);
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to pro. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTpro2004Crosswalk extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTpro2004Crosswalk(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "info:ofi/pro-2004 http://www.openurl.info/registry/docs/xsd/info:ofi/fmt:xml:xsd:pro-2004", null);
        try {
            String xsltName = properties.getProperty("XSLTproCrosswalk.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to pro. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTproCrosswalk extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTproCrosswalk(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "info:ofi/pro http://www.openurl.info/registry/docs/xsd/info:ofi/fmt:xml:xsd:pro", null);
        try {
            String xsltName = properties.getProperty("XSLTproCrosswalk.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to xsd. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTxsdCrosswalk extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTxsdCrosswalk(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd", null);
        try {
            String xsltName = properties.getProperty("XSLTxsdCrosswalk.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to xsl. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTxslCrosswalk extends XSLTCrosswalk {
//    private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTxslCrosswalk(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.w3.org/1999/XSL/Transform http://www.w3.org/1999/XSL/Transform.xsd", null);
        try {
            String xsltName = properties.getProperty("XSLTxslCrosswalk.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to z39_19. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLTz39_19Crosswalk extends XSLTCrosswalk {
//     private Transformer transformer = null;
    
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLTz39_19Crosswalk(Properties properties)
        throws OAIInternalServerError {
 	super(properties, "http://www.oclc.org/z39_19 http://www.oclc.org/z39_19.xsd", null);
        try {
            String xsltName = properties.getProperty("XSLTz39_19Crosswalk.xsltName");
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                TransformerFactory tFactory = TransformerFactory.newInstance();
                this.transformer = tFactory.newTransformer(xslSource);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI badArgumentException
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class BadArgumentException extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public BadArgumentException() {
	super("<error code=\"badArgument\">The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax.</error>");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI badResumptionTokenException
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class BadResumptionTokenException extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public BadResumptionTokenException() {
	super("<error code=\"badResumptionToken\">The value of the resumptionToken argument is invalid or expired</error>");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.util.Date;
import java.util.HashMap;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

/**
 * This class represents an BadVerb response on either the server or
 * on the client
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class BadVerb extends ServerVerb {
    /**
     * Construct the xml response on the server side.
     *
     * @param context the servlet context
     * @param request the servlet request
     * @param response 
     * @param serverTransformer 
     * @return a String containing the xml response
     * @throws TransformerException 
     */
    public static String construct(HashMap context,
                                   HttpServletRequest request, HttpServletResponse response,
                                   Transformer serverTransformer)
        throws TransformerException {
        Properties properties =
	    (Properties)context.get("OAIHandler.properties");
        StringBuffer sb = new StringBuffer();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
	if (styleSheet != null) {
	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
	    sb.append(styleSheet);
	    sb.append("\"?>");
	}
        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
	sb.append("<responseDate>");
	sb.append(createResponseDate(new Date()));
	sb.append("</responseDate>");
// 	sb.append("<requestURL>");
//         sb.append(getRequestURL(request));
// 	sb.append("</requestURL>");
	sb.append("<request>");
	try {
	    sb.append(request.getRequestURL().toString());
	} catch (java.lang.NoSuchMethodError e) {
	    sb.append(request.getRequestURL().toString());
//	    sb.append(HttpUtils.getRequestURL(request).toString());
	}
	sb.append("</request>");
	sb.append("<error code=\"badVerb\">Illegal verb</error>");
        sb.append("</OAI-PMH>");
	return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI cannotDisseminateFormatException
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class CannotDisseminateFormatException extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public CannotDisseminateFormatException(String metadataPrefix) {
	super("<error code=\"cannotDisseminateFormat\">\"" + metadataPrefix + "\" is not supported by the item or by the repository</error>");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.crosswalk.Crosswalks;

/**
 * This class represents a GetRecord response on either the server or
 * the client.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class GetRecord extends ServerVerb {
    private static final boolean debug = false;
    private static ArrayList validParamNames = new ArrayList();
    static {
	validParamNames.add("verb");
	validParamNames.add("identifier");
	validParamNames.add("metadataPrefix");
    }

    /**
     * Construct the xml response on the server-side.
     *
     * @param context the servlet context
     * @param request the servlet request
     * @return a String containing the XML response
     * @exception OAIBadRequestException an http 400 status error occurred
     * @exception OAINotFoundException an http 404 status error occurred
     * @exception OAIInternalServerError an http 500 status error occurred
     */
    public static String construct(HashMap context,
                                   HttpServletRequest request,
                                   HttpServletResponse response,
                                   Transformer serverTransformer)
        throws OAIInternalServerError, TransformerException {
        Properties properties = (Properties)context.get("OAIHandler.properties");
	AbstractCatalog abstractCatalog =
	    (AbstractCatalog)context.get("OAIHandler.catalog");
	String baseURL = properties.getProperty("OAIHandler.baseURL");
	if (baseURL == null) {
	    try {
		baseURL = request.getRequestURL().toString();
	    } catch (java.lang.NoSuchMethodError f) {
		baseURL = request.getRequestURL().toString();
	    }
	}
        StringBuffer sb = new StringBuffer();
        String identifier = request.getParameter("identifier");
        String metadataPrefix = request.getParameter("metadataPrefix");

        if (debug) {
            System.out.println("GetRecord.constructGetRecord: identifier=" +
                               identifier);
            System.out.println("GetRecord.constructGetRecord: metadataPrefix="
                               + metadataPrefix);
        }
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
	if (styleSheet != null) {
	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
	    sb.append(styleSheet);
	    sb.append("\"?>");
	}
        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
	String extraXmlns = properties.getProperty("OAIHandler.extraXmlns");
	if (extraXmlns != null)
	    sb.append(" ").append(extraXmlns);
        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
        sb.append("<responseDate>");
        sb.append(createResponseDate(new Date()));
        sb.append("</responseDate>");
//         sb.append("<requestURL>");
//         sb.append(getRequestURL(request));
//         sb.append("</requestURL>");
        Crosswalks crosswalks = abstractCatalog.getCrosswalks();
	try {
	    if (metadataPrefix == null || metadataPrefix.length() == 0
		|| identifier == null || identifier.length() == 0
		|| hasBadArguments(request, validParamNames.iterator(), validParamNames, abstractCatalog)) {
		throw new BadArgumentException();
	    }
	    else if (!crosswalks.containsValue(metadataPrefix)) {
		throw new CannotDisseminateFormatException(metadataPrefix);
	    } else {
		String record = abstractCatalog.getRecord(identifier, metadataPrefix);
		if (record != null) {
		    sb.append(getRequestElement(request, validParamNames, baseURL));
		    sb.append("<GetRecord>");
		    sb.append(record);
		    sb.append("</GetRecord>");
		} else {
		    throw new IdDoesNotExistException(identifier);
		}
	    }
	} catch (BadArgumentException e) {
	    sb.append("<request verb=\"GetRecord\">");
	    sb.append(baseURL);
	    sb.append("</request>");
	    sb.append(e.getMessage());
	} catch (CannotDisseminateFormatException e) {
	    sb.append(getRequestElement(request, validParamNames, baseURL));
	    sb.append(e.getMessage());
	} catch (IdDoesNotExistException e) {
	    sb.append(getRequestElement(request, validParamNames, baseURL));
	    sb.append(e.getMessage());
	}
        sb.append("</OAI-PMH>");
        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI idDoesNotExist Exception
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class IdDoesNotExistException extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public IdDoesNotExistException(String identifier) {
	super("<error code=\"idDoesNotExist\">\"" + identifier + "\" is unknown or illegal in this repository</error>");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
//import org.xml.sax.SAXException;

/**
 * This class represents an Identify response on either the server or
 * on the client
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class Identify extends ServerVerb {
    private static ArrayList validParamNames = new ArrayList();
    static {
        validParamNames.add("verb");
    }

    /**
     * Construct the xml response on the server side.
     *
     * @param context the servlet context
     * @param request the servlet request
     * @return a String containing the xml response
     */
    public static String construct(HashMap context,
            HttpServletRequest request,
            HttpServletResponse response,
            Transformer serverTransformer)
    throws TransformerException {
        String version = (String)context.get("OAIHandler.version");
        AbstractCatalog abstractCatalog =
            (AbstractCatalog)context.get("OAIHandler.catalog");
        Properties properties =
            (Properties)context.get("OAIHandler.properties");
        String baseURL = properties.getProperty("OAIHandler.baseURL");
        if (baseURL == null) {
            try {
                baseURL = request.getRequestURL().toString();
            } catch (java.lang.NoSuchMethodError f) {
                baseURL = request.getRequestURL().toString();
            }
        }
        StringBuffer sb = new StringBuffer();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
        String styleSheet = properties.getProperty("OAIHandler.styleSheet");
        if (styleSheet != null) {
            sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
            sb.append(styleSheet);
            sb.append("\"?>");
        }
        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
        sb.append("<responseDate>");
        sb.append(createResponseDate(new Date()));
        sb.append("</responseDate>");
//      sb.append("<requestURL>");
//      sb.append(getRequestURL(request));
//      sb.append("</requestURL>");
        sb.append(getRequestElement(request, validParamNames, baseURL));
        if (hasBadArguments(request, validParamNames.iterator(), validParamNames, abstractCatalog)) {
            sb.append(new BadArgumentException().getMessage());
        } else {
            sb.append("<Identify>");
            sb.append("<repositoryName>");
            sb.append(properties.getProperty("Identify.repositoryName",
            "undefined"));
            sb.append("</repositoryName>");
            sb.append("<baseURL>");
            sb.append(baseURL);
            sb.append("</baseURL>");
            sb.append("<protocolVersion>2.0</protocolVersion>");
            sb.append("<adminEmail>");
            sb.append(properties.getProperty("Identify.adminEmail", "undefined"));
            sb.append("</adminEmail>");
            sb.append("<earliestDatestamp>");
            sb.append(properties.getProperty("Identify.earliestDatestamp", "undefined"));
            sb.append("</earliestDatestamp>");
            sb.append("<deletedRecord>");
            sb.append(properties.getProperty("Identify.deletedRecord", "undefined"));
            sb.append("</deletedRecord>");
            String granularity = properties.getProperty("AbstractCatalog.granularity");
            if (granularity != null) {
                sb.append("<granularity>");
                sb.append(granularity);
                sb.append("</granularity>");
            }
            // 	String compression = properties.getProperty("Identify.compression");
            // 	if (compression != null) {
            sb.append("<compression>gzip</compression>");
//          sb.append("<compression>compress</compression>");
            sb.append("<compression>deflate</compression>");
            // 	}
            String repositoryIdentifier = properties.getProperty("Identify.repositoryIdentifier");
            String sampleIdentifier = properties.getProperty("Identify.sampleIdentifier");
            if (repositoryIdentifier != null && sampleIdentifier != null) {
                sb.append("<description>");
                sb.append("<oai-identifier xmlns=\"http://www.openarchives.org/OAI/2.0/oai-identifier\"");
                sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
                sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai-identifier http://www.openarchives.org/OAI/2.0/oai-identifier.xsd\">");
                sb.append("<scheme>oai</scheme>");
                sb.append("<repositoryIdentifier>");
                sb.append(repositoryIdentifier);
                sb.append("</repositoryIdentifier>");
                sb.append("<delimiter>:</delimiter>");
                sb.append("<sampleIdentifier>");
                sb.append(sampleIdentifier);
                sb.append("</sampleIdentifier>");
                sb.append("</oai-identifier>");
                sb.append("</description>");
            }
            String propertyPrefix = "Identify.description";
            Enumeration propNames = properties.propertyNames();
            while (propNames.hasMoreElements()) {
                String propertyName = (String)propNames.nextElement();
                if (propertyName.startsWith(propertyPrefix)) {
                    sb.append((String)properties.get(propertyName));
                    sb.append("\n");
                }
            }
            sb.append("<description><toolkit xsi:schemaLocation=\"http://oai.dlib.vt.edu/OAI/metadata/toolkit http://alcme.oclc.org/oaicat/toolkit.xsd\" xmlns=\"http://oai.dlib.vt.edu/OAI/metadata/toolkit\"><title>OCLC's OAICat Repository Framework</title><author><name>Jeffrey A. Young</name><email>jyoung@oclc.org</email><institution>OCLC</institution></author><version>");
            sb.append(version);
            sb.append("</version><toolkitIcon>http://alcme.oclc.org/oaicat/oaicat_icon.gif</toolkitIcon><URL>http://www.oclc.org/research/software/oai/cat.shtm</URL></toolkit></description>");
            String descriptions = abstractCatalog.getDescriptions();
            if (descriptions != null) {
                sb.append(descriptions);
            }
            sb.append("</Identify>");
        }
        sb.append("</OAI-PMH>");
        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.crosswalk.Crosswalks;

/**
 * This class represents a ListIdentifiers verb on either the server or
 * on the client side.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class ListIdentifiers extends ServerVerb {
    private static final boolean debug = false;
    private static ArrayList validParamNames1 = new ArrayList();
    static {
	validParamNames1.add("verb");
	validParamNames1.add("from");
	validParamNames1.add("until");
	validParamNames1.add("set");
	validParamNames1.add("metadataPrefix");
    }
    private static ArrayList validParamNames2 = new ArrayList();
    static {
	validParamNames2.add("verb");
	validParamNames2.add("resumptionToken");
    }
    private static ArrayList requiredParamNames1 = new ArrayList();
    static {
	requiredParamNames1.add("verb");
	requiredParamNames1.add("metadataPrefix");
    }
    private static ArrayList requiredParamNames2 = new ArrayList();
    static {
	requiredParamNames2.add("verb");
	requiredParamNames2.add("resumptionToken");
    }

    public static String construct(HashMap context,
                                   HttpServletRequest request,
                                   HttpServletResponse response,
                                   Transformer serverTransformer)
	throws OAIInternalServerError, TransformerException {
        Properties properties =
	    (Properties)context.get("OAIHandler.properties");
	AbstractCatalog abstractCatalog =
	    (AbstractCatalog)context.get("OAIHandler.catalog");
	boolean xmlEncodeSetSpec = "true".equalsIgnoreCase(properties.getProperty("OAIHandler.xmlEncodeSetSpec"));
	boolean urlEncodeSetSpec = !"false".equalsIgnoreCase(properties.getProperty("OAIHandler.urlEncodeSetSpec"));
	String baseURL = properties.getProperty("OAIHandler.baseURL");
	if (baseURL == null) {
	    try {
		baseURL = request.getRequestURL().toString();
	    } catch (java.lang.NoSuchMethodError f) {
		baseURL = request.getRequestURL().toString();
	    }
	}
	StringBuffer sb = new StringBuffer();
	String oldResumptionToken = request.getParameter("resumptionToken");
	String metadataPrefix = request.getParameter("metadataPrefix");

	if (metadataPrefix != null && metadataPrefix.length() == 0)
	    metadataPrefix = null;

	sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
	if (styleSheet != null) {
	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
	    sb.append(styleSheet);
	    sb.append("\"?>");
	}
	sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
	sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
	sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
	sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
	sb.append("<responseDate>");
	sb.append(createResponseDate(new Date()));
	sb.append("</responseDate>");
// 	sb.append("<requestURL>");
// 	sb.append(getRequestURL(request));
// 	sb.append("</requestURL>");

	if (!abstractCatalog.isHarvestable()) {
	    sb.append("<request verb=\"ListIdentifiers\">");
	    sb.append(baseURL);
	    sb.append("</request>");
	    sb.append("<error code=\"badArgument\">Database is unavailable for harvesting</error>");
	} else {
	    ArrayList validParamNames = null;
	    ArrayList requiredParamNames = null;
	    Map listIdentifiersMap = null;
	    if (oldResumptionToken == null) {
		validParamNames = validParamNames1;
		requiredParamNames = requiredParamNames1;
		String from = request.getParameter("from");
		String until = request.getParameter("until");
		try {
		    if (from != null && from.length() > 0 && from.length() < 10) {
			throw new BadArgumentException();
		    }
		    if (until != null && until.length() > 0 && until.length() < 10) {
			throw new BadArgumentException();
		    }
		    if (from != null && until != null && from.length() != until.length()) {
			throw new BadArgumentException();
		    }
		    if (from == null || from.length() == 0) {
			from = "0001-01-01";
		    }
		    if (until == null || until.length() == 0) {
			until = "9999-12-31";
		    }
		    from = abstractCatalog.toFinestFrom(from);
		    until = abstractCatalog.toFinestUntil(until);
		    if (from.compareTo(until) > 0)
			throw new BadArgumentException();
		    String set = request.getParameter("set");
                    if (set != null) {
                        if (set.length() == 0) set = null;
                        else if (urlEncodeSetSpec) set = set.replace(' ', '+');
                    }
		    Crosswalks crosswalks = abstractCatalog.getCrosswalks();
		    if (metadataPrefix == null) {
			throw new BadArgumentException();
		    }

		    if (!crosswalks.containsValue(metadataPrefix)) {
			throw new CannotDisseminateFormatException(metadataPrefix);
		    } else {
			listIdentifiersMap = abstractCatalog.listIdentifiers(from, until, set,
									     metadataPrefix);
		    }
		} catch (NoItemsMatchException e) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(e.getMessage());
		} catch (BadArgumentException e) {
		    sb.append("<request verb=\"ListIdentifiers\">");
// 		    sb.append(HttpUtils.getRequestURL(request));
		    sb.append(baseURL);
		    sb.append("</request>");
		    sb.append(e.getMessage());
// 		} catch (BadGranularityException e) {
// 		    sb.append(getRequestElement(request));
// 		    sb.append(e.getMessage());
		} catch (CannotDisseminateFormatException e) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(e.getMessage());
		} catch (NoSetHierarchyException e) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(e.getMessage());
		}
	    } else {
		validParamNames = validParamNames2;
		requiredParamNames = requiredParamNames2;
		if (hasBadArguments(request, requiredParamNames.iterator(), validParamNames, abstractCatalog)) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(new BadArgumentException().getMessage());
		} else {
		    try {
			listIdentifiersMap = abstractCatalog.listIdentifiers(oldResumptionToken);
		    } catch (BadResumptionTokenException e) {
			sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
			sb.append(e.getMessage());
		    }
		}
	    }

	    if (listIdentifiersMap != null) {
		sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		if (hasBadArguments(request, requiredParamNames.iterator(),
				    validParamNames, abstractCatalog)) {
		    sb.append(new BadArgumentException().getMessage());
		} else {
		    sb.append("<ListIdentifiers>");
		    Iterator identifiers = (Iterator)listIdentifiersMap.get("headers");
		    while (identifiers.hasNext()) {
			sb.append((String)identifiers.next());
		    }

		    Map newResumptionMap = (Map)listIdentifiersMap.get("resumptionMap");
		    if (newResumptionMap != null) {
			String newResumptionToken = (String)newResumptionMap.get("resumptionToken");
			String expirationDate = (String)newResumptionMap.get("expirationDate");
			String completeListSize = (String)newResumptionMap.get("completeListSize");
			String cursor = (String)newResumptionMap.get("cursor");
			sb.append("<resumptionToken");
			if (expirationDate != null) {
			    sb.append(" expirationDate=\"");
			    sb.append(expirationDate);
			    sb.append("\"");
			}
			if (completeListSize != null) {
			    sb.append(" completeListSize=\"");
			    sb.append(completeListSize);
			    sb.append("\"");
			}
			if (cursor != null) {
			    sb.append(" cursor=\"");
			    sb.append(cursor);
			    sb.append("\"");
			}
			sb.append(">");
			sb.append(newResumptionToken);
			sb.append("</resumptionToken>");
		    } else if (oldResumptionToken != null) {
			sb.append("<resumptionToken />");
		    }
		    sb.append("</ListIdentifiers>");
		}
	    }
	}
	sb.append("</OAI-PMH>");
	if (debug) {
	    System.out.println("ListIdentifiers.constructListIdentifiers: returning: "
			       + sb.toString());
	}
        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.crosswalk.Crosswalk;
import ORG.oclc.oai.server.crosswalk.CrosswalkItem;
import ORG.oclc.oai.server.crosswalk.Crosswalks;

/**
 * This class represents a ListMetadataFormats verb on either
 * the client or on the server.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class ListMetadataFormats extends ServerVerb {
    private static ArrayList validParamNames = new ArrayList();
    static {
        validParamNames.add("verb");
        validParamNames.add("identifier");
    }
    private static ArrayList requiredParamNames = new ArrayList();
    static {
        requiredParamNames.add("verb");
    }

    /**
     * Server-side construction of the xml response
     *
     * @param context the servlet context
     * @param request the servlet request
     * @exception OAIBadRequestException an http 400 status code problem
     * @exception OAINotFoundException an http 404 status code problem
     * @exception OAIInternalServerError an http 500 status code problem
     */
    public static String construct(HashMap context,
            HttpServletRequest request,
            HttpServletResponse response,
            Transformer serverTransformer)
    throws OAIInternalServerError, TransformerException {
        Properties properties = (Properties)context.get("OAIHandler.properties");
        AbstractCatalog abstractCatalog =
            (AbstractCatalog)context.get("OAIHandler.catalog");
        String baseURL = properties.getProperty("OAIHandler.baseURL");
        if (baseURL == null) {
            try {
                baseURL = request.getRequestURL().toString();
            } catch (java.lang.NoSuchMethodError f) {
                baseURL = request.getRequestURL().toString();
            }
        }
        StringBuffer sb = new StringBuffer();
        String identifier = request.getParameter("identifier");
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
        String styleSheet = properties.getProperty("OAIHandler.styleSheet");
        if (styleSheet != null) {
            sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
            sb.append(styleSheet);
            sb.append("\"?>");
        }
        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
        sb.append("<responseDate>");
        sb.append(createResponseDate(new Date()));
        sb.append("</responseDate>");
//      sb.append("<requestURL>");
//      sb.append(getRequestURL(request));
//      sb.append("</requestURL>");
        sb.append(getRequestElement(request, validParamNames, baseURL));
        if (hasBadArguments(request, requiredParamNames.iterator(),
                validParamNames, abstractCatalog)) {
            sb.append(new BadArgumentException().getMessage());
        } else {
            Crosswalks crosswalks = abstractCatalog.getCrosswalks();
            if (identifier == null || identifier.length() == 0) {
                Iterator iterator = crosswalks.iterator();
                sb.append("<ListMetadataFormats>");
                while (iterator.hasNext()) {
                    Map.Entry entry = (Map.Entry)iterator.next();
                    String oaiSchemaLabel = (String)entry.getKey();
                    CrosswalkItem crosswalkItem = (CrosswalkItem)entry.getValue();
                    Crosswalk crosswalk = crosswalkItem.getCrosswalk();
//                  StringTokenizer tokenizer = new StringTokenizer(crosswalk.getSchemaLocation());
//                  String namespaceURI = tokenizer.nextToken();
//                  String schemaURL = tokenizer.nextToken();
                    String[] tokenizer = split(crosswalk.getSchemaLocation());
                    String namespaceURI = null;
                    String schemaURL = null;
                    if (tokenizer.length == 1) {
                        schemaURL = tokenizer[0];
                    } else if (tokenizer.length > 1) {
                        namespaceURI = tokenizer[0];
                        schemaURL = tokenizer[1];
                    }
                    sb.append("<metadataFormat>");
                    sb.append("<metadataPrefix>");
                    sb.append(oaiSchemaLabel);
                    sb.append("</metadataPrefix>");
                    sb.append("<schema>");
                    sb.append(schemaURL);
                    sb.append("</schema>");
                    sb.append("<metadataNamespace>");
                    if (namespaceURI != null) {
                    	sb.append(namespaceURI);
                    }
                    sb.append("</metadataNamespace>");
                    sb.append("</metadataFormat>");
                }
                sb.append("</ListMetadataFormats>");
            } else {
                try {
                    Vector schemaLocations = abstractCatalog.getSchemaLocations(identifier);
                    sb.append("<ListMetadataFormats>");
                    for (int i=0; i<schemaLocations.size(); ++i) {
                        String schemaLocation = (String)schemaLocations.elementAt(i);
//                      StringTokenizer tokenizer = new StringTokenizer(schemaLocation);
//                      String namespaceURI = tokenizer.nextToken();
//                      String schemaURL = tokenizer.nextToken();
                        String[] tokenizer = split(schemaLocation);
                        String namespaceURI = null;
                        String schemaURL = null;
                        if (tokenizer.length == 1) {
                            schemaURL = tokenizer[0];
                        } else if (tokenizer.length > 1) {
                            namespaceURI = tokenizer[0];
                            schemaURL = tokenizer[1];
                        }
                        sb.append("<metadataFormat>");
                        sb.append("<metadataPrefix>");
                        // make sure it's a space that separates them
                        sb.append(crosswalks.getMetadataPrefix(namespaceURI, schemaURL));
                        sb.append("</metadataPrefix>");
                        sb.append("<schema>");
                        sb.append(schemaURL);
                        sb.append("</schema>");
                        sb.append("<metadataNamespace>");
                        if (namespaceURI != null) {
                        	sb.append(namespaceURI);
                        }
                        sb.append("</metadataNamespace>");
                        sb.append("</metadataFormat>");
                    }
                    sb.append("</ListMetadataFormats>");
                } catch (IdDoesNotExistException e) {
                    sb.append(e.getMessage());
                } catch (NoMetadataFormatsException e) {
                    sb.append(e.getMessage());
                }
            }
        }
        sb.append("</OAI-PMH>");
        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
    }

    private static String[] split(String s) {
        StringTokenizer tokenizer = new StringTokenizer(s);
        String[] tokens = new String[tokenizer.countTokens()];
        for (int i=0; i<tokens.length; ++i) {
            tokens[i] = tokenizer.nextToken();
        }
        return tokens;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.crosswalk.Crosswalks;

/**
 * Represents an OAI ListRecords Verb response. This class is used on both
 * the client-side and on the server-side to represent a ListRecords response
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class ListRecords extends ServerVerb {
    private static final boolean debug = false;
    private static ArrayList validParamNames1 = new ArrayList();
    static {
	validParamNames1.add("verb");
	validParamNames1.add("from");
	validParamNames1.add("until");
	validParamNames1.add("set");
	validParamNames1.add("metadataPrefix");
    }
    private static ArrayList validParamNames2 = new ArrayList();
    static {
	validParamNames2.add("verb");
	validParamNames2.add("resumptionToken");
    }
    private static ArrayList requiredParamNames1 = new ArrayList();
    static {
	requiredParamNames1.add("verb");
	requiredParamNames1.add("metadataPrefix");
    }
    private static ArrayList requiredParamNames2 = new ArrayList();
    static {
	requiredParamNames2.add("verb");
	requiredParamNames2.add("resumptionToken");
    }

    /**
     * Server-side method to construct an xml response to a ListRecords verb.
     */
    public static String construct(HashMap context,
                                   HttpServletRequest request, HttpServletResponse response,
                                   Transformer serverTransformer)
        throws OAIInternalServerError, TransformerException {
        if (debug) System.out.println("ListRecords.construct: entered");

        Properties properties = (Properties)context.get("OAIHandler.properties");
        AbstractCatalog abstractCatalog = (AbstractCatalog)context.get("OAIHandler.catalog");
	boolean xmlEncodeSetSpec = "true".equalsIgnoreCase(properties.getProperty("OAIHandler.xmlEncodeSetSpec"));
	boolean urlEncodeSetSpec = !"false".equalsIgnoreCase(properties.getProperty("OAIHandler.urlEncodeSetSpec"));
	String baseURL = properties.getProperty("OAIHandler.baseURL");
	if (baseURL == null) {
	    try {
		baseURL = request.getRequestURL().toString();
	    } catch (java.lang.NoSuchMethodError f) {
		baseURL = request.getRequestURL().toString();
	    }
	}
        StringBuffer sb = new StringBuffer();
        String oldResumptionToken = request.getParameter("resumptionToken");
	String metadataPrefix = request.getParameter("metadataPrefix");

	if (metadataPrefix != null && metadataPrefix.length() == 0)
	    metadataPrefix = null;

        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
	if (styleSheet != null) {
	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
	    sb.append(styleSheet);
	    sb.append("\"?>");
	}
        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
	String extraXmlns = properties.getProperty("OAIHandler.extraXmlns");
	if (extraXmlns != null)
	    sb.append(" ").append(extraXmlns);
        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
        sb.append("<responseDate>");
	sb.append(createResponseDate(new Date()));
	sb.append("</responseDate>");
//         sb.append("<requestURL>");
//         sb.append(getRequestURL(request));
//         sb.append("</requestURL>");

	if (!abstractCatalog.isHarvestable()) {
	    sb.append("<request verb=\"ListRecords\">");
	    sb.append(baseURL);
	    sb.append("</request>");
	    sb.append("<error code=\"badArgument\">Database is unavailable for harvesting</error>");
	} else {
// 	    if (debug) {
// 		System.gc();
// 		System.gc();
// 		Runtime rt = Runtime.getRuntime();
// 		long freeMemoryK = rt.freeMemory() / 1024;
// 		long totalMemoryK = rt.totalMemory() / 1024;
// 		System.out.print("ListRecords.construct: " + oldResumptionToken);
// 		System.out.print(" freeMemory=" + freeMemoryK / 1024.0 + "M");
// 		System.out.print(" of " + totalMemoryK / 1024.0 + "M ");
// 		System.out.println("(" + (100 * freeMemoryK) / totalMemoryK + "%)");
// 	    }

	    Map listRecordsMap = null;

	    ArrayList validParamNames = null;
	    ArrayList requiredParamNames = null;
	    if (oldResumptionToken == null) {
		validParamNames = validParamNames1;
		requiredParamNames = requiredParamNames1;
		String from = request.getParameter("from");
		String until = request.getParameter("until");
		try {
		    if (from != null && from.length() > 0 && from.length() < 10) {
			throw new BadArgumentException();
		    }
		    if (until != null && until.length() > 0 && until.length() < 10) {
			throw new BadArgumentException();
		    }
		    if (from != null && until != null && from.length() != until.length()) {
			throw new BadArgumentException();
		    }
		    if (from == null || from.length() == 0) {
			from = "0001-01-01";
		    }
		    if (until == null || until.length() == 0) {
			until = "9999-12-31";
		    }
		    from = abstractCatalog.toFinestFrom(from);
		    until = abstractCatalog.toFinestUntil(until);
		    if (from.compareTo(until) > 0)
			throw new BadArgumentException();
		    String set = request.getParameter("set");
                    if (set != null) {
                        if (set.length() == 0) set = null;
                        else if (urlEncodeSetSpec) set = set.replace(' ', '+');
                    }
		    Crosswalks crosswalks = abstractCatalog.getCrosswalks();
		    if (metadataPrefix == null) {
			throw new BadArgumentException();
		    }
		    if (!crosswalks.containsValue(metadataPrefix)) {
			throw new CannotDisseminateFormatException(metadataPrefix);
		    } else {
			listRecordsMap = abstractCatalog.listRecords(from, until, set,
								     metadataPrefix);
		    }
		} catch (NoItemsMatchException e) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(e.getMessage());
		} catch (BadArgumentException e) {
		    sb.append("<request verb=\"ListRecords\">");
		    sb.append(baseURL);
		    sb.append("</request>");
		    sb.append(e.getMessage());
// 		} catch (BadGranularityException e) {
// 		    sb.append(getRequestElement(request));
// 		    sb.append(e.getMessage());
		} catch (CannotDisseminateFormatException e) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(e.getMessage());
		} catch (NoSetHierarchyException e) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(e.getMessage());
		}
	    } else {
		validParamNames = validParamNames2;
		requiredParamNames = requiredParamNames2;
		if (hasBadArguments(request, requiredParamNames.iterator(), validParamNames, abstractCatalog)) {
		    sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		    sb.append(new BadArgumentException().getMessage());
		} else {
		    try {
			listRecordsMap = abstractCatalog.listRecords(oldResumptionToken);
		    } catch (BadResumptionTokenException e) {
			sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
			sb.append(e.getMessage());
		    }
		}
	    }
	    if (listRecordsMap != null) {
		sb.append(getRequestElement(request, validParamNames, baseURL, xmlEncodeSetSpec));
		if (hasBadArguments(request, requiredParamNames.iterator(),
				    validParamNames, abstractCatalog)) {
		    sb.append(new BadArgumentException().getMessage());
		} else {
		    sb.append("<ListRecords>\n");
		    Iterator records = (Iterator)listRecordsMap.get("records");
		    while (records.hasNext()) {
			sb.append((String)records.next());
			sb.append("\n");
		    }
		    Map newResumptionMap = (Map)listRecordsMap.get("resumptionMap");
		    if (newResumptionMap != null) {
			String newResumptionToken = (String)newResumptionMap.get("resumptionToken");
			String expirationDate = (String)newResumptionMap.get("expirationDate");
			String completeListSize = (String)newResumptionMap.get("completeListSize");
			String cursor = (String)newResumptionMap.get("cursor");
			sb.append("<resumptionToken");
			if (expirationDate != null) {
			    sb.append(" expirationDate=\"");
			    sb.append(expirationDate);
			    sb.append("\"");
			}
			if (completeListSize != null) {
			    sb.append(" completeListSize=\"");
			    sb.append(completeListSize);
			    sb.append("\"");
			}
			if (cursor != null) {
			    sb.append(" cursor=\"");
			    sb.append(cursor);
			    sb.append("\"");
			}
			sb.append(">");
			sb.append(newResumptionToken);
			sb.append("</resumptionToken>");
		    } else if (oldResumptionToken != null) {
			sb.append("<resumptionToken />");
		    }
		    sb.append("</ListRecords>");
		}
	    }
	}
        sb.append("</OAI-PMH>");
        if (debug) {
            System.out.println("ListRecords.constructListRecords: returning: " + sb.toString());
        }
        return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import ORG.oclc.oai.server.catalog.AbstractCatalog;

/**
 * A ListSets OAI verb representation.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class ListSets extends ServerVerb {
    private static ArrayList validParamNames = new ArrayList();
    static {
	validParamNames.add("verb");
	validParamNames.add("resumptionToken");
    }
    private static ArrayList requiredParamNames = new ArrayList();
    static {
	validParamNames.add("verb");
    }

    /**
     * construct ListSets response
     *
     * @param context the context object from the local OAI server
     * @param request the request object from the local OAI server
     * @exception OAIInternalServerError
     */
    public static String construct(HashMap context, HttpServletRequest request,
                                   HttpServletResponse response, Transformer serverTransformer)
	throws OAIInternalServerError, TransformerException {
        Properties properties =
	    (Properties)context.get("OAIHandler.properties");
	AbstractCatalog abstractCatalog =
	    (AbstractCatalog)context.get("OAIHandler.catalog");
	String baseURL = properties.getProperty("OAIHandler.baseURL");
	if (baseURL == null) {
	    try {
		baseURL = request.getRequestURL().toString();
	    } catch (java.lang.NoSuchMethodError f) {
		baseURL = request.getRequestURL().toString();
	    }
	}
        StringBuffer sb = new StringBuffer();
	String oldResumptionToken = request.getParameter("resumptionToken");
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
	String styleSheet = properties.getProperty("OAIHandler.styleSheet");
	if (styleSheet != null) {
	    sb.append("<?xml-stylesheet type=\"text/xsl\" href=\"");
	    sb.append(styleSheet);
	    sb.append("\"?>");
	}
        sb.append("<OAI-PMH xmlns=\"http://www.openarchives.org/OAI/2.0/\"");
        sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
        sb.append(" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/");
        sb.append(" http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd\">");
        sb.append("<responseDate>");
	sb.append(createResponseDate(new Date()));
	sb.append("</responseDate>");
//         sb.append("<requestURL>");
//         sb.append(getRequestURL(request));
//         sb.append("</requestURL>");
	sb.append(getRequestElement(request, validParamNames, baseURL));
	Map listSetsMap = null;
	if (hasBadArguments(request, requiredParamNames.iterator(),
			    validParamNames, abstractCatalog)) {
	    sb.append(new BadArgumentException().getMessage());
	} else {
	    try {
		if (oldResumptionToken == null) {
		    listSetsMap = abstractCatalog.listSets();
		} else {
		    listSetsMap = abstractCatalog.listSets(oldResumptionToken);
		}
		sb.append("<ListSets>");
		Iterator sets = (Iterator)listSetsMap.get("sets");
		while (sets.hasNext()) {
		    sb.append((String)sets.next());
		}
		Map newResumptionMap = (Map)listSetsMap.get("resumptionMap");
		if (newResumptionMap != null) {
		    String newResumptionToken = (String)newResumptionMap.get("resumptionToken");
		    String expirationDate = (String)newResumptionMap.get("expirationDate");
		    String completeListSize = (String)newResumptionMap.get("completeListSize");
		    String cursor = (String)newResumptionMap.get("cursor");
		    sb.append("<resumptionToken");
		    if (expirationDate != null) {
			sb.append(" expirationDate=\"");
			sb.append(expirationDate);
			sb.append("\"");
		    }
		    if (completeListSize != null) {
			sb.append(" completeListSize=\"");
			sb.append(completeListSize);
			sb.append("\"");
		    }
		    if (cursor != null) {
			sb.append(" cursor=\"");
			sb.append(cursor);
			sb.append("\"");
		    }
		    sb.append(">");
		    sb.append(newResumptionToken);
		    sb.append("</resumptionToken>");
		} else if (oldResumptionToken != null) {
		    sb.append("<resumptionToken />");
		}
		sb.append("</ListSets>");
	    } catch (NoSetHierarchyException e) {
		sb.append(e.getMessage());
	    } catch (BadResumptionTokenException e) {
		sb.append(e.getMessage());
	    }
	}
        sb.append("</OAI-PMH>");
	return render(response, "text/xml; charset=UTF-8", sb.toString(), serverTransformer);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI cannotDisseminateFormatException
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class NoItemsMatchException extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public NoItemsMatchException() {
	super("<error code=\"noRecordsMatch\">The combination of the values of the from, until, set, and metadataPrefix arguments results in an empty list.</error>");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI cannotDisseminateFormatException
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class NoMetadataFormatsException extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public NoMetadataFormatsException() {
	super("<error code=\"noMetadataFormats\">There are no metadata formats available for the specified item.</error>");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI cannotDisseminateFormatException
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class NoRecordsMatchException extends NoItemsMatchException {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * An OAI cannotDisseminateFormatException
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class NoSetHierarchyException extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public NoSetHierarchyException() {
	super("<error code=\"noSetHierarchy\">The repository does not support sets.</error>");
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

/**
 * http 500 internal server error exception
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class OAIInternalServerError extends Exception {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    /**
     * Construct the exception
     *
     * @param s a message
     */
    public OAIInternalServerError(String s) { super(s); }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb;

import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.TimeZone;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.util.OAIUtil;
//import javax.servlet.http.HttpUtils;

/**
 * ServerVerb is the parent class for each of the server-side OAI verbs.
 *
 * @author Jefffrey A. Young, OCLC Online Computer Library Center
 */
public abstract class ServerVerb {
    private static final boolean debug = false;

    private int statusCode = HttpServletResponse.SC_OK; // http status
    private String message = null; // http response message

    /**
     * Complete XML response String
     */
    protected String xmlText = null;

    /**
     * Constructor
     */
    protected ServerVerb() { }

    public static void init(Properties properties) throws Exception {}

    /**
     * initialize the Verb from the specified xml text
     *
     * @param xmlText complete XML response string
     */
    protected void init(String xmlText) {
        if (debug) {
            System.out.println("ServerVerb.init: xmlText=" + xmlText);
        }
        this.xmlText = xmlText;
    }

    /**
     * Server-side verb constructor
     *
     * @param xmlText complete XML response string
     */
    public ServerVerb(String xmlText) {
        init(xmlText);
    }

    /**
     * Retrieve the http status code
     *
     * @return the http status code;
     */
    public int getStatus() { return statusCode; }

    /**
     * Retrieve the http status message
     *
     * @return the http status message;
     */
    public String getMessage() { return message; }

    /**
     * set the http status code and message
     *
     * @param statusCode the http status code
     * @param message the http status message
     */
    protected void setError(int statusCode, String message) {
        this.statusCode = statusCode;
        this.message = message;
    }

    /**
     * Create an OAI response date from the specified date
     *
     * @param date the date to be transformed to an OAI response date
     * @return a String representation of the OAI response Date.
     */
    public static String createResponseDate(Date date) {
        StringBuffer sb = new StringBuffer();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        TimeZone tz = TimeZone.getTimeZone("UTC");
        formatter.setTimeZone(tz);
        sb.append(formatter.format(date));
        return sb.toString();
    }

    protected static String getRequestElement(HttpServletRequest request,
            List validParamNames,
            String baseURL) {
        return getRequestElement(request, validParamNames, baseURL, false);
    }

    protected static String getRequestElement(HttpServletRequest request,
            List validParamNames,
            String baseURL,
            boolean xmlEncodeSetSpec) {
        StringBuffer sb = new StringBuffer();
        sb.append("<request");
        Enumeration params = request.getParameterNames();
        while (params.hasMoreElements()) {
            String name = (String)params.nextElement();
            if (validParamNames.contains(name)) {
                String value = request.getParameter(name);
                if (value != null && value.length() > 0) {
                    sb.append(" ");
                    sb.append(name);
                    sb.append("=\"");
                    if (!xmlEncodeSetSpec && "set".equals(name)) {
//                      try {
                        sb.append(value);
//                      sb.append(URLEncoder.encode(value, "UTF-8"));
//                      } catch (UnsupportedEncodingException e) {
//                      e.printStackTrace();
//                      sb.append("UnsupportedEncodingException");
//                      }
                    } else {
                        sb.append(OAIUtil.xmlEncode(value));
                    }
                    sb.append("\"");
                }
            }
        }
        sb.append(">");
        sb.append(baseURL);
        sb.append("</request>");
        return sb.toString();
    }

    protected static boolean hasBadArguments(HttpServletRequest request,
            Iterator requiredParamNames,
            List validParamNames,
            AbstractCatalog catalog) {
        while (requiredParamNames.hasNext()) {
            String name = (String)requiredParamNames.next();
            String value = request.getParameter(name);
            if (value == null || value.length() == 0) {
                return true;
            }
        }
        Enumeration params = request.getParameterNames();
        while (params.hasMoreElements()) {
            String name = (String)params.nextElement();
            String[] values = request.getParameterValues(name);
            if (!validParamNames.contains(name)) {
                return true;
            } else if (values.length > 1) {
                return true;
            } else if (values.length == 1 && !catalog.isValidParam(name, values[0])) {
            	return true;
            }
        }
        String identifier = request.getParameter("identifier");
        try {
            if (identifier != null && identifier.length() > 0) {
                identifier = URLEncoder.encode(identifier, "UTF-8");
                new URI(identifier);
            }
        } catch (Exception e) {
            return true;
        }
        return false;
    }

//  /**
//  * Get the OAI requestURL from the verb response
//  *
//  * @param request the HTTP servlet request object
//  * @return the current verb's requestURL value
//  */
//  protected static String getRequestURL(HttpServletRequest request) {
//  StringBuffer sb = new StringBuffer();
//  sb.append(HttpUtils.getRequestURL(request));
//  sb.append("?");
//  Enumeration params = request.getParameterNames();
//  while (params.hasMoreElements()) {
//  String name = (String)params.nextElement();
//  String value = request.getParameter(name);
//  sb.append(OAIUtil.xmlEncode(name));
//  sb.append("=");
//  sb.append(OAIUtil.xmlEncode(value));
//  if (params.hasMoreElements()) {
//  sb.append("&amp;");
//  }
//  }
//  return sb.toString();
//  }

    /**
     * Get the complete XML response for the verb request
     *
     * @return the complete XML response for the verb request
     */
    public String toString() {
        return xmlText;
    }

    protected static String render(HttpServletResponse response, String contentType, String result,
            Transformer transformer) throws TransformerException {
        String renderedResult = null;
        if (transformer != null) { // render on the server
            response.setContentType("text/html; charset=UTF-8");
            StringReader stringReader = new StringReader(result);
            StreamSource streamSource = new StreamSource(stringReader);
            StringWriter stringWriter = new StringWriter();
            synchronized (transformer) {
                transformer.transform(streamSource, new StreamResult(stringWriter));
            }
            renderedResult = stringWriter.toString();
        } else { // render on the client
            response.setContentType(contentType);
            renderedResult = result;
        }
        return renderedResult;
    }

    public static HashMap getVerbs(Properties properties) {
        HashMap serverVerbsMap = new HashMap();
        serverVerbsMap.put("ListRecords", ListRecords.class);
        serverVerbsMap.put("ListIdentifiers", ListIdentifiers.class);
        serverVerbsMap.put("GetRecord", GetRecord.class);
        serverVerbsMap.put("Identify", Identify.class);
        serverVerbsMap.put("ListMetadataFormats", ListMetadataFormats.class);
        serverVerbsMap.put("ListSets", ListSets.class);
        return serverVerbsMap;
    }

    public static HashMap getExtensionVerbs(Properties properties) {
        HashMap extensionVerbsMap = new HashMap();
        String propertyPrefix = "ExtensionVerbs.";
        Enumeration propNames = properties.propertyNames();
        while (propNames.hasMoreElements()) {
            String propertyName = (String)propNames.nextElement();
            if (propertyName.startsWith(propertyPrefix)) {
                String verb = propertyName.substring(propertyPrefix.length());
                String verbClassName = (String)properties.get(propertyName);
                if (debug) {
                    System.out.println("ExtensionVerb.getVerbs: verb=" + verb);
                    System.out.println("ExtensionVerb.verbClassName=" + verbClassName);
                }
                try {
                    Class serverVerbClass = Class.forName(verbClassName);
                    Method init =
                        serverVerbClass.getMethod("init",
                                new Class[] {Properties.class});
                    try {
                        init.invoke(null, new Object[] {properties});
                    } catch (InvocationTargetException e) {
                        throw e.getTargetException();
                    }
                    extensionVerbsMap.put(verb, serverVerbClass);
                    if (debug) {
                        System.out.println("ExtensionVerb.getVerbs: " + verb + "=" + verbClassName);
                    }
                } catch (Throwable e) {
                    System.err.println("ExtensionVerb: couldn't construct: " + verbClassName);
                    e.printStackTrace();
                }
            }
        }
        return extensionVerbsMap;
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb.extension;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.crosswalk.Crosswalks;
import ORG.oclc.oai.server.verb.BadArgumentException;
import ORG.oclc.oai.server.verb.BadVerb;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.server.verb.ServerVerb;

/**
 * This class represents a GetMetadata response on either the server or
 * the client.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class GetMetadata extends ServerVerb {
    private static final boolean debug = false;
    private static ArrayList validParamNames = new ArrayList();
    static {
	validParamNames.add("verb");
	validParamNames.add("identifier");
	validParamNames.add("metadataPrefix");
    }
    
    /**
     * Construct the xml response on the server-side.
     *
     * @param context the servlet context
     * @param request the servlet request
     * @return a String containing the XML response
     * @exception OAIBadRequestException an http 400 status error occurred
     * @exception OAINotFoundException an http 404 status error occurred
     * @exception OAIInternalServerError an http 500 status error occurred
     */
    public static String construct(HashMap context,
                                   HttpServletRequest request, HttpServletResponse response,
                                   Transformer serverTransformer)
        throws FileNotFoundException, TransformerException {
        Properties properties = (Properties)context.get("OAIHandler.properties");
	AbstractCatalog abstractCatalog =
	    (AbstractCatalog)context.get("OAIHandler.catalog");
	String baseURL = properties.getProperty("OAIHandler.baseURL");
	if (baseURL == null) {
	    try {
		baseURL = request.getRequestURL().toString();
	    } catch (java.lang.NoSuchMethodError f) {
		baseURL = request.getRequestURL().toString();
	    }
	}
        StringBuffer sb = new StringBuffer();
        String identifier = request.getParameter("identifier");
        String metadataPrefix = request.getParameter("metadataPrefix");

        if (debug) {
            System.out.println("GetMetadata.constructGetMetadata: identifier=" +
                               identifier);
            System.out.println("GetMetadata.constructGetMetadata: metadataPrefix="
                               + metadataPrefix);
        }
        Crosswalks crosswalks = abstractCatalog.getCrosswalks();
        sb.append("<?xml version=\"1.0\" encoding=\"");
        String encoding = crosswalks.getEncoding(metadataPrefix);
        if (encoding != null)
            sb.append(encoding);
        else
            sb.append("UTF-8");
        sb.append("\" ?>\n");
        
        String docType = crosswalks.getDocType(metadataPrefix);
        if (docType != null) {
            sb.append(docType);
            sb.append("\n");
        }
	try {
	    if (metadataPrefix == null || metadataPrefix.length() == 0
		|| identifier == null || identifier.length() == 0) {
                if (debug) System.out.println("Bad argument");
		throw new BadArgumentException();
	    }
	    else if (!crosswalks.containsValue(metadataPrefix)) {
                if (debug) System.out.println("crosswalk not present: " + metadataPrefix);
		throw new CannotDisseminateFormatException(metadataPrefix);
	    } else {
 		String metadata = abstractCatalog.getMetadata(identifier, metadataPrefix);
 		if (metadata != null) {
                    sb.append(metadata);
 		} else {
                    if (debug) System.out.println("ID does not exist");
 		    throw new IdDoesNotExistException(identifier);
 		}
	    }
	} catch (BadArgumentException e) {
            if (debug) e.printStackTrace();
            throw new FileNotFoundException();
	} catch (CannotDisseminateFormatException e) {
            if (debug) e.printStackTrace();
            throw new FileNotFoundException();
	} catch (IdDoesNotExistException e) {
            if (debug) e.printStackTrace();
            throw new FileNotFoundException();
	} catch (OAIInternalServerError e) {
            e.printStackTrace();
            return BadVerb.construct(context, request, response, serverTransformer);
	}
        if (debug) {
            System.out.println("GetMetadata.construct: contentType=" + crosswalks.getContentType(metadataPrefix));
            System.out.println("GetMetadata.construct: prerendered sb=" + sb.toString());
        }
        return render(response, crosswalks.getContentType(metadataPrefix), sb.toString(), (Transformer)null);
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.server.verb.extension;

import java.io.FileNotFoundException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.crosswalk.Crosswalks;
import ORG.oclc.oai.server.verb.BadArgumentException;
import ORG.oclc.oai.server.verb.BadVerb;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.server.verb.ServerVerb;

/**
 * This class represents a Redirect response on either the server or
 * the client.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class Redirect extends ServerVerb {
    private static final boolean debug = true;
    private static Transformer transformer;
    static {
        String xsltString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+"<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n"
+"                              xmlns:oai=\"http://www.openarchives.org/OAI/2.0/\"\n"
+"                              xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\"\n"
+"                              xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n"
+"  <xsl:output method=\"html\" version=\"4.0\"/>\n"
+"  <xsl:param name=\"base.url\"/>\n"
+"\n"
+"  <xsl:template match=\"/\">\n"
+"    <html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
+"      <xsl:choose>\n"
+"        <xsl:when test=\"record/metadata/oai_dc:dc/dc:identifier[1]\">\n"
+"          <head>\n"
+"            <meta http-equiv=\"Refresh\">\n"
+"              <xsl:attribute name=\"content\"><xsl:text>0; URL=</xsl:text><xsl:value-of select=\"record/metadata/oai_dc:dc/dc:identifier[1]\" /></xsl:attribute>\n"
+"            </meta>\n"
+"          </head>\n"
+"          <body/>\n"
+"       </xsl:when>\n"            
+"       <xsl:when test=\"$base.url\">\n"
+"          <head>\n"
+"            <meta http-equiv=\"Refresh\">\n"
+"              <xsl:attribute name=\"content\"><xsl:text>0; URL=</xsl:text><xsl:value-of select=\"$base.url\" /><xsl:text>/extension?verb=GetMetadata&amp;metadataPrefix=oai_dc&amp;identifier=</xsl:text><xsl:value-of select=\"record/header/identifier\" /></xsl:attribute>\n"
+"            </meta>\n"
+"          </head>\n"
+"          <body/>\n"
+"       </xsl:when>\n"
+"       <xsl:otherwise>\n"
+"          <head>\n"
+"            <title><xsl:text>No dc:identifier field found for &apos;</xsl:text><xsl:value-of select=\"record/header/identifier\"/><xsl:text>&apos;</xsl:text></title>\n"
+"          </head>\n"
+"          <body>\n"
+"            <h1><xsl:text>No dc:identifier field found for &apos;</xsl:text><xsl:value-of select=\"record/header/identifier\"/><xsl:text>&apos;</xsl:text></h1>\n"
+"          </body>\n"
+"       </xsl:otherwise>\n"            
+"      </xsl:choose>\n"
+"    </html>\n"
+"  </xsl:template>\n"
+"</xsl:stylesheet>\n";
        try {
            if (debug) {
                System.out.println("Redirect.<init>: xsltString=" + xsltString);
            }
            StreamSource xslSource = new StreamSource(new StringReader(xsltString));
            TransformerFactory tFactory = TransformerFactory.newInstance();
            transformer = tFactory.newTransformer(xslSource);
        } catch (TransformerException e) {
            e.printStackTrace();
        }
    };
    private static ArrayList validParamNames = new ArrayList();
    static {
	validParamNames.add("verb");
	validParamNames.add("identifier");
    }
    
    /**
     * Construct the xml response on the server-side.
     *
     * @param context the servlet context
     * @param request the servlet request
     * @return a String containing the XML response
     * @exception OAIBadRequestException an http 400 status error occurred
     * @exception OAINotFoundException an http 404 status error occurred
     * @exception OAIInternalServerError an http 500 status error occurred
     */
    public static String construct(HashMap context,
                                   HttpServletRequest request, HttpServletResponse response,
                                   Transformer serverTransformer)
        throws FileNotFoundException, TransformerException {
        Properties properties = (Properties)context.get("OAIHandler.properties");
	AbstractCatalog abstractCatalog =
	    (AbstractCatalog)context.get("OAIHandler.catalog");
	String baseURL = properties.getProperty("OAIHandler.baseURL");
	if (baseURL == null) {
	    try {
		baseURL = request.getRequestURL().toString();
	    } catch (java.lang.NoSuchMethodError f) {
		baseURL = request.getRequestURL().toString();
	    }
	}
        StringBuffer sb = new StringBuffer();
        String identifier = request.getParameter("identifier");

        if (debug) {
            System.out.println("Redirect.construct: identifier=" +
                               identifier);
        }
        Crosswalks crosswalks = abstractCatalog.getCrosswalks();
	try {
	    if (identifier == null || identifier.length() == 0) {
                if (debug) System.out.println("Bad argument");
		throw new BadArgumentException();
	    }
	    else if (!crosswalks.containsValue("oai_dc")) {
                if (debug) System.out.println("crosswalk not present: oai_dc");
		throw new CannotDisseminateFormatException("oai_dc");
	    } else {
 		String metadata = abstractCatalog.getRecord(identifier, "oai_dc");
 		if (metadata != null) {
                    sb.append(metadata);
  		} else {
                    if (debug) System.out.println("ID does not exist");
  		    throw new IdDoesNotExistException(identifier);
 		}
	    }
	} catch (BadArgumentException e) {
            if (debug) e.printStackTrace();
            throw new FileNotFoundException(e.getMessage());
	} catch (CannotDisseminateFormatException e) {
            if (debug) e.printStackTrace();
            throw new FileNotFoundException(e.getMessage());
 	} catch (IdDoesNotExistException e) {
            if (debug) e.printStackTrace();
            throw new FileNotFoundException(e.getMessage());
	} catch (OAIInternalServerError e) {
            e.printStackTrace();
            return BadVerb.construct(context, request, response, serverTransformer);
	}
        if (debug) {
            System.out.println("Redirect.construct: prerendered sb=" + sb.toString());
        }
        if (debug) {
            System.out.println("Redirect.construct: transformer=" + transformer);
        }
        synchronized (transformer) {
            transformer.setParameter("base.url", baseURL);
            String out = render(response, (String)null, sb.toString(), transformer);
            transformer.clearParameters();
            if (debug) {
                System.out.println("Redirect.construct: out=" + out);
            }
            return out;
        }
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ORG.oclc.oai.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;


/**
 * Utility methods for OAICat and OAIHarvester
 */
public class OAIUtil {
    private static DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
    private static TransformerFactory tFactory = TransformerFactory.newInstance();
    static {
        dbFactory.setNamespaceAware(true);
    }
    
    /**
     * XML encode a string.
     * @param s any String
     * @return the String with &amp;, &lt;, and &gt; encoded for use in XML.
     */
    public static String xmlEncode(String s) {
        StringBuffer sb = new StringBuffer();

        for (int i=0; i<s.length(); ++i) {
            char c = s.charAt(i);
            switch (c) {
            case '&':
                sb.append("&amp;");
                break;
            case '<':
                sb.append("&lt;");
                break;
            case '>':
                sb.append("&gt;");
                break;
            case '"':
                sb.append("&quot;");
                break;
            case '\'':
                sb.append("&apos;");
                break;
            default:
                sb.append(c);
                break;
            }
        }
        return sb.toString();
    }

    /**
     * Convert a packed LCCN String to MARC display format.
     * @param packedLCCN an LCCN String in packed storage format (e.g. 'n&nbsp;&nbsp;2001050268').
     * @return  an LCCN String in MARC display format (e.g. 'n2001-50268').
     */
    public static String toLCCNDisplay(String packedLCCN) {
	StringBuffer sb = new StringBuffer();
	if (Character.isDigit(packedLCCN.charAt(2))) {
	    sb.append(packedLCCN.substring(0, 2).trim());
	    sb.append(packedLCCN.substring(2, 6));
	    sb.append("-");
	    int i = Integer.parseInt(packedLCCN.substring(6).trim());
	    sb.append(Integer.toString(i));
	} else {
	    sb.append(packedLCCN.substring(0, 3).trim());
	    sb.append(packedLCCN.substring(3, 5));
	    sb.append("-");
	    int i = Integer.parseInt(packedLCCN.substring(5).trim());
	    sb.append(Integer.toString(i));
	}
	return sb.toString();
    }

    /**
     * convert a packed LCCN to display format.
     * @param packedLCCN an LCCN String in packed storage format (e.g. 'n&nbsp;&nbsp;2001050268').
     * @return  an LCCN String in MARC display format (e.g. 'n2001-50268').
     * @deprecated use toLCCNDisplay() instead.
     */
    public static String getLCCN(String packedLCCN) {
	return toLCCNDisplay(packedLCCN);
    }

    public static Document parse(InputStream is)
    throws SAXException, IOException, ParserConfigurationException {
        return getThreadedDocumentBuilder().parse(is);
    }

    public static DocumentBuilder getThreadedDocumentBuilder()
    throws ParserConfigurationException {
        DocumentBuilder builder = dbFactory.newDocumentBuilder();
        return builder;
    }
    
    /**
     * Transform a DOM Node into an XML String.
     * @param node
     * @return an XML String representation of the specified Node
     * @throws TransformerException
     */
    public static String toString(Node node)
    throws TransformerException {
        return toString(node, true);
    }
    
    /**
     * Transform a DOM Node into an XML String
     * @param node
     * @param omitXMLDeclaration
     * @return an XML String representation of the specified Node
     * @throws TransformerException
     */
    public static String toString(Node node, boolean omitXMLDeclaration)
    throws TransformerException {
        StringWriter writer = new StringWriter();
        Transformer transformer =
            getThreadedIdentityTransformer(omitXMLDeclaration);
        Source source = new DOMSource(node);
        Result result = new StreamResult(writer);
        transformer.transform(source, result);
        return writer.toString();
    }
    
    /**
     * Get a thread-safe Transformer without an assigned transform. This is useful
     * for transforming a DOM Document into XML text.
     * @param omitXmlDeclaration 
     * @return an "identity" Transformer assigned to the current thread
     * @throws TransformerConfigurationException
     */
    public static Transformer getThreadedIdentityTransformer(
            boolean omitXmlDeclaration)
    throws TransformerConfigurationException {
        return getTransformer(omitXmlDeclaration, (String) null);
    }
    
    /**
     * Get a thread-safe Transformer.
     * @param omitXmlDeclaration
     * @param xslURL
     * @return a thread-safe Transformer
     * @throws TransformerConfigurationException
     */
    public static Transformer getTransformer(boolean omitXmlDeclaration,
            String xslURL)
    throws TransformerConfigurationException {
        return getTransformer(omitXmlDeclaration, true, xslURL);
    }
    
    /**
     * @param omitXmlDeclaration
     * @param standalone
     * @param xslURL 
     * @return a Transformer for the specified XSL document
     * @throws TransformerConfigurationException
     */
    public static Transformer getTransformer(
            boolean omitXmlDeclaration,
            boolean standalone, String xslURL)
    throws TransformerConfigurationException {
        Transformer transformer = null;
        if (xslURL == null) {
            transformer = tFactory.newTransformer(); // "never null"
        } else {
            Source xslSource = null;
            if (xslURL.startsWith("file://")) {
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(xslURL.substring(6));
                xslSource = new StreamSource(is);
            } else {
                xslSource = new StreamSource(xslURL);
            }
            transformer = tFactory.newTransformer(xslSource);
        }
//      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.STANDALONE,
                standalone?"yes":"no");
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
                omitXmlDeclaration?"yes":"no");
        return transformer;
    }
    
}

/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.acme.oai;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;

/**
 * DummyOAICatalog is an example of how to implement the AbstractCatalog interface.
 * Pattern an implementation of the AbstractCatalog interface after this class
 * to have OAICat work with your database. Your effort may be minimized by confining
 * your changes to areas identified by "YOUR CODE GOES HERE" comments. In truth, though,
 * you can do things however you want, as long as the non-private methods return
 * what they're supposed to.
 *
 * @author Jeffrey A. Young, OCLC Online Computer Library Center
 */
public class DummyOAICatalog extends AbstractCatalog {
    /**
     * maximum number of entries to return for ListRecords and ListIdentifiers
     */
    private static int maxListSize;

    /**
     * pending resumption tokens
     */
    private HashMap resumptionResults = new HashMap();

    /**************************************************************
     * YOUR CODE GOES HERE
     * delete dummyDb and create new private variables
     * to manage your database here
     **************************************************************/
    
    /**
     * Dummy database. Note that the two items each contain two metadataFormats;
     * oai_dc and oai_etdms
     */
    private String[] dummyDb = {
        "<record><header><identifier>oai:oaicat.oclc.org:OCLCNo/ocm00000012</identifier><datestamp>2001-02-02</datestamp><setSpec>music</setSpec><setSpec>music:(muzak)</setSpec></header><metadata><oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd\"><dc:language>eng</dc:language><dc:date>1964</dc:date><dc:type>Text data</dc:type><dc:identifier>ocm00000012</dc:identifier><dc:identifier>64063999 //r83</dc:identifier><dc:coverage>n-us---</dc:coverage><dc:subject>KF6369.3--.G3</dc:subject><dc:subject>340</dc:subject><dc:creator>Galles, George Raymond,--1918-</dc:creator><dc:title>Involuntary conversions under the Federal income tax laws.</dc:title><dc:publisher>[University of Idaho, Bureau of Business and Economic Research]</dc:publisher><dc:publisher>Moscow</dc:publisher><dc:format>v, 46 l.</dc:format><dc:format>28 cm.</dc:format><dc:relation>Idaho BBER--monograph no. 2</dc:relation><dc:subject>Income tax--Law and legislation--United States.</dc:subject><dc:relation>University of Idaho.--Bureau of Business and Economic Research.--Idaho BBER monograph ;--no. 2.</dc:relation></oai_dc:dc><oai_etdms:thesis xmlns:oai_etdms=\"http://www.ndltd.org/standards/metadata/etdms/1.0/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd\"><oai_etdms:title>Involuntary conversions under the Federal income tax laws.</oai_etdms:title><oai_etdms:creator>Galles, George Raymond,--1918-</oai_etdms:creator><oai_etdms:subject>KF6369.3--.G3</oai_etdms:subject><oai_etdms:subject>340</oai_etdms:subject><oai_etdms:subject>Income tax--Law and legislation--United States.</oai_etdms:subject><oai_etdms:publisher>[University of Idaho, Bureau of Business and Economic Research]</oai_etdms:publisher><oai_etdms:publisher>Moscow</oai_etdms:publisher><oai_etdms:date>1964</oai_etdms:date><oai_etdms:type>Text data</oai_etdms:type><oai_etdms:format>v, 46 l.--28 cm.</oai_etdms:format><oai_etdms:identifier>ocm00000012</oai_etdms:identifier><oai_etdms:identifier>64063999 //r83</oai_etdms:identifier><oai_etdms:language>eng</oai_etdms:language><oai_etdms:coverage>n-us---</oai_etdms:coverage></oai_etdms:thesis></metadata></record>",
        "<rec><header><identifier>oai:oaicat.oclc.org:OCLCNo/ocm00003601</identifier><datestamp>2001-02-02</datestamp><setSpec>music</setSpec></header><metadata><oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd\"><dc:language>eng</dc:language><dc:date>1967</dc:date><dc:type>Text data</dc:type><dc:identifier>ocm00003600</dc:identifier><dc:identifier>68066972 //r915</dc:identifier><dc:subject>E78.W3--W3 no. 25</dc:subject><dc:subject>970.4/96/37</dc:subject><dc:creator>Barnes, Paul L.</dc:creator><dc:title>Archaeology of the Dean Site, Twin Falls County, Idaho,</dc:title><dc:publisher>[Pullman,</dc:publisher><dc:format>vii, 89 p.--illus., maps.</dc:format><dc:format>28 cm.</dc:format><dc:relation>Laboratory of Anthropology, Washington State University. Report of investigations,--no. 25</dc:relation><dc:subject>Dean site.</dc:subject><dc:subject>Idaho--Antiquities.</dc:subject><dc:relation>Reports of investigations (Washington State University. Laboratory of Anthropology) ;--no. 25.</dc:relation></oai_dc:dc><oai_etdms:thesis xmlns:oai_etdms=\"http://www.ndltd.org/standards/metadata/etdms/1.0/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ndltd.org/standards/metadata/etdms/1.0/ http://www.ndltd.org/standards/metadata/etdms/1.0/etdms.xsd\"><oai_etdms:title>Archaeology of the Dean Site, Twin Falls County, Idaho,</oai_etdms:title><oai_etdms:creator>Barnes, Paul L.</oai_etdms:creator><oai_etdms:subject>E78.W3--W3 no. 25</oai_etdms:subject><oai_etdms:subject>970.4/96/37</oai_etdms:subject><oai_etdms:subject>Dean site.</oai_etdms:subject><oai_etdms:subject>Idaho--Antiquities.</oai_etdms:subject><oai_etdms:publisher>[Pullman,</oai_etdms:publisher><oai_etdms:date>1967</oai_etdms:date><oai_etdms:type>Text data</oai_etdms:type><oai_etdms:format>vii, 89 p.--illus., maps.--28 cm.</oai_etdms:format><oai_etdms:identifier>ocm00003600</oai_etdms:identifier><oai_etdms:identifier>68066972 //r915</oai_etdms:identifier><oai_etdms:language>eng</oai_etdms:language></oai_etdms:thesis></metadata></rec>"
    };
    
    /**************************************************************
     * YOUR CODE GOES HERE
     * delete dummySets and then figure out an alternative way
     * to identify the sets to be returned to ListSets requests.
     * One way might be to include them in the oaicat.properties
     * file and then have the constructor grab them. The ideal
     * solution, though,  is probably to query your database
     * somehow for any sets that exist there.
     **************************************************************/
    
    /**
     * Dummy sets.
     */
    private String[] dummySets = {
        "<set><setSpec>music</setSpec><setName>Music collection</setName></set>",
        "<set><setSpec>music:(muzak)</setSpec><setName>Muzak collection</setName></set>",
        "<set><setSpec>music:(elec)</setSpec><setName>Electronic Music Collection</setName><setDescription><oai_dc:dc xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.epenarchives.org/OAI/2.0/oai_dc.xsd\"><dc:description>This set contains metadata describing electronic music recordings made during the 1950ies</dc:description></oai_dc:dc></setDescription></set>"
    };
    
    /**
     * Construct a DummyOAICatalog object
     *
     * @param properties a properties object containing initialization parameters
     */
    public DummyOAICatalog(Properties properties) {
        String maxListSize = properties.getProperty("DummyOAICatalog.maxListSize");
        if (maxListSize == null) {
            throw new IllegalArgumentException("DummyOAICatalog.maxListSize is missing from the properties file");
        } else {
            DummyOAICatalog.maxListSize = Integer.parseInt(maxListSize);
        }
        
        /************************************************************
         * YOUR CODE GOES HERE
         * Load other properties you need to initialize your database
         * (and perhaps sets) and store them in private instance
         * variables.
         ************************************************************/
        /************************************************************
         * YOUR CODE GOES HERE
         * Given the configuration properties above, open your
         * database here and store any necessary variables
         * to manage it in private instance variables.
         ************************************************************/
    }
    
    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * identifier.
     *
     * @param identifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception IdDoesNotExistException the specified identifier can't be found
     * @exception NoMetadataFormatsException the specified identifier was found
     * but the item is flagged as deleted and thus no schemaLocations (i.e.
     * metadataFormats) can be produced.
     */
    public Vector getSchemaLocations(String identifier)
        throws IdDoesNotExistException, NoMetadataFormatsException {
        /**********************************************************************
         * YOUR CODE GOES HERE
         * Retrieve the specified native item from your database.
         **********************************************************************/
        Object nativeItem = dummyDb[0];
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        /*
         * Let your recordFactory decide which schemaLocations
         * (i.e. metadataFormats) it can produce from the record.
         * Doing so will preserve the separation of database access
         * (which happens here) from the record content interpretation
         * (which is the responsibility of the RecordFactory implementation).
         */
        if (nativeItem == null) {
            throw new IdDoesNotExistException(identifier);
        } else {
            return getRecordFactory().getSchemaLocations(nativeItem);
        }
    }

    /**
     * Retrieve a list of identifiers that satisfy the specified criteria
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * It may seem strange for the map to include both "headers" and "identifiers"
     * since the identifiers can be obtained from the headers. This may be true, but
     * AbstractCatalog.listRecords() can operate quicker if it doesn't
     * need to parse identifiers from the XML headers itself. Better
     * still, do like I do below and override AbstractCatalog.listRecords().
     * AbstractCatalog.listRecords() is relatively inefficient because given the list
     * of identifiers, it must call getRecord() individually for each as it constructs
     * its response. It's much more efficient to construct the entire response in one fell
     * swoop by overriding listRecords() as I've done here.
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix) {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        
        /**********************************************************************
         * YOUR CODE GOES HERE
         **********************************************************************/
        
        /* Get some records from your database */
        Object[] nativeItems = dummyDb;
        int count;
        
        /* load the headers and identifiers ArrayLists. */
        for (count=0; count < maxListSize && count < nativeItems.length; ++count) {
            /* Use the RecordFactory to extract header/identifier pairs for each item */
            String[] header = getRecordFactory().createHeader(nativeItems[count]);
            headers.add(header[0]);
            identifiers.add(header[1]);
        }
        
        /* decide if you're done */
        if (count < nativeItems.length) {
            String resumptionId = getResumptionId();
            /*****************************************************************
             * Store an object appropriate for your database API in the
             * resumptionResults Map in place of nativeItems. This object
             * should probably encapsulate the information necessary to
             * perform the next resumption of ListIdentifiers. It might even
             * be possible to encode everything you need in the
             * resumptionToken, in which case you won't need the
             * resumptionResults Map. Here, I've done a silly combination
             * of the two. Stateless resumptionTokens have some advantages.
             *****************************************************************/
            resumptionResults.put(resumptionId, nativeItems);

            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                                                                     nativeItems.length,
                                                                     0));
            //          listIdentifiersMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSb.toString()));
        }
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     */
    public Map listIdentifiers(String resumptionToken)
        throws BadResumptionTokenException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        
        /**********************************************************************
         * YOUR CODE GOES HERE
         **********************************************************************/
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }
        
        /* Get some more records from your database */
        Object[] nativeItems = (Object[])resumptionResults.remove(resumptionId);
        if (nativeItems == null) {
            throw new BadResumptionTokenException();
        }
        int count;
        
        /* load the headers and identifiers ArrayLists. */
        for (count = 0; count < maxListSize && count+oldCount < nativeItems.length; ++count) {
            /* Use the RecordFactory to extract header/identifier pairs for each item */
            String[] header = getRecordFactory().createHeader(nativeItems[count+oldCount]);
            headers.add(header[0]);
            identifiers.add(header[1]);
        }
        
        /* decide if you're done. */
        if (count+oldCount < nativeItems.length) {
            resumptionId = getResumptionId();
            
            /*****************************************************************
             * Store an object appropriate for your database API in the
             * resumptionResults Map in place of nativeItems. This object
             * should probably encapsulate the information necessary to
             * perform the next resumption of ListIdentifiers. It might even
             * be possible to encode everything you need in the
             * resumptionToken, in which case you won't need the
             * resumptionResults Map. Here, I've done a silly combination
             * of the two. Stateless resumptionTokens have some advantages.
             *****************************************************************/
            resumptionResults.put(resumptionId, nativeItems);
            
            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(oldCount + count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                                                                     nativeItems.length,
                                                                     oldCount));
            //          listIdentifiersMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSb.toString()));
        }
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the specified metadata for the specified identifier
     *
     * @param identifier the OAI identifier
     * @param metadataPrefix the OAI metadataPrefix
     * @return the <record/> portion of the XML response.
     * @exception CannotDisseminateFormatException the metadataPrefix is not
     * supported by the item.
     * @exception IdDoesNotExistException the identifier wasn't found
     */
    public String getRecord(String identifier, String metadataPrefix)
        throws CannotDisseminateFormatException,
               IdDoesNotExistException {
        /**********************************************************************
         * YOUR CODE GOES HERE
         * Replace this nativeItem assignment with your database API code.
         **********************************************************************/
        Object nativeItem = dummyDb[0];
        if (nativeItem == null)
            throw new IdDoesNotExistException(identifier);
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        return constructRecord(nativeItem, metadataPrefix);
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria. Note, though,
     * that unlike the other OAI verb type methods implemented here, both of the
     * listRecords methods are already implemented in AbstractCatalog rather than
     * abstracted. This is because it is possible to implement ListRecords as a
     * combination of ListIdentifiers and GetRecord combinations. Nevertheless,
     * I suggest that you override both the AbstractCatalog.listRecords methods
     * here since it will probably improve the performance if you create the
     * response in one fell swoop rather than construct it one GetRecord at a time.
     *
     * @param from beginning date using the proper granularity
     * @param until ending date using the proper granularity
     * @param set the set name or null if no such limit is requested
     * @param metadataPrefix the OAI metadataPrefix or null if no such limit is requested
     * @return a Map object containing entries for a "records" Iterator object
     * (containing XML <record/> Strings) and an optional "resumptionMap" Map.
     * @exception CannotDisseminateFormatException the metadataPrefix isn't
     * supported by the item.
     */
    public Map listRecords(String from, String until, String set, String metadataPrefix)
        throws CannotDisseminateFormatException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        
        /**********************************************************************
         * YOUR CODE GOES HERE
         **********************************************************************/

        /* Get some records from your database */
        Object[] nativeItem = dummyDb;
        int count;

        /* load the records ArrayList */
        for (count=0; count < maxListSize && count < nativeItem.length; ++count) {
            String record = constructRecord(nativeItem[count], metadataPrefix);
            records.add(record);
        }

        /* decide if you're done */
        if (count < nativeItem.length) {
            String resumptionId = getResumptionId();
            
            /*****************************************************************
             * Store an object appropriate for your database API in the
             * resumptionResults Map in place of nativeItems. This object
             * should probably encapsulate the information necessary to
             * perform the next resumption of ListIdentifiers. It might even
             * be possible to encode everything you need in the
             * resumptionToken, in which case you won't need the
             * resumptionResults Map. Here, I've done a silly combination
             * of the two. Stateless resumptionTokens have some advantages.
             *****************************************************************/
            resumptionResults.put(resumptionId, nativeItem);

            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                                                                 nativeItem.length,
                                                                 0));
            //          listRecordsMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSbSb.toString()));
        }
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing entries for "headers" and "identifiers" Iterators
     * (both containing Strings) as well as an optional "resumptionMap" Map.
     * @exception BadResumptionTokenException the value of the resumptionToken argument
     * is invalid or expired.
     */
    public Map listRecords(String resumptionToken)
        throws BadResumptionTokenException {
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        purge(); // clean out old resumptionTokens
        
        /**********************************************************************
         * YOUR CODE GOES HERE
         **********************************************************************/
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }
        
        /* Get some more records from your database */
        Object[] nativeItem = (Object[])resumptionResults.remove(resumptionId);
        if (nativeItem == null) {
            throw new BadResumptionTokenException();
        }
        int count;
        
        /* load the headers and identifiers ArrayLists. */
        for (count = 0; count < maxListSize && count+oldCount < nativeItem.length; ++count) {
            try {
                String record = constructRecord(nativeItem[count+oldCount], metadataPrefix);
                records.add(record);
            } catch (CannotDisseminateFormatException e) {
                /* the client hacked the resumptionToken beyond repair */
                throw new BadResumptionTokenException();
            }
        }
        
        /* decide if you're done */
        if (count+oldCount < nativeItem.length) {
            resumptionId = getResumptionId();
            
            /*****************************************************************
             * Store an object appropriate for your database API in the
             * resumptionResults Map in place of nativeItems. This object
             * should probably encapsulate the information necessary to
             * perform the next resumption of ListIdentifiers. It might even
             * be possible to encode everything you need in the
             * resumptionToken, in which case you won't need the
             * resumptionResults Map. Here, I've done a silly combination
             * of the two. Stateless resumptionTokens have some advantages.
             *****************************************************************/
            resumptionResults.put(resumptionId, nativeItem);
            
            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(oldCount + count));
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(metadataPrefix);
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                                                                 nativeItem.length,
                                                                 oldCount));
            //          listRecordsMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSb.toString()));
        }
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }

    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(Object nativeItem, String metadataPrefix)
        throws CannotDisseminateFormatException {
        String schemaURL = null;

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix);
    }

    /**
     * Retrieve a list of sets that satisfy the specified criteria
     *
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception OAIBadRequestException signals an http status code 400 problem
     */
    public Map listSets() {
        purge(); // clean out old resumptionTokens
        Map listSetsMap = new HashMap();
        ArrayList sets = new ArrayList();
        /**********************************************************************
         * YOUR CODE GOES HERE
         **********************************************************************/

        /* decide which sets you're going to support */
        String[] dbSets = dummySets;
        int count;

        /* load the sets ArrayList */
        for (count=0; count < maxListSize && count < dbSets.length; ++count) {
            sets.add(dbSets[count]);
        }

        /* decide if you're done */
        if (count < dbSets.length) {
            String resumptionId = getResumptionId();
            
            /*****************************************************************
             * Store an object appropriate for your database API in the
             * resumptionResults Map in place of nativeItems. This object
             * should probably encapsulate the information necessary to
             * perform the next resumption of ListIdentifiers. It might even
             * be possible to encode everything you need in the
             * resumptionToken, in which case you won't need the
             * resumptionResults Map. Here, I've done a silly combination
             * of the two. Stateless resumptionTokens have some advantages.
             *****************************************************************/
            resumptionResults.put(resumptionId, dbSets);

            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(count));
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                                                              dbSets.length,
                                                              0));
            //          listSetsMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSbSb.toString()));
        }
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        listSetsMap.put("sets", sets.iterator());
        return listSetsMap;
    }

    /**
     * Retrieve the next set of sets associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listSets() Map result.
     * @return a Map object containing "sets" Iterator object (contains
     * <setSpec/> XML Strings) as well as an optional resumptionMap Map.
     * @exception BadResumptionTokenException the value of the resumptionToken
     * is invalid or expired.
     */
    public Map listSets(String resumptionToken)
        throws BadResumptionTokenException {
        Map listSetsMap = new HashMap();
        ArrayList sets = new ArrayList();
        purge(); // clean out old resumptionTokens
        
        /**********************************************************************
         * YOUR CODE GOES HERE
         **********************************************************************/
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

        /* Get some more sets */
        String[] dbSets = (String[])resumptionResults.remove(resumptionId);
        if (dbSets == null) {
            throw new BadResumptionTokenException();
        }
        int count;

        /* load the sets ArrayList */
        for (count = 0; count < maxListSize && count+oldCount < dbSets.length; ++count) {
            sets.add(dbSets[count+oldCount]);
        }

        /* decide if we're done */
        if (count+oldCount < dbSets.length) {
            resumptionId = getResumptionId();
            
            /*****************************************************************
             * Store an object appropriate for your database API in the
             * resumptionResults Map in place of nativeItems. This object
             * should probably encapsulate the information necessary to
             * perform the next resumption of ListIdentifiers. It might even
             * be possible to encode everything you need in the
             * resumptionToken, in which case you won't need the
             * resumptionResults Map. Here, I've done a silly combination
             * of the two. Stateless resumptionTokens have some advantages.
             *****************************************************************/
            resumptionResults.put(resumptionId, dbSets);
            
            /*****************************************************************
             * Construct the resumptionToken String however you see fit.
             *****************************************************************/
            StringBuffer resumptionTokenSb = new StringBuffer();
            resumptionTokenSb.append(resumptionId);
            resumptionTokenSb.append(":");
            resumptionTokenSb.append(Integer.toString(oldCount + count));
            
            /*****************************************************************
             * Use the following line if you wish to include the optional
             * resumptionToken attributes in the response. Otherwise, use the
             * line after it that I've commented out.
             *****************************************************************/
            listSetsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
                                                              dbSets.length,
                                                              oldCount));
            //          listSetsMap.put("resumptionMap",
            //                                 getResumptionMap(resumptionTokenSb.toString()));
        }
        /***********************************************************************
         * END OF CUSTOM CODE SECTION
         ***********************************************************************/
        listSetsMap.put("sets", sets.iterator());
        return listSetsMap;
    }

    /**
     * close the repository
     */
    public void close() { }
    
    /**
     * Purge tokens that are older than the configured time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date now = new Date();
        Iterator keySet = resumptionResults.keySet().iterator();
        while (keySet.hasNext()) {
            String key = (String)keySet.next();
            Date then = new Date(Long.parseLong(key) + getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            String key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }
    
    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a String version of the current time
     */
    private synchronized static String getResumptionId() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}

/**
*Copyright (c) 2000-2002 OCLC Online Computer Library Center,
*Inc. and other contributors. All rights reserved.  The contents of this file, as updated
*from time to time by the OCLC Office of Research, are subject to OCLC Research
*Public License Version 2.0 (the "License"); you may not use this file except in
*compliance with the License. You may obtain a current copy of the License at
*http://www.apache.org/licenses/LICENSE-2.0.html.  Software distributed under the License is
*distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
*or implied. See the License for the specific language governing rights and limitations
*under the License.  This software consists of voluntary contributions made by many
*individuals on behalf of OCLC Research. For more information on OCLC Research,
*please see http://www.oclc.org/oclc/research/.
*
*The Original Code is NewFileRecordFactory.java.
*The Initial Developer of the Original Code is Jeff Young.
*Portions created by Cristian Grunca 
*    CDWALite support
*    File name encoding
*Copyright (C) 2005 J. Paul Getty. All Rights Reserved.
*Contributor(s): Ted Dancescu, Steven Swimmer.
*/

package edu.getty.oai.server.catalog;

import java.io.UnsupportedEncodingException;
//import java.util.ArrayList;
//import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
//import java.util.StringTokenizer;
import ORG.oclc.oai.server.catalog.*;

/**
 * GettyFileRecordFactory converts native XML "items" to "record" Strings.
 * This factory assumes the native XML item looks exactly like the <record>
 * element of an OAI GetRecord response, with the possible exception that the
 * <metadata> element contains multiple metadataFormats from which to choose.
 */
public class GettyFileRecordFactory extends RecordFactory {
    private String repositoryIdentifier = null;
    
    /**
     * Construct an GettyFileRecordFactory capable of producing the Crosswalk(s)
     * specified in the properties file.
     * @param properties Contains information to configure the factory:
     *                   specifically, the names of the crosswalk(s) supported
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public GettyFileRecordFactory(Properties properties)
	throws IllegalArgumentException {
	super(properties);
	repositoryIdentifier = properties.getProperty("GettyFileRecordFactory.repositoryIdentifier");
	if (repositoryIdentifier == null) {
	    throw new IllegalArgumentException("GettyFileRecordFactory.repositoryIdentifier is missing from the properties file");
	}
    }

    /**
     * Utility method to parse the 'local identifier' from the OAI identifier
     *
     * @param identifier OAI identifier (e.g. oai:oaicat.oclc.org:ID/12345)
     * @return local identifier (e.g. ID/12345).
     */
    public String fromOAIIdentifier(String identifier) {
        return identifier;
    }

    /**
     * Construct an OAI identifier from the native item
     *
     * @param nativeItem native Item object
     * @return OAI identifier
     */
    public String getOAIIdentifier(Object nativeItem) {
	StringBuffer sb = new StringBuffer();
	sb.append(getLocalIdentifier(nativeItem));
	return sb.toString();
    }

    /**
     * Extract the local identifier from the native item
     *
     * @param nativeItem native Item object
     * @return local identifier
     */
    public String getLocalIdentifier(Object nativeItem) {
        return (String)((HashMap)nativeItem).get("localIdentifier");
    }

    /**
     * get the datestamp from the item
     *
     * @param nativeItem a native item presumably containing a datestamp somewhere
     * @return a String containing the datestamp for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public String getDatestamp(Object nativeItem)
	throws IllegalArgumentException  {
        return (String)((HashMap)nativeItem).get("lastModified");
    }

    /**
     * get the setspec from the item
     *
     * @param nativeItem a native item presumably containing a setspec somewhere
     * @return a String containing the setspec for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getSetSpecs(Object nativeItem)
	throws IllegalArgumentException  {
        return (Iterator)((HashMap)nativeItem).get("setSpecs");
    }

    /**
     * Get the about elements from the item
     *
     * @param nativeItem a native item presumably containing about information somewhere
     * @return a Iterator of Strings containing &lt;about&gt;s for the item
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public Iterator getAbouts(Object nativeItem) throws IllegalArgumentException {
	return null;
    }

    /**
     * Is the record deleted?
     *
     * @param nativeItem a native item presumably containing a possible delete indicator
     * @return true if record is deleted, false if not
     * @exception IllegalArgumentException Something is wrong with the argument.
     */
    public boolean isDeleted(Object nativeItem)
	throws IllegalArgumentException {
	return false;
    }

    /**
     * Allows classes that implement RecordFactory to override the default create() method.
     * This is useful, for example, if the entire &lt;record&gt; is already packaged as the native
     * record. Return null if you want the default handler to create it by calling the methods
     * above individually.
     * 
     * @param nativeItem the native record
     * @return a String containing the OAI &lt;record&gt; or null if the default method should be
     * used.
     */
    public String quickCreate(Object nativeItem, String schemaLocation, String metadataPrefix) {
	// Don't perform quick creates
        try {
            String result = new String((byte[])((HashMap)nativeItem).get("recordBytes"), "UTF-8");
            if (result.startsWith("<?")) {
                int offset = result.indexOf("?>");
                result = result.substring(offset+2);
            }
            return result;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
// 	return null;
    }
}
/*
 * The contents of this file, as updated from time to time by the OCLC Office
 * of Research, are subject to the OCLC Office of Research Public License
 * Version 1.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a current copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0.html.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the License.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the OCLC Office of Research. For more information on the OCLC
 * Office of Research, please see http://www.oclc.org/oclc/research/.
 *
 * This is Original Code.
 *
 * The Initial Developer(s) of the Original Code is (are):
 *  - Ralph LeVan <levan@oclc.org>
 * 
 * Portions created by OCLC are Copyright (C) 2001.
 *
 * 2002-04-09 Created
 */

package edu.getty.oai.server.catalog;
import ORG.oclc.oai.server.catalog.*;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.BadArgumentException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;



/**
 * GettyFileSystemOAICatalog is an implementation of AbstractCatalog interface
 * with the data sitting in a directory on a filesystem.
 *
 * @author Cristian Grunca, J. Paul Getty Trust
 */

public class GettyFileSystemOAICatalog extends AbstractCatalog {
    static final boolean debug=false;

    private SimpleDateFormat dateFormatter = new SimpleDateFormat();
    protected String           homeDir;
    private HashMap          fileDateMap = new HashMap();
    private HashMap setMap = new HashMap();
    private HashMap          resumptionResults=new HashMap();
    private int              maxListSize;
    private ArrayList sets = null;

    public GettyFileSystemOAICatalog(Properties properties) throws IOException {
        String          temp;

        dateFormatter.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
        temp=properties.getProperty("GettyFileSystemOAICatalog.maxListSize");
        if (temp==null)
            throw new IllegalArgumentException("GettyFileSystemOAICatalog."+
                "maxListSize is missing from the properties file");
        maxListSize = Integer.parseInt(temp);
        if(debug)
            System.out.println("in GettyFileSystemOAICatalog(): maxListSize="+
                maxListSize);

        homeDir=properties.getProperty("GettyFileSystemOAICatalog.homeDir");
        if (homeDir==null)
            throw new IllegalArgumentException("GettyFileSystemOAICatalog."+
                "homeDir is missing from the properties file");
        if(debug)
            System.out.println("in GettyFileSystemOAICatalog(): homeDir="+
                homeDir);


	File homeFile = new File(homeDir);
	int homeDirLen = homeFile.getPath().length()+1;
	loadFileMap(homeDirLen, homeFile);
// 	Iterator iterator = fileDateMap.entrySet().iterator();
// 	while (iterator.hasNext()) {
// 	    Map.Entry entry = (Map.Entry)iterator.next();
// 	    System.out.println(entry.getKey() + ":" + entry.getValue());
// 	}
	sets = getSets(properties);
    }

    private static ArrayList getSets(Properties properties) {
        TreeMap treeMap = new TreeMap();
	String propertyPrefix = "Sets.";
	Enumeration propNames = properties.propertyNames();
	while (propNames.hasMoreElements()) {
	    String propertyName = (String)propNames.nextElement();
	    if (propertyName.startsWith(propertyPrefix)) {
                treeMap.put(propertyName, properties.get(propertyName));
	    }
	}
 	return new ArrayList(treeMap.values());
    }
    
    private void loadFileMap(int homeDirLen, File currentDir)
        throws IOException {
        try {
            String[] list = currentDir.list();
            //DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            //DocumentBuilder builder = factory.newDocumentBuilder();
            for (int i=0; i<list.length; ++i) {
                File child = new File(currentDir, list[i]);
                if (child.isDirectory() && !"CVS".equals(child.getName())) {
                    loadFileMap(homeDirLen, child);
                } else if (isMetadataFile(child)) {
                    String path = file2path(homeDirLen, child);
                    System.out.println("parsing : " + path);
                    String datestamp = date2OAIDatestamp(new Date(child.lastModified()));

					fileDateMap.put(path, datestamp);
                                                        
                    String setSpec = file2OAISetSpec(child);
                    if(setSpec != null)
                    {
						ArrayList setSpecList = (ArrayList)setMap.get(setSpec);
						if (setSpecList == null) {
							setSpecList = new ArrayList();
							setMap.put(setSpec, setSpecList);
						}
						
						setSpecList.add(path);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Override this method if some files exist in the
     * filesystem that aren't metadata records.
     *
     * @param child the File to be investigated
     * @return true if it contains metadata, false otherwise
     */
    protected boolean isMetadataFile(File child) {
        return true;
    }

    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param homeDirLen the length of the home directory path
     * @param file the File object containing the native record
     * @return localIdentifier
     */
    protected String file2path(int homeDirLen, File file) {
	String fileName = file.getPath().substring(homeDirLen).replace(File.separatorChar, '/');
	return fileName;
    }
    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param file the File object containing the native record
     * @return localIdentifier
     */
    protected String file2OAISetSpec(File file) {
	String fileName = file.getName();
	int lastIndex = fileName.lastIndexOf(".");
	if(lastIndex < 0)
		return null;
	lastIndex = fileName.lastIndexOf(".", lastIndex - 1);	
	if(lastIndex < 0)
		return null;
	int firstIndex = fileName.lastIndexOf(".", lastIndex - 1);
	if(firstIndex < 0)
		return null;	
	String setSpec = fileName.substring(firstIndex+1,lastIndex);
	
	return setSpec;
    }

    /**
     * Override this method if you don't like the default localIdentifiers.
     * @param localIdentifier the localIdentifier as parsed from the OAI identifier
     * @return the File object containing the native record
     */
    protected File localIdentifier2File(String localIdentifier) {
        String fileName = localIdentifier.replace('/', File.separatorChar);
	return new File(homeDir, fileName);
    }

    private String date2OAIDatestamp(Date date) {
         return dateFormatter.format(date);
    }

    private HashMap getNativeHeader(String path)
        throws IOException {
        HashMap recordMap = null;
        if (fileDateMap.containsKey(path)) {
            recordMap = new HashMap();
            recordMap.put("localIdentifier", path.substring(0, path.lastIndexOf(".")));
            recordMap.put("lastModified", fileDateMap.get(path));
            ArrayList setSpecs = new ArrayList();
            Iterator keySet = setMap.keySet().iterator();
            while (keySet.hasNext()) {
                String key = (String)keySet.next();
                ArrayList identifierList = (ArrayList)setMap.get(key);
                if (identifierList.contains(path)) {
                    setSpecs.add(key);
                }
            }
            recordMap.put("setSpecs", setSpecs.iterator());
            return recordMap;
        }
        return recordMap;
    }

    private ArrayList getExtensionList(String localIdentifier) {
//         System.out.println("getExtensionList: localIdentifier="
//                            + localIdentifier);
        ArrayList list = new ArrayList();
        Iterator iterator = fileDateMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry)iterator.next();
//             System.out.println("entry.getKey()=" + entry.getKey());
            if (((String)entry.getKey()).startsWith(localIdentifier)) {
                list.add(((String)entry.getKey()).substring(localIdentifier.length()+1));
            }
        }
        return list;
    }
    
    private HashMap getNativeRecord(String path)
        throws IOException {
        HashMap recordMap = getNativeHeader(path);
        if (recordMap == null) {
            return null;
        } else {
            File file = localIdentifier2File(path);
            try {
                FileInputStream fis = new FileInputStream(file);
                BufferedInputStream bis = new BufferedInputStream(fis);
                byte[] buffer = new byte[(int)file.length()];
                bis.read(buffer, 0, (int)file.length());
                recordMap.put("recordBytes", buffer);
                bis.close();
                fis.close();
                return recordMap;
            } catch (FileNotFoundException e) {
                return null;
            }
        }
    }
    
    /**
     * Retrieve the specified metadata for the specified oaiIdentifier
     *
     * @param     oaiIdentifier the OAI identifier
     * @param     metadataPrefix the OAI metadataPrefix
     * @return    the Record object containing the result.
     * @exception CannotDisseminateFormatException signals an http status
     *                code 400 problem
     * @exception IdDoesNotExistException signals an http status code 404
     *                problem
     * @exception OAIInternalServerError signals an http status code 500
     *                problem
     */
    public String getRecord(String oaiIdentifier, String metadataPrefix)
        throws IdDoesNotExistException, CannotDisseminateFormatException,
               OAIInternalServerError {
        HashMap nativeItem = null;
        try {
	    String localIdentifier
		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);

            nativeItem = getNativeRecord(localIdentifier + "." + metadataPrefix);
            if (nativeItem == null)
                throw new IdDoesNotExistException(oaiIdentifier);
            return constructRecord(nativeItem, metadataPrefix);
        } catch (IOException e) {
            e.printStackTrace();
            throw new OAIInternalServerError("Database Failure");
        }
    }


    /**
     * Retrieve a list of schemaLocation values associated with the specified
     * oaiIdentifier.
     *
     * We get passed the ID for a record and are supposed to return a list
     * of the formats that we can deliver the record in.  Since we are assuming
     * that all the records in the directory have the same format, the
     * response to this is static;
     *
     * @param oaiIdentifier the OAI identifier
     * @return a Vector containing schemaLocation Strings
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAINotFoundException signals an http status code 404 problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Vector getSchemaLocations(String oaiIdentifier)
      throws IdDoesNotExistException, OAIInternalServerError, NoMetadataFormatsException {
        ArrayList extensionList = null;
         try {
	    String localIdentifier
		= getRecordFactory().fromOAIIdentifier(oaiIdentifier);
            extensionList = getExtensionList(localIdentifier);
         } catch (Exception e) {
             e.printStackTrace();
             throw new OAIInternalServerError("Database Failure");
         }
        
        if (extensionList != null) {
            return getRecordFactory().getSchemaLocations(extensionList);
        } else {
            throw new IdDoesNotExistException(oaiIdentifier);
        }
    }


    /**
     * Retrieve a list of Identifiers that satisfy the criteria parameters
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix)
        throws BadArgumentException, CannotDisseminateFormatException, OAIInternalServerError,
               NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
	Iterator iterator = fileDateMap.entrySet().iterator();
	int numRows = fileDateMap.entrySet().size();
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            String path = (String)entryDateMap.getKey();
            if (fileDate.compareTo(from) >= 0
                && fileDate.compareTo(until) <= 0
                && (set == null || setIdentifiers != null && setIdentifiers.contains(path))) {
                try {
                    HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
                    String[] header = getRecordFactory().createHeader(nativeHeader);
                    headers.add(header[0]);
                    identifiers.add(header[1]);
                    count++;
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            }
	}

        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listIdentifiersMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listIdentifiersMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }

    /**
     * Retrieve the next set of Identifiers associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listIdentifiers() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and an "identifiers" Map object. The "identifiers" Map contains OAI
     * identifier keys with corresponding values of "true" or null depending on
     * whether the identifier is deleted or not.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listIdentifiers(String resumptionToken)
      throws BadResumptionTokenException, OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listIdentifiersMap = new HashMap();
        ArrayList headers = new ArrayList();
        ArrayList identifiers = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        String set;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            set = tokenizer.nextToken();
            if(set.equals("null")) set = null;            
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("GettyFileSystemOAICatalog.listIdentifiers: reuse of old resumptionToken?");
	    iterator = fileDateMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the headers and identifiers ArrayLists. */
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String path = (String)entryDateMap.getKey();
            if (set == null || setIdentifiers != null && setIdentifiers.contains(path)) {
                try {
                    HashMap nativeHeader = getNativeHeader((String)entryDateMap.getKey());
                    String[] header = getRecordFactory().createHeader(nativeHeader);
                    headers.add(header[0]);
                    identifiers.add(header[1]);
                    count++;
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            }
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listIdentifiersMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        return listIdentifiersMap;
    }


    /**
     * Utility method to construct a Record object for a specified
     * metadataFormat from a native record
     *
     * @param nativeItem native item from the dataase
     * @param metadataPrefix the desired metadataPrefix for performing the crosswalk
     * @return the <record/> String
     * @exception CannotDisseminateFormatException the record is not available
     * for the specified metadataPrefix.
     */
    private String constructRecord(HashMap nativeItem, String metadataPrefix)
        throws CannotDisseminateFormatException, OAIInternalServerError {
        String schemaURL = null;
	Iterator setSpecs = getSetSpecs(nativeItem);
	Iterator abouts = getAbouts(nativeItem);

        if (metadataPrefix != null) {
            if ((schemaURL = getCrosswalks().getSchemaURL(metadataPrefix)) == null)
                throw new CannotDisseminateFormatException(metadataPrefix);
        }
        return getRecordFactory().create(nativeItem, schemaURL, metadataPrefix, setSpecs, abouts);
    }

    /**
     * get an Iterator containing the setSpecs for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of setSpec values for this nativeItem
     */
    private Iterator getSetSpecs(HashMap nativeItem)
	throws OAIInternalServerError {
        return null;
    }

    /**
     * get an Iterator containing the abouts for the nativeItem
     *
     * @param rs ResultSet containing the nativeItem
     * @return an Iterator containing the list of about values for this nativeItem
     */
    private Iterator getAbouts(HashMap nativeItem)
	throws OAIInternalServerError {
        return null;
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @param metadataPrefix the OAI metadataPrefix
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listRecords(String from, String until, String set,
                                    String metadataPrefix)
      throws BadArgumentException, CannotDisseminateFormatException,
      OAIInternalServerError, NoItemsMatchException {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
	Iterator iterator = fileDateMap.entrySet().iterator();
	int numRows = fileDateMap.entrySet().size();
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String fileDate = (String)entryDateMap.getValue();
            String path = (String)entryDateMap.getKey();
            String extension = path.substring(path.lastIndexOf(".")+1);
            if (fileDate.compareTo(from) >= 0
                && fileDate.compareTo(until) <= 0
                && extension.equals(metadataPrefix)
                && (set == null || setIdentifiers != null && setIdentifiers.contains(path))) {
                try {
                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                    count++;
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new OAIInternalServerError(e.getMessage());
                }
            }
	}
        
        if (count == 0)
            throw new NoItemsMatchException();
        
	/* decide if you're done */
	if (iterator.hasNext()) {
	    String resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);

	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(count));
	    resumptionTokenSb.append(":");
 	    resumptionTokenSb.append(Integer.toString(numRows));
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
 	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
 	    listRecordsMap.put("resumptionMap",
 				   getResumptionMap(resumptionTokenSb.toString(),
 						    numRows,
 						    0));
// 	    listRecordsMap.put("resumptionMap",
// 				   getResumptionMap(resumptionTokenSb.toString()));
	}
        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception OAIBadRequestException signals an http status code 400
     *            problem
     * @exception OAIInternalServerError signals an http status code 500
     *            problem
     */
    public Map listRecords(String resumptionToken)
      throws BadResumptionTokenException,
      OAIInternalServerError {
        purge(); // clean out old resumptionTokens
        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        
        /**********************************************************************
         * parse your resumptionToken and look it up in the resumptionResults,
         * if necessary
         **********************************************************************/
        StringTokenizer tokenizer = new StringTokenizer(resumptionToken, ":");
        String resumptionId;
        int oldCount;
        String metadataPrefix;
	int numRows;
        String set;
        try {
            resumptionId = tokenizer.nextToken();
            oldCount = Integer.parseInt(tokenizer.nextToken());
	    numRows = Integer.parseInt(tokenizer.nextToken());
            metadataPrefix = tokenizer.nextToken();
            set = tokenizer.nextToken();
            if(set.equals("null")) set = null;
        } catch (NoSuchElementException e) {
            throw new BadResumptionTokenException();
        }

	/* Get some more records from your database */
	Iterator iterator = (Iterator)resumptionResults.remove(resumptionId);
	if (iterator == null) {
	    System.out.println("GettyFileSystemOAICatalog.listRecords: reuse of old resumptionToken?");
	    iterator = fileDateMap.entrySet().iterator();
	    for (int i = 0; i<oldCount; ++i)
		iterator.next();
	}
        
	/* load the records ArrayLists. */
	int count = 0;
        ArrayList setIdentifiers = (ArrayList)setMap.get(set);
	while (count < maxListSize && iterator.hasNext()) {
	    Map.Entry entryDateMap = (Map.Entry)iterator.next();
            String path = (String)entryDateMap.getKey();
            if (set == null || setIdentifiers != null && setIdentifiers.contains(path)) {
                try {
                    HashMap nativeItem = getNativeRecord((String)entryDateMap.getKey());
                    String record = constructRecord(nativeItem, metadataPrefix);
                    records.add(record);
                    count++;
                } catch (CannotDisseminateFormatException e) {
                    /* the client hacked the resumptionToken beyond repair */
                    throw new BadResumptionTokenException();
                } catch (IOException e) {
                    /* the file is probably missing */
                    throw new BadResumptionTokenException();
                }
            }
	}
        
	/* decide if you're done. */
	if (iterator.hasNext()) {
	    resumptionId = getRSName();
	    resumptionResults.put(resumptionId, iterator);
            
	    /*****************************************************************
	     * Construct the resumptionToken String however you see fit.
	     *****************************************************************/
	    StringBuffer resumptionTokenSb = new StringBuffer();
	    resumptionTokenSb.append(resumptionId);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(oldCount + count));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(Integer.toString(numRows));
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(metadataPrefix);
	    resumptionTokenSb.append(":");
	    resumptionTokenSb.append(set);
            
	    /*****************************************************************
	     * Use the following line if you wish to include the optional
	     * resumptionToken attributes in the response. Otherwise, use the
	     * line after it that I've commented out.
	     *****************************************************************/
	    listRecordsMap.put("resumptionMap", getResumptionMap(resumptionTokenSb.toString(),
								     numRows,
								     oldCount));
	    //          listRecordsMap.put("resumptionMap",
	    //                                 getResumptionMap(resumptionTokenSb.toString()));
	}

        listRecordsMap.put("records", records.iterator());
        return listRecordsMap;
    }


    public Map listSets() throws OAIInternalServerError,
				 NoSetHierarchyException {
        if (sets.size() == 0)
            throw new NoSetHierarchyException();
         Map listSetsMap = new LinkedHashMap();
         listSetsMap.put("sets", sets.iterator());
         return listSetsMap;
    }


    public Map listSets(String resumptionToken)
      throws BadResumptionTokenException, OAIInternalServerError {
	throw new BadResumptionTokenException();
    }


    /**
     * close the repository
     */
    public void close() { }


    /**
     * Purge tokens that are older than the time-to-live.
     */
    private void purge() {
        ArrayList old = new ArrayList();
        Date      then, now = new Date();
        Iterator  keySet = resumptionResults.keySet().iterator();
        String    key;

        while (keySet.hasNext()) {
            key=(String)keySet.next();
            then=new Date(Long.parseLong(key)+getMillisecondsToLive());
            if (now.after(then)) {
                old.add(key);
            }
        }
        Iterator iterator = old.iterator();
        while (iterator.hasNext()) {
            key = (String)iterator.next();
            resumptionResults.remove(key);
        }
    }


    /**
     * Use the current date as the basis for the resumptiontoken
     *
     * @return a long integer version of the current time
     */
    private synchronized static String getRSName() {
        Date now = new Date();
        return Long.toString(now.getTime());
    }
}
/**
 * Copyright 2006 OCLC Online Computer Library Center Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or
 * agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package gov.lanl.myProject.oaicat.crosswalk;

import java.io.FileInputStream;
import java.util.Properties;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import ORG.oclc.oai.server.crosswalk.XSLTCrosswalk;
import ORG.oclc.oai.server.verb.OAIInternalServerError;

/**
 * Convert native "item" to pro. In this case, the native "item"
 * is assumed to already be formatted as an OAI <record> element,
 * with the possible exception that multiple metadataFormats may
 * be present in the <metadata> element. The "crosswalk", merely
 * involves pulling out the one that is requested.
 */
public class XSLToai_wdCrosswalk extends XSLTCrosswalk {
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public XSLToai_wdCrosswalk(Properties properties)
    throws OAIInternalServerError {
        super(properties, "http://www.lanl.gov/nisac/schemas/watchdog/ http://www.lanl.gov/nisac/schemas/oai_wd.xsd", null);
        try {
            String xsltName = properties.getProperty("XSLToai_wdCrosswalk.xsltName");
            TransformerFactory tFactory = TransformerFactory.newInstance();
            if (xsltName != null) {
                StreamSource xslSource = new StreamSource(new FileInputStream(xsltName));
                this.transformer = tFactory.newTransformer(xslSource);
            } else {
                this.transformer = tFactory.newTransformer();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new OAIInternalServerError(e.getMessage());
        }
    }
}