/**
 *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://purl.oclc.org/oclc/research/ORPL/.  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 DummyOAICatalog.java
 *The Initial Developer of the Original Code is Jeff Young.
 *Portions created by François Jannin are
 *Copyright (C) ESUP-PORTAIL Consortium. All Rights Reserved.
 *
 */

package org.injac.oai.server.catalog;

import java.io.File;

import java.io.IOException;
import java.net.URLDecoder;

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 java.util.Enumeration;

import org.apache.commons.httpclient.HttpException;

import org.apache.commons.httpclient.HttpURL;

import org.apache.log4j.Logger;

import org.apache.webdav.lib.WebdavResource;
import org.apache.webdav.lib.methods.DepthSupport;
import org.apache.webdav.lib.methods.PropFindMethod;
import org.apache.webdav.lib.methods.XMLResponseMethodBase;
import org.apache.webdav.lib.BaseProperty;
import org.injac.oai.util.DateChecker;

import org.w3c.dom.Element;
import org.injac.oai.server.catalog.set.*;
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;
import org.oclc.oai.server.verb.NoRecordsMatchException;
import org.oclc.oai.server.verb.NoSetHierarchyException;
import org.oclc.oai.server.verb.OAIInternalServerError;

/**
 * WebdavOAICatalog implements the AbstractCatalog interface, making
 * OAI-harvestable an inJAC WEBDAV repository. Complete adaptation includes
 * WebdavRecordFactory and WebdavCrosswalk classes
 * 
 * @author François Jannin, ENSEEIHT
 */
public class WebdavOAICatalog extends AbstractCatalog {
	/**
	 * maximum number of entries to return for ListRecords and ListIdentifiers
	 */
	private static int maxListSize;

	/**
	 * String tested for filtering metadatas by namespaces
	 */
	private static String URI_FILTER = "INJAC:";

	private static String TAGNAME_FILTER = "D:modificationdate D:creationdate";

	/**
	 * Static logger
	 */
	static Logger logger = Logger.getLogger(WebdavOAICatalog.class);

	/**
	 * Connection with session management
	 * 
	 */
	private WebdavResource webdavResource;

	/**
	 * webdav url
	 */
	private String webdavpath;

	/**
	 * webdav login
	 */
	private String login;

	/**
	 * webdav password
	 */
	private String password;

	/**
	 * Base directory for configuration files...
	 */
	private String baseDir;

	/**
	 * upper root for inJAC spaces
	 */
	private String upnode;

	/**
	 * URL to inJAC's rendering engine
	 */
	private String renderURL;

	/**
	 * URI context to inJAC's rendering engine
	 */
	private String renderContext;

	/**
	 * extensions for HTML rendered files
	 */
	private String intDocs;

	/**
	 * file name for sets definitions
	 */
	private String setsFile;

	/**
	 * pending resumption tokens
	 */
	private HashMap resumptionResults = new HashMap();

	private String[] setSpecs = { "" };

	private SetManager setManager = null;

	/**
	 * Construct a WebdavOAICatalog object
	 * 
	 * @param properties
	 *            a properties object containing initialization parameters
	 * @exception IOException
	 *                an I/O error occurred during webdav connection.
	 */
	public WebdavOAICatalog(Properties properties) throws IOException {
		logger.info("WebdavOAICatalog constructor");
		String maxListSize = properties
				.getProperty("WebdavOAICatalog.maxListSize");
		if (maxListSize == null) {
			throw new IllegalArgumentException(
					"WebdavOAICatalog.maxListSize is missing from the properties file");
		} else {
			WebdavOAICatalog.maxListSize = Integer.parseInt(maxListSize);
		}

		webdavpath = properties.getProperty("WebdavOAICatalog.url");
		if (webdavpath == null)
			throw new IllegalArgumentException("WebdavOAICatalog."
					+ "webdav url is missing from the properties file");
		login = properties.getProperty("WebdavOAICatalog.login");

		password = properties.getProperty("WebdavOAICatalog.password");
		baseDir = properties.getProperty("WebdavOAICatalog.baseDir");
		setsFile = properties.getProperty("WebdavOAICatalog.setsFile");

		upnode = properties.getProperty("Injac.upnode");
		renderURL = properties.getProperty("Injac.renderURL");
		renderContext = properties.getProperty("Injac.renderContext");

		intDocs = properties.getProperty("intDocs");

		if ((setsFile == null) || (setsFile.equals(""))) {
			setsFile = baseDir + "/WEB-INF/setspecs.xml";
		}
		if ((login == null) ^ (password == null))
			throw new IllegalArgumentException(
					"WebdavOAICatalog."
							+ "Both login AND password must be set, or none of them, in the properties file");
		/***********************************************************************
		 * create Webdav resource
		 **********************************************************************/
		createWebdavResource();
		initSets();

	}

	/**
	 * Retrieve a list of schemaLocation values associated with the specified
	 * identifier.
	 * 
	 * @param identifier
	 *            the OAI identifier
	 * @return a Vector containing schemaLocation Strings
	 * @exception OAIInternalServerError
	 *                signals an http status code 500 problem
	 * @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 OAIInternalServerError, IdDoesNotExistException,
			NoMetadataFormatsException {
		/***********************************************************************
		 * Retrieve the specified native item from webdav repository
		 **********************************************************************/
		Object nativeItem = getNativeItem(identifier);

		/*
		 * 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.
	 * @exception OAIInternalServerError
	 *                signals an http status code 500 problem
	 * @exception NoSetHierarchyException
	 *                the repository doesn't support sets.
	 * @exception CannotDisseminateFormatException
	 *                the metadata format specified is not supported by your
	 *                repository.
	 */
	public Map listIdentifiers(String from, String until, String set,
			String metadataPrefix) throws OAIInternalServerError,
			NoSetHierarchyException, CannotDisseminateFormatException,
			NoRecordsMatchException {
		purge(); // clean out old resumptionTokens
		Map listIdentifiersMap = new HashMap();
		ArrayList headers = new ArrayList();
		ArrayList identifiers = new ArrayList();

		/* Get matching records from webdav repository */
		Object[] nativeItems = getNativeItems(from, until, set, metadataPrefix);
		if (nativeItems == null)
			throw new NoRecordsMatchException();
		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.
	 * @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;
		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 OAIInternalServerError
	 *                signals an http status code 500 problem
	 * @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 OAIInternalServerError, CannotDisseminateFormatException,
			IdDoesNotExistException {
		Object nativeItem = getNativeItem(identifier);
		if (nativeItem == null)
			throw new IdDoesNotExistException(identifier);
		/***********************************************************************
		 * END OF CUSTOM CODE SECTION
		 **********************************************************************/
		return constructRecord(nativeItem, metadataPrefix);
	}

	/***************************************************************************
	 * @return a hashmap containing all webdav properties for a given item's
	 *         identifier
	 * 
	 * formerly : return an Enum of Properties as an Object
	 **************************************************************************/

	private Object getNativeItem(String identifier) {

		logger.debug("WebdavOAICatalog::getNativeItem : " + identifier);
		logger.debug("\n\t identifier : " + identifier);
		String localIdentifier = getRecordFactory().fromOAIIdentifier(
				identifier);
		logger.debug("\n\t local identifier : " + localIdentifier);

		int index = localIdentifier.indexOf("/slide/");
		// no slide context case
		if (index == -1)
			index = localIdentifier.indexOf("/files");

		String proppath = localIdentifier.substring(index);
		logger.debug("\n\t proppath : " + proppath);
		if (webdavResource != null) {
			Enumeration props;
			try {
				PropFindMethod method = new PropFindMethod(proppath);
				webdavResource.retrieveSessionInstance().executeMethod(method);
				props = method.getResponseProperties(proppath);
			} catch (Exception ex) {
				logger.error(ex.getMessage());
				return null;
			}
			// transform into hashmap
			return propsToHashMap(props);
		} else
			return null;

	}

	private boolean isValidDocument(HashMap hash) {
		String type = (String) hash.get("injac:injac-type");
		if (type == null || !type.equals("document"))
			return false;
		else {
			String begin = (String) hash.get("injac:publication-date-begin");
			String end = (String) hash.get("injac:publication-date-end");

			boolean published = ((String) hash.get("injac:document-state"))
					.equals("published");
			boolean check = true;
			if (published && (begin != null && end != null)) {
				check = DateChecker.checkDate(begin, end);
			}
			return check && published;
		}
	}

	/**
	 * Convert webdav properties enumeration to hashmap
	 * 
	 * @param props
	 *            the webdav properties
	 * @return HashMap containing all metadatas for an item
	 */
	private HashMap propsToHashMap(Enumeration props) {
		if (props == null)
			return null;

		HashMap hash = new HashMap();
		String url = "";
		logger.debug("START propsToHashMap");
		while (props.hasMoreElements()) {
			Object resp_element = props.nextElement();
			BaseProperty prop = (BaseProperty) resp_element;
			url = prop.getOwningURL();

			Element el = prop.getElement();
			String tagname = el.getNodeName();
			String prefix = el.getPrefix();
			String uri = el.getNamespaceURI();

			String value = "";
			// filter namespace
			if ((URI_FILTER.indexOf(uri) != -1)
					|| (TAGNAME_FILTER.indexOf(tagname) != -1)) {

				if (el.hasChildNodes())
					value = el.getFirstChild().getNodeValue();
				String mapname = tagname.substring(tagname.indexOf(":") + 1);
				try {
					value = URLDecoder.decode(value, "UTF-8");

				} catch (Exception e) {
					try {
						value = URLDecoder.decode(value, "iso-8859-1");
					} catch (Exception e2) {/* NOP */
					}

				}
				// append prefix found in namespace ie INJAC:
				if (prefix == null)
					tagname = uri.toLowerCase() + tagname;
				hash.put(tagname, value);
				logger.debug("prefix : " + prefix + " ns URI :" + uri
						+ " [ key=" + tagname + " value=" + value + " ]");
			}

		}
		logger.debug("END propsToHashMap");

		int index = url.indexOf("/" + upnode + "/") + upnode.length() + 1;

		// extract root webdav url for source
		// String prefixURL = url.substring(0, url.indexOf('/',1)+1);

		// concat with webdav relative path
		// url=webdavpath.substring(0, webdavpath.indexOf(prefixURL))+url;
		url = webdavpath + url.substring(index);

		// add render url for document nodes
		String rootfile = (String) hash.get("injac:root-file-name");
		if (rootfile != null) {
			hash.put("renderurl", getRenderURL(url, rootfile));
			hash.put("webdavurl", url + "/" + rootfile);
		} else {
			hash.put("webdavurl", url);
		}

		logger.debug("webdavurl : " + url);
		// set datestamp
		String datestamp = (String) hash.get("D:modificationdate");
		hash.put("datestamp", datestamp);
		logger.debug("datestamp : " + datestamp);
		// set identifier
		hash.put("local-identifier", getRecordFactory()
				.getLocalIdentifier(hash));
		// get setSpecs for item
		if (setManager != null) {
			ArrayList setspecs = setManager.getSetSpecsFromItem(hash);
			if (setspecs != null)
				hash.put("setSpecs", setspecs);
		}

		return hash;

	}

	/**
	 * Translate WEBDAV URL and filename into rendering URL
	 * 
	 * @param path :
	 *            webdav url
	 * @param file :
	 *            file name
	 * @return rendering URL
	 */
	protected String getRenderURL(String path, String file) {
		if (upnode.equals("") || renderURL.equals("")
				|| renderContext.equals("")) {
			return path;
		} else {
			String subPath = path.substring(path.indexOf(upnode)
					+ upnode.length());
			String renderURI = "/ext";
			String suffix = null;
			int index = file.lastIndexOf('.');
			if (index != -1) {
				suffix = file.substring(index + 1);

			}
			if ((suffix != null) && (intDocs.indexOf(suffix) != -1)) {
				renderURI = renderContext;
				path = renderURL + renderURI + subPath + "?file=" + file;
			} else {
				path = renderURL + renderURI + subPath + "/" + file;
			}

			return path;
		}

	}

	/**
	 * 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 NoSetHierarchyException
	 *                The repository doesn't support sets.
	 * @exception CannotDisseminateFormatException
	 *                the metadataPrefix isn't supported by the item.
	 */
	public Map listRecords(String from, String until, String set,
			String metadataPrefix) throws OAIInternalServerError,
			NoSetHierarchyException, CannotDisseminateFormatException,
			NoRecordsMatchException {
		purge(); // clean out old resumptionTokens
		Map listRecordsMap = new HashMap();
		ArrayList records = new ArrayList();
		/* Get some records from webdav repository */
		Object[] nativeItems = getNativeItems(from, until, set, metadataPrefix);
		if (nativeItems == null)
			throw new NoRecordsMatchException();
		int count = 0;

		/* load the records ArrayList */
		int realcount = 0;
		while (realcount < maxListSize && count < nativeItems.length) {
			try {
				String record = null;
				record = constructRecord(nativeItems[count++], metadataPrefix);
				records.add(record);
				realcount++;
			} catch (CannotDisseminateFormatException cdfe) {
				// NOP
			}
		}
		/* 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.
			 ******************************************************************/
			listRecordsMap.put("resumptionMap", getResumptionMap(
					resumptionTokenSb.toString(), nativeItems.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 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;
		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
	 * @exception OAIInternalServerError
	 *                signals an http status code 500 problem
	 */
	public Map listSets() throws NoSetHierarchyException,
			OAIInternalServerError {
		purge(); // clean out old resumptionTokens
		Map listSetsMap = new HashMap();
		ArrayList sets = new ArrayList();
		/* decide which sets you're going to support */
		String[] dbSets = getSets();
		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.
	 * @exception OAIInternalServerError
	 *                signals an http status code 500 problem
	 */
	public Map listSets(String resumptionToken)
			throws BadResumptionTokenException, OAIInternalServerError {
		Map listSetsMap = new HashMap();
		ArrayList sets = 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;
		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;
	}

	private void initSets() {

		setSpecs = new String[] {};
		File setFile = new File(setsFile);
		if (logger.isDebugEnabled()) {
			logger.debug("file path for sets : " + setFile.getPath());
		}
		if (setFile.exists()) {
			setManager = new SetManager(setFile);
			setSpecs = setManager.loadSetsFromFile(setFile);
		}

	}

	private String[] getSets() {
		return setSpecs;
	}

	/**
	 * 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());
	}

	private Object[] getNativeItems(String from, String until, String set,
			String metadataPrefix) {
		Object[] result = null;
		if (webdavResource != null) {
			// 1. Fetch responses
			Enumeration responses = null;
			try {
				PropFindMethod method = new PropFindMethod(webdavpath,
						DepthSupport.DEPTH_INFINITY);
				webdavResource.retrieveSessionInstance().executeMethod(method);
				responses = method.getResponses();
				logger.info("WebdavOAICatalog::getNativeItems getResponses ok");
			} catch (Exception e) {
				logger
						.error("WebdavOAICatalog::getNativeItems : getResponses error:\npath: "
								+ webdavpath + "\n" + e.getMessage());
				return null;
			}

			// 2. Make object array
			try {
				Vector vresult = new Vector();
				int i = 0;

				while (responses.hasMoreElements()) {
					Object resp_element = responses.nextElement();
					logger.debug("class resp_element " + i + " : "
							+ resp_element.getClass());// +"
														// resp_element.toString
														// :
														// "+resp_element.toString());
					XMLResponseMethodBase.Response resp = (XMLResponseMethodBase.Response) resp_element;
					HashMap hash = propsToHashMap(resp.getProperties());
					boolean addResult = isValidDocument(hash);
					if (set != null)
						addResult = addResult && isSetSpecDocument(hash, set);
					if (from != null)
						addResult = addResult && isFromDocument(hash, from);
					if (until != null)
						addResult = addResult && isUntilDocument(hash, until);
					if (addResult) {
						vresult.add(hash);
						i++;
					}
				}
				result = vresult.toArray();
				logger.info("WebdavOAICatalog::getNativeItems :" + i
						+ " documents");

			} catch (Exception ex) {
				logger.error(ex.toString());
				return null;
			}
			return result;
		} else
			return null;
	}

	/**
	 * Test if item's datestamp is more recent than a UTC Date Dates are
	 * UTCDateTime format
	 * 
	 * @param item
	 *            native item
	 * @param from
	 *            UTC date
	 * @return true if condition fulfilled
	 */
	private boolean isFromDocument(Object item, String from) {
		String datestamp = getRecordFactory().getDatestamp(item);
		return from.compareTo(datestamp) < 0;
	}

	/**
	 * Test if item's datestamp is earlier than a given Date Dates are
	 * UTCDateTime format
	 * 
	 * @param item
	 *            native item
	 * @param until
	 *            UTC date
	 * @return true if condition fulfilled
	 */
	private boolean isUntilDocument(Object item, String until) {
		String datestamp = getRecordFactory().getDatestamp(item);
		return until.compareTo(datestamp) > 0;
	}

	/**
	 * Test if item is within a setSpec
	 * 
	 * @param item
	 *            native item
	 * @param setSpec
	 * @return true if setSpecs from item contains param set
	 */
	private boolean isSetSpecDocument(Object item, String setSpec) {
		if ((setSpec == null) || setSpec.equals(""))
			return true;
		logger.debug("isSetSpecDocument setSpec wanted : " + setSpec);
		Iterator it = getRecordFactory().getSetSpecs(item);
		if (it == null)
			return false;
		while (it.hasNext()) {
			String ItemSetSpec = (String) it.next();
			logger.debug("\t\t setSpec found : " + ItemSetSpec);
			if (ItemSetSpec.equalsIgnoreCase(setSpec))
				return true;
		}
		return false;

	}

	private void createWebdavResource() throws HttpException, IOException {

		logger.debug("WebdavOAICatalog:createWebdavResource : path: "
				+ webdavpath + " login: " + login);
		HttpURL httpUrl = null;
		// try{
		if ((login != null) && (password != null)) {
			httpUrl = new HttpURL(webdavpath);
			httpUrl.setUserinfo(login, password);
		} else {
			httpUrl = new HttpURL(webdavpath);

		}
		webdavResource = new WebdavResource(httpUrl);

		logger
				.debug("WebdavOAICatalog:createWebdavResource : new webdavresource created.");
		/*
		 * }catch(HttpException hex) {
		 * logger.error("WebdavOAICatalog:createWebdavResource
		 * :"+hex.getMessage()+"\n"+hex.getCause()+"\n"+hex.getStackTrace());
		 * 
		 * }catch(IOException ioex) {
		 * logger.error("WebdavOAICatalog:createWebdavResource
		 * :"+ioex.getMessage()+"\n"+ioex.getCause()+"\n"+ioex.getStackTrace());
		 * 
		 * }catch(Exception ex) {
		 * logger.error("WebdavOAICatalog:createWebdavResource
		 * :"+ex.getMessage()+"\n"+ex.getCause()+"\n"+ex.getStackTrace());
		 *  }
		 */

	}

}
