org.opendap.d1.DAPResourceHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.opendap.d1.DAPResourceHandler.java

Source

/**
 *  Copyright: 2014 OpenDAP, Inc.
 *
 * Author: James Gallagher <jgallagher@opendap.org>
 * 
 * This is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * You can contact OpenDAP, Inc. at PO Box 112, Saunderstown, RI. 02874-0112.
 */

package org.opendap.d1;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.io.IOUtils;
import org.dataone.client.auth.CertificateManager;
import org.dataone.configuration.Settings;
import org.dataone.mimemultipart.MultipartRequest;
import org.dataone.mimemultipart.MultipartRequestResolver;
import org.dataone.service.exceptions.BaseException;
import org.dataone.service.exceptions.InsufficientResources;
import org.dataone.service.exceptions.InvalidRequest;
import org.dataone.service.exceptions.InvalidToken;
import org.dataone.service.exceptions.NotAuthorized;
import org.dataone.service.exceptions.NotFound;
import org.dataone.service.exceptions.NotImplemented;
import org.dataone.service.exceptions.ServiceFailure;
import org.dataone.service.exceptions.SynchronizationFailed;
import org.dataone.service.types.v1.Checksum;
import org.dataone.service.types.v1.DescribeResponse;
import org.dataone.service.types.v1.Event;
import org.dataone.service.types.v1.Identifier;
import org.dataone.service.types.v1.Log;
import org.dataone.service.types.v1.Node;
import org.dataone.service.types.v1.ObjectFormatIdentifier;
import org.dataone.service.types.v1.ObjectList;
import org.dataone.service.types.v1.Session;
import org.dataone.service.types.v1.SystemMetadata;
import org.dataone.service.util.Constants;
import org.dataone.service.util.DateTimeMarshaller;
import org.dataone.service.util.ExceptionHandler;
import org.dataone.service.util.TypeMarshaller;
import org.jibx.runtime.JiBXException;
import org.opendap.d1.DatasetsDatabase.DAPD1DateParser;
import org.opendap.d1.DatasetsDatabase.DAPDatabaseException;
import org.opendap.d1.DatasetsDatabase.DatasetsDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

/**
 * @brief Handle GET, POST and HEAD requests for the DAP/D1 servlet.
 * 
 * MN REST service implementation handler
 * 
 * MNCore -- Partly 
 * ping() - GET /d1/mn/monitor/v1/ping (done)
 * log() - GET /d1/mn/v1/log (done)
 * getCapabilities() - GET /d1/mn/ and /d1/mn/v1/node (done)
 * 
 * MNRead -- Partly 
 * get() - GET /d1/mn/v1/object/PID (done)
 * getSystemMetadata() - GET /d1/mn/v1/meta/PID (done)
 * getReplica() - GET /replica/PID (done)
 * describe() - HEAD /d1/mn/v1/object/PID (done)
 * getChecksum() - GET /d1/mn/v1/checksum/PID (done)
 * listObjects() - GET /d1/mn/v1/object (done)
 * synchronizationFailed() - POST /d1/mn/v1/error
 * 
 * @author James Gallagher, after Ben Leinfelder
 */

public class DAPResourceHandler {

    /** HTTP Verb GET */
    public static final byte GET = 1;
    /** HTTP Verb POST; only used by this servlet for sync failed. */
    public static final byte POST = 2;
    /** HTTP Verb HEAD; only for the describe method */
    public static final byte HEAD = 5;

    // API Resources
    private static final String RESOURCE_OBJECTS = "object";
    private static final String RESOURCE_CHECKSUM = "checksum";
    private static final String RESOURCE_REPLICA = "replica";

    private static final String RESOURCE_META = "meta";
    private static final String RESOURCE_LOG = "log";

    private static final String RESOURCE_MONITOR = "monitor";
    private static final String RESOURCE_NODE = "node";
    private static final String RESOURCE_ERROR = "error";

    private static final String API_VERSION = "v1/"; // needs the trailing slash

    // The default number of responses for the listObjects() call
    private static int DEFAULT_COUNT = 1000;

    private static String OPENDAP_PROPERTIES = "opendap.properties";

    private static Logger log = LoggerFactory.getLogger(DAPResourceHandler.class);

    private ServletContext servletContext;

    protected HttpServletRequest request;
    protected HttpServletResponse response;

    /// An open connection to the database that holds the dataset info
    protected DatasetsDatabase db;

    /// An open connection to the log db
    protected LogDatabase logDb;

    /// The query string params
    //protected Hashtable<String, String[]> params;

    // D1 certificate-based authentication
    protected Session session;

    /* There are a number of ways to improve the performance of this
     * servlet. One is to pool the two database connections. The second
     * is to use a shared executor as illustrated below.  Explore these
     * if we get the opportunity to optimize the code. jhrg 7/22/14
     */
    /*
     // shared executor
    private static ExecutorService executor = null;
        
    static {
       // use a shared executor service with nThreads == one less than available processors
        int availableProcessors = Runtime.getRuntime().availableProcessors();
    int nThreads = availableProcessors * 1;
    nThreads--;
    nThreads = Math.max(1, nThreads);
        executor = Executors.newFixedThreadPool(nThreads);   
    }
    */

    /*
     // run it in a thread to avoid connection timeout
     Runnable runner = new Runnable() {
       @Override
       public void run() {
     try {
          MNodeService.getInstance(request).replicate(session, sysmeta, sourceNode);
     } catch (Exception e) {
        logMetacat.error("Error running replication: " + e.getMessage(), e);
        throw new RuntimeException(e.getMessage(), e);
     }
       }
    };
    // submit the task, and that's it
    executor.submit(runner);
     */

    /**
     * @brief Initializes new instance by setting servlet context,request and response.
     * 
     * This is called by DAPRestServlet.createHandler(). The resulting
     * instance is used 'handle' the GET, POST or HEAD request.
     */
    public DAPResourceHandler(ServletContext servletContext, HttpServletRequest request,
            HttpServletResponse response) throws DAPDatabaseException {

        this.servletContext = servletContext;
        this.request = request;
        this.response = response;

        try {
            Settings.augmentConfiguration(OPENDAP_PROPERTIES);
        } catch (ConfigurationException ce) {
            log.error("Failed to read the config file: {}", OPENDAP_PROPERTIES);
        }

        String dbName = Settings.getConfiguration().getString("org.opendap.d1.DatasetsDatabaseName");
        log.debug("DAPResourceHandler, Datasets Database Name: {}", dbName);

        try {
            db = new DatasetsDatabase(dbName);
            if (!db.isValid())
                throw new DAPDatabaseException("The database is not valid (" + dbName + ").");
        } catch (SQLException e) {
            throw new DAPDatabaseException("The database is not valid (" + dbName + "): " + e.getMessage());
        } catch (ClassNotFoundException e) {
            throw new DAPDatabaseException("The database is not valid (" + dbName + "): " + e.getMessage());
        }

        String logDbName = Settings.getConfiguration().getString("org.opendap.d1.LogDatabaseName");
        String nodeId = Settings.getConfiguration().getString("org.opendap.d1.nodeId");
        log.debug("DAPResourceHandler, Log database name: {}; nodeId: {}", logDbName, nodeId);
        try {
            logDb = new LogDatabase(logDbName, nodeId);
            if (!logDb.isValid())
                throw new DAPDatabaseException("The database is not valid (" + logDbName + ").");
        } catch (SQLException e) {
            throw new DAPDatabaseException("The database is not valid (" + logDbName + "): " + e.getMessage());
        } catch (ClassNotFoundException e) {
            throw new DAPDatabaseException("The database is not valid (" + logDbName + "): " + e.getMessage());
        }

    }

    /**
     * This function is called from the REST API servlet and handles each request
     * 
     * @param httpVerb (GET, HEAD, POST)
     */
    public void handle(byte httpVerb) {

        try {
            // Set the Session member; null indicates no session info
            // FIXME getSession();

            try {
                // get the resource
                String resource = request.getPathInfo();

                log.debug("handling verb {} request with resource '{}'", httpVerb, resource);

                // In the web.xml for the DAPRestServlet, I set the url pattern
                // like this: <url-pattern>/d1/mn/*</url-pattern> which means
                // that the leading '/d1/mn/' is removed by the servlet container.
                // Since this servlet implements only the 'v1' API, I've hardcoded
                // that value here. It could be read from the config file using
                // the org.opendap.d1.mnCore.serviceVersion and mnRead...
                // properties. jhrg 5/20/14
                resource = parseTrailing(resource, API_VERSION);

                log.debug("processed resource: '" + resource + "'");

                // default to node info
                if (resource == null || resource.equals("")) {
                    resource = RESOURCE_NODE;
                }

                // get the rest of the path info
                String extra = null;
                boolean status = false;

                if (resource.startsWith(RESOURCE_NODE)) {
                    log.debug("Using resource '" + RESOURCE_NODE + "'");

                    if (httpVerb == GET) {
                        // node (aka getCapabilities) response. The method uses
                        // the output stream to serialize the result and throws
                        // an
                        // exception if there's a problem.
                        sendNodeResponse();
                        status = true;
                    }
                } else if (resource.startsWith(RESOURCE_META)) {
                    log.debug("Using resource '" + RESOURCE_META + "'");

                    if (httpVerb == GET) {
                        // after the command
                        extra = parseTrailing(resource, RESOURCE_META);
                        // NB: When Tomcat is configured to allow URL encoded paths into the servlets,
                        // it does the decoding before  making the doGet(), ..., calls.
                        // However, here's code to decode the PID, if it's ever needed. jhrg 6/13/14
                        // logDAP.debug("PID before decoding: " + parseTrailing(resource, RESOURCE_META));
                        // extra = new URI(parseTrailing(resource, RESOURCE_META)).getPath();
                        // logDAP.debug("PID after decoding: " + extra);

                        sendSysmetaResponse(extra);
                        status = true;
                    }
                } else if (resource.startsWith(RESOURCE_OBJECTS)) {
                    // This is the get() call which returns SDOs and SMOs
                    // or the describe() call for the same depending on the
                    // HTTP verb (GET or HEAD)
                    log.debug("Using resource '" + RESOURCE_OBJECTS + "'");
                    // 'extra' is text that follows the command in the URL's path.
                    extra = parseTrailing(resource, RESOURCE_OBJECTS);
                    log.debug("objectId: " + extra);
                    log.debug("verb:" + httpVerb);

                    if (httpVerb == GET) {
                        if (extra == null || extra.isEmpty()) {
                            Hashtable<String, String[]> params = new Hashtable<String, String[]>();
                            initParams(params);

                            sendListObjects(params);
                        } else {
                            // In the line that follows, I cannot get Event.READ to work but I know
                            // that simple strings work.
                            logDb.addEntry(extra, request.getRemoteAddr(), request.getHeader("user-agent"),
                                    Constants.SUBJECT_PUBLIC, "read");
                            sendObject(extra);
                        }
                        status = true;
                    } else if (httpVerb == HEAD) {
                        sendDescribeObject(extra);
                        status = true;
                    }
                } else if (resource.startsWith(RESOURCE_LOG)) {
                    log.debug("Using resource '" + RESOURCE_LOG + "'");
                    // handle log events
                    if (httpVerb == GET) {
                        Hashtable<String, String[]> params = new Hashtable<String, String[]>();
                        initParams(params);

                        sendLogEntries(params);
                        status = true;
                    }
                } else if (resource.startsWith(RESOURCE_CHECKSUM)) {
                    log.debug("Using resource '" + RESOURCE_CHECKSUM + "'");
                    // handle checksum requests
                    if (httpVerb == GET) {
                        // 'extra' is text that follows the command in the URL's path.
                        extra = parseTrailing(resource, RESOURCE_CHECKSUM);
                        String algorithm = "SHA-1";
                        sendChecksum(extra, algorithm);
                        status = true;
                    }
                } else if (resource.startsWith(RESOURCE_REPLICA)) {
                    log.debug("Using resource '" + RESOURCE_REPLICA + "'");
                    // handle replica requests
                    if (httpVerb == GET) {
                        extra = parseTrailing(resource, RESOURCE_REPLICA);
                        sendReplica(extra);
                        status = true;
                    }

                } else if (resource.startsWith(RESOURCE_MONITOR)) {
                    log.debug("Processing resource '" + RESOURCE_MONITOR + "'");
                    // there are various parts to monitoring
                    if (httpVerb == GET) {
                        extra = parseTrailing(resource, RESOURCE_MONITOR);

                        // ping
                        if (extra.toLowerCase().equals("ping")) {
                            log.debug("processing ping request");

                            Date result = DAPMNodeService.getInstance(request, db, logDb).ping();
                            if (result != null) {
                                log.debug("processing ping result: " + result.toString());

                                response.setDateHeader("Date", result.getTime());
                                response.setStatus(200);

                                response.getWriter().println(result.toString());
                            } else {
                                log.debug("processing ping result: null");
                                response.setStatus(400);

                                response.getWriter().println("No response from the underlying DAP server.");
                            }

                            status = true;
                        }
                    }
                } else if (resource.startsWith(RESOURCE_ERROR)) {
                    log.debug("Processing resource '{}'", RESOURCE_ERROR);
                    SynchronizationFailed sf = collectSynchronizationFailed();
                    DAPMNodeService.getInstance(request, db, logDb).synchronizationFailed(sf);
                    status = true;
                } else {
                    throw new InvalidRequest("0000", "No resource matched for " + resource);
                }

                if (!status) {
                    throw new ServiceFailure("0000", "Unknown error while processing resource: " + resource);
                }

            } catch (BaseException be) {
                // report Exceptions as clearly as possible
                OutputStream out = null;
                try {
                    out = response.getOutputStream();
                } catch (IOException e) {
                    log.error("Could not get output stream from response", e);
                }
                serializeException(be, out);
            } catch (Exception e) {
                // report Exceptions as clearly and generically as possible
                log.error(e.getClass() + ": " + e.getMessage(), e);
                OutputStream out = null;
                try {
                    out = response.getOutputStream();
                } catch (IOException ioe) {
                    log.error("Could not get output stream from response", ioe);
                }
                ServiceFailure se = new ServiceFailure("2162", e.getMessage());
                serializeException(se, out);
            }

        } catch (Exception e) {
            response.setStatus(400);
            printError("Incorrect resource!", response);
            log.error(e.getClass() + ": " + e.getMessage(), e);
        }
    }

    /**
     * @brief Get a Session from the D1 CertificateManager
     * 
     * If there is no certificate, the Session member will be set to
     * null. This method was made simply to reduce clutter in the
     * handle() method.
     * 
     * @throws InvalidToken
     */
    @SuppressWarnings("unused")
    private void getSession() throws InvalidToken {
        // initialize the session - two options
        // #1
        // load session from certificate in request
        session = CertificateManager.getInstance().getSession(request);

        // #2
        if (session == null) {
            // check for session-based certificate from the portal
            try {
                // FIXME: configurationFileName is null.
                String configurationFileName = servletContext.getInitParameter("oa4mp:client.config.file");
                String configurationFilePath = servletContext.getRealPath(configurationFileName);
                /*
                PortalCertificateManager portalManager = new PortalCertificateManager(configurationFilePath);
                log.debug("Initialized the PortalCertificateManager using config file: "+ configurationFilePath);
                X509Certificate certificate = portalManager.getCertificate(request);
                log.debug("Retrieved certificate: " + certificate);
                PrivateKey key = portalManager.getPrivateKey(request);
                log.debug("Retrieved key: " + key);
                if (certificate != null && key != null) {
                   request.setAttribute("javax.servlet.request.X509Certificate", certificate);
                   log.debug("Added certificate to the request: " + certificate.toString());
                }
                */
                // reload session from certificate that we jsut set in request
                session = CertificateManager.getInstance().getSession(request);
            } catch (Throwable t) {
                // don't require configured OAuth4MyProxy
                log.error(t.getMessage(), t);
            }
        }
    }

    /**
     * Get the Node information. This is where the getCapabilities() response is
     * built.
     * 
     * @throws JiBXException
     * @throws IOException
     * @throws InvalidRequest
     * @throws ServiceFailure
     * @throws NotAuthorized
     * @throws NotImplemented
     */
    private void sendNodeResponse()
            throws JiBXException, IOException, NotImplemented, NotAuthorized, ServiceFailure, InvalidRequest {
        log.debug("in node...");

        Node n = DAPMNodeService.getInstance(request, db, logDb).getCapabilities();

        response.setContentType("text/xml");
        response.setStatus(200);
        TypeMarshaller.marshalTypeToOutputStream(n, response.getOutputStream());
    }

    private void sendSysmetaResponse(String extra) throws InvalidToken, NotAuthorized, NotImplemented,
            ServiceFailure, NotFound, JiBXException, IOException {
        log.debug("in sysmeta...");

        Identifier pid = new Identifier();
        pid.setValue(extra);

        SystemMetadata sm = DAPMNodeService.getInstance(request, db, logDb).getSystemMetadata(pid);

        response.setContentType("text/xml");
        response.setStatus(200);

        TypeMarshaller.marshalTypeToOutputStream(sm, response.getOutputStream());
    }

    /**
     * Get a stream back the object. This gets the DAP URL from the database, dereferences it
     * and uses IOUtils.copyLarge() to dump the result to the response's output stream.
     * 
     * @param extra The PID text
     * 
     * @throws InvalidToken
     * @throws ServiceFailure
     * @throws NotFound
     * @throws InsufficientResources
     * @throws NotAuthorized
     * @throws NotImplemented
     */
    private void sendObject(String extra)
            throws InvalidToken, ServiceFailure, NotFound, InsufficientResources, NotAuthorized, NotImplemented {
        log.debug("in object (pid: {})...", extra);

        try {
            dereferenceDapURL(extra);

        } catch (SQLException e) {
            log.error("SQL Exception: {}", e.getMessage());
            throw new ServiceFailure("1030", e.getMessage());
        } catch (DAPDatabaseException e) {
            log.error("DAP Database Exception: {}", e.getMessage());
            throw new ServiceFailure("1030", e.getMessage());
        } catch (IOException e) {
            log.error("Failed to copy a response object to the sevlet's out stream: {}", e.getMessage());
            throw new ServiceFailure("1030", e.getMessage());
        }
    }

    /**
     * Using the PID, lookup the DAP URL, dereference it and stream the result back
     * to the client using the response object's output stream. This is used by both
     * sendObject and sendReplica.
     * 
     * @param extra The D1 PID text.
     * 
     * @throws InvalidToken
     * @throws NotAuthorized
     * @throws NotImplemented
     * @throws ServiceFailure
     * @throws NotFound
     * @throws InsufficientResources
     * @throws SQLException
     * @throws DAPDatabaseException
     * @throws IOException
     */
    private void dereferenceDapURL(String extra) throws InvalidToken, NotAuthorized, NotImplemented, ServiceFailure,
            NotFound, InsufficientResources, SQLException, DAPDatabaseException, IOException {
        Identifier pid = new Identifier();
        pid.setValue(extra);

        // get(pid) throws if 'pid' is null (i.e., it will not return a null InputStream)
        InputStream in = DAPMNodeService.getInstance(request, db, logDb).get(pid);

        /* Here's how they did it in the metacat server; as with describe, optimizing
           access to the system metadata via the getSystemMetadata() call and then using
           that here would probably boost performance. jhrg 6/10/14
            
          // set the headers for the content
          String mimeType = ObjectFormatInfo.instance().getMimeType(sm.getFormatId().getValue());
          if (mimeType == null) {
             mimeType = "application/octet-stream";
          }
          String extension = ObjectFormatInfo.instance().getExtension(sm.getFormatId().getValue());
          String filename = id.getValue();
          if (extension != null) {
             filename = id.getValue() + extension;
          }
          response.setContentType(mimeType);
          response.setHeader("Content-Disposition", "inline; filename=" + filename);
            
        */

        String formatId = db.getFormatId(extra);
        String responseType = getResponseType(formatId);
        response.setContentType(responseType);

        response.setStatus(200);

        IOUtils.copyLarge(in, response.getOutputStream());
    }

    // ...could make this a map
    private String getResponseType(String formatId) {
        if (formatId.equals(DatasetsDatabase.SDO_FORMAT)) // netcdf
            return "application/octet-stream";
        else if (formatId.equals(DatasetsDatabase.SMO_FORMAT)) // iso19115
            return "text/xml";
        else if (formatId.equals(DatasetsDatabase.ORE_FORMAT)) // http ...
            return "text/xml";
        else
            return "text/plain";
    }

    /**
     * Return an object in response to a /replica request. This differs from am /object 
     * request only in how it is recorded in the log file and in the detail code for
     * the ServiceFailure objects throw when various errors happen.
     * 
     * @param extra The PID text
     * 
     * @throws InvalidToken
     * @throws ServiceFailure
     * @throws NotFound
     * @throws InsufficientResources
     * @throws NotAuthorized
     * @throws NotImplemented
     */
    private void sendReplica(String extra)
            throws InvalidToken, ServiceFailure, NotFound, InsufficientResources, NotAuthorized, NotImplemented {

        log.debug("in replica (pid: {})...", extra);

        if (!Settings.getConfiguration().getString("org.opendap.d1.nodeReplicate").equals("true"))
            throw new NotAuthorized("2182", "This host does not allow replication.");

        try {
            dereferenceDapURL(extra);

        } catch (SQLException e) {
            log.error("SQL Exception: {}", e.getMessage());
            throw new ServiceFailure("2181", e.getMessage());
        } catch (DAPDatabaseException e) {
            log.error("DAP Database Exception: {}", e.getMessage());
            throw new ServiceFailure("2181", e.getMessage());
        } catch (IOException e) {
            log.error("Failed to copy a response object to the sevlet's out stream: {}", e.getMessage());
            throw new ServiceFailure("2181", e.getMessage());
        }
    }

    /**
     * Build the response to describe(). Unlike the other calls, describe() 
     * is used with the http verb HEAD and puts metadata in the headers of the 
     * response object. Otherwise, it is a call to retrieve system metadata.
     * Also note that this never throws an exception. Instead, all exceptions
     * are trapped and returned not as XML but in the response headers.
     * 
     * @param extra The PID text
     * 
     * @throws InvalidToken
     * @throws NotAuthorized
     * @throws NotImplemented
     * @throws ServiceFailure
     * @throws NotFound
     * @throws JiBXException
     * @throws IOException
     */
    private void sendDescribeObject(String extra) {
        log.debug("in describe...");

        response.setContentType("text/xml");

        Identifier pid = new Identifier();
        pid.setValue(extra);

        DescribeResponse dr = null;
        try {
            dr = DAPMNodeService.getInstance(request, db, logDb).describe(pid);
        } catch (BaseException e) {
            response.setStatus(e.getCode());
            response.addHeader("DataONE-Exception-Name", e.getClass().getName());
            response.addHeader("DataONE-Exception-DetailCode", e.getDetail_code());
            response.addHeader("DataONE-Exception-Description", e.getDescription());
            response.addHeader("DataONE-Exception-PID", pid.getValue());
            return;
        }

        response.setStatus(200);

        //response.addHeader("pid", pid);
        response.addHeader("DataONE-Checksum",
                dr.getDataONE_Checksum().getAlgorithm() + "," + dr.getDataONE_Checksum().getValue());
        response.addHeader("Content-Length", dr.getContent_Length() + "");
        response.addHeader("Last-Modified", DateTimeMarshaller.serializeDateToUTC(dr.getLast_Modified()));
        response.addHeader("DataONE-ObjectFormat", dr.getDataONE_ObjectFormatIdentifier().getValue());
        response.addHeader("DataONE-SerialVersion", dr.getSerialVersion().toString());
    }

    /**
     * Send the list of PIDs and their associated metadata in response to a GET /object
     * request. This method extracts the arguments from the parameters parsed from the 
     * URL's query string and passes them onto the 'NodeService' class which builds the
     * actual DataONE response object. Once the response object is in hand, this method
     * serializes it.
     * 
     * @note This servlet never holds replicas, so the 'replicas' parameter to this method
     * is ignored. There's no error if it's given, because without replicas in the database
     * the response would be the same regardless of the parameter's value.
     *  
     * @param params A Hashtable of parsed query string info where the QS keys are keys and
     * the values are, well, values (arrays of strings).
     * 
     * @throws InvalidRequest
     * @throws InvalidToken
     * @throws NotAuthorized
     * @throws NotImplemented
     * @throws ServiceFailure
     * @throws JiBXException
     * @throws IOException
     */
    private void sendListObjects(Hashtable<String, String[]> params) throws InvalidRequest, InvalidToken,
            NotAuthorized, NotImplemented, ServiceFailure, JiBXException, IOException {
        // call listObjects with specified params
        Date fromDate = null;
        Date toDate = null;
        ObjectFormatIdentifier formatId = null;
        int start = 0;
        int count = DEFAULT_COUNT;

        try {
            // Hmmm Metacat used this class to parse the URL params, but it seems to assume that 
            // params should default to the local time. E.G. 2014-06-12T00:00:00 becomes
            // 2014-06-11T18:00:00 if we are at GMT-6. Our database stores time in GMT,
            // however, so I think we should perform no time zone conversion. If we need to
            // change this, make sure to also change the toDate stuff below. jhrg 6/11/14
            //
            // DateTimeMarshaller.deserializeDateToUTC(params.get("fromDate")[0]);

            if (params.get("fromDate") != null)
                fromDate = DAPD1DateParser.StringToDate(params.get("fromDate")[0]);

        } catch (Exception e) {
            log.warn("Could not parse toDate: " + params.get("fromDate")[0]);
            fromDate = null;
        }

        try {
            if (params.get("toDate") != null)
                toDate = DAPD1DateParser.StringToDate(params.get("toDate")[0]);
        } catch (Exception e) {
            log.warn("Could not parse toDate: " + params.get("toDate")[0]);
            toDate = null;
        }

        if (params.get("formatId") != null) {
            formatId = new ObjectFormatIdentifier();
            formatId.setValue(params.get("formatId")[0]);
        }

        if (params.get("start") != null)
            start = new Integer(params.get("start")[0]).intValue();

        if (params.get("count") != null)
            count = new Integer(params.get("count")[0]).intValue();

        log.debug("List Objects call, fromDate: " + fromDate + " toDate: " + toDate + " formatId: " + formatId
                + " start: " + start + " count: " + count);

        // replicas == false (never return replicas). This is ignored but we include the 
        // parameter in this method because the implementation is of an interface and we
        // must provide a version of all of its methods. jhrg 6/11/14
        ObjectList ol = DAPMNodeService.getInstance(request, db, logDb).listObjects(fromDate, toDate, formatId,
                false, start, count);

        response.setStatus(200);
        response.setContentType("text/xml");
        // Serialize and write it to the output stream
        TypeMarshaller.marshalTypeToOutputStream(ol, response.getOutputStream());
    }

    private void sendLogEntries(Hashtable<String, String[]> params) throws InvalidRequest, InvalidToken,
            NotAuthorized, NotImplemented, ServiceFailure, JiBXException, IOException {
        // call listObjects with specified params
        Date fromDate = null;
        Date toDate = null;
        Event event = null;
        String pidFilter = null;

        int start = 0;
        int count = DEFAULT_COUNT;

        try {
            if (params.get("fromDate") != null)
                fromDate = DAPD1DateParser.StringToDate(params.get("fromDate")[0]);

        } catch (Exception e) {
            log.warn("Could not parse toDate: " + params.get("fromDate")[0]);
            fromDate = null;
        }

        try {
            if (params.get("toDate") != null)
                toDate = DAPD1DateParser.StringToDate(params.get("toDate")[0]);
        } catch (Exception e) {
            log.warn("Could not parse toDate: " + params.get("toDate")[0]);
            toDate = null;
        }

        if (params.get("event") != null) {
            event = Event.convert(params.get("event")[0]);
        }

        if (params.get("pidFilter") != null) {
            pidFilter = params.get("pidFilter")[0];
        }

        if (params.get("start") != null)
            start = new Integer(params.get("start")[0]).intValue();

        if (params.get("count") != null)
            count = new Integer(params.get("count")[0]).intValue();

        log.debug("List log entries call, fromDate: " + fromDate + " toDate: " + toDate + " event: " + event
                + " idFilter: " + pidFilter + " start: " + start + " count: " + count);

        Log D1Log = DAPMNodeService.getInstance(request, db, logDb).getLogRecords(fromDate, toDate, event,
                pidFilter, start, count);

        response.setStatus(200);
        response.setContentType("text/xml");
        // Serialize and write it to the output stream
        TypeMarshaller.marshalTypeToOutputStream(D1Log, response.getOutputStream());
    }

    private void sendChecksum(String extra, String algorithm) throws InvalidRequest, InvalidToken, NotAuthorized,
            NotImplemented, ServiceFailure, NotFound, JiBXException, IOException {
        log.debug("in checksum...");

        Identifier pid = new Identifier();
        pid.setValue(extra);

        Checksum c = DAPMNodeService.getInstance(request, db, logDb).getChecksum(pid, algorithm);

        response.setContentType("text/xml");
        response.setStatus(200);

        TypeMarshaller.marshalTypeToOutputStream(c, response.getOutputStream());
    }

    /**
     * Look for the org.opendap.d1.tempDir property and use its value or
     * the default value of "/tmp" to build a File object.
     * @return A File object for the temp directory
     */
    private static File getTempDirectory() {
        String tempName = Settings.getConfiguration().getString("org.opendap.d1.tempDir");
        if (tempName == null || tempName.isEmpty())
            tempName = "/tmp";
        log.debug("Temp directory name: {}", tempName);
        return new File(tempName);
    }

    private SynchronizationFailed collectSynchronizationFailed()
            throws ServiceFailure, InvalidRequest, ParserConfigurationException, SAXException, IOException {

        // Read the incoming data from its Mime Multipart encoding
        // handle MMP inputs
        File tmpDir = getTempDirectory();
        MultipartRequestResolver mrr = new MultipartRequestResolver(tmpDir.getAbsolutePath(), 1000000000, 0);
        MultipartRequest mr = null;
        try {
            mr = mrr.resolveMultipart(request);
        } catch (Exception e) {
            throw new ServiceFailure("2161", "Could not resolve multipart: " + e.getMessage());
        }

        Map<String, File> files = mr.getMultipartFiles();
        if (files == null || files.keySet() == null) {
            throw new InvalidRequest("2163", "must have multipart file with name 'message'");
        }

        // Map<String, List<String>> multipartparams = mr.getMultipartParameters();

        File sfFile = files.get("message");
        if (sfFile == null) {
            throw new InvalidRequest("2163",
                    "Missing the required file-part 'message' from the multipart request.");
        }

        InputStream sf = new FileInputStream(sfFile);

        SynchronizationFailed syncFailed = (SynchronizationFailed) ExceptionHandler.deserializeXml(sf,
                "Error deserializing exception");

        return syncFailed;
    }

    /**
     * Extract the path info following the string 'resource'.
     * 
     * @param resource
     * @param token
     * @return
     */
    private String parseTrailing(String resource, String token) {
        // get the rest
        String extra = null;
        if (resource.indexOf(token) != -1) {
            // what comes after the token?
            extra = resource.substring(resource.indexOf(token) + token.length());
            // remove the slash
            if (extra.startsWith("/")) {
                extra = extra.substring(1);
            }
            // is there anything left?
            if (extra.length() == 0) {
                extra = null;
            }
        }
        return extra;
    }

    /**
     * copies request parameters to a hash table which is given as argument to
     * native metacat handler functions
     */
    @SuppressWarnings({ "rawtypes" })
    private void initParams(Hashtable<String, String[]> params) {

        String name = null;
        String[] value = null;
        Enumeration paramlist = request.getParameterNames();
        while (paramlist.hasMoreElements()) {
            name = (String) paramlist.nextElement();
            value = request.getParameterValues(name);
            params.put(name, value);
        }
    }

    /**
     * Prints xml response
     * 
     * @param message
     *            Message to be displayed
     * @param response
     *            Servlet response that xml message will be printed
     *
     */
    private void printError(String message, HttpServletResponse response) {
        try {
            log.error("D1ResourceHandler: Printing error to servlet response: " + message);
            PrintWriter out = response.getWriter();
            response.setContentType("text/xml");
            out.println("<?xml version=\"1.0\"?>");
            out.println("<error>");
            out.println(message);
            out.println("</error>");
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * serialize a D1 exception using jibx
     * 
     * @param e
     * @param out
     */
    private void serializeException(BaseException e, OutputStream out) {
        response.setContentType("text/xml");
        response.setStatus(e.getCode());

        log.error("D1ResourceHandler: Serializing exception with code " + e.getCode() + ": " + e.getMessage());
        e.printStackTrace();

        try {
            IOUtils.write(e.serialize(BaseException.FMT_XML), out);
        } catch (IOException e1) {
            log.error("Error writing exception to stream. " + e1.getMessage());
        }
    }
}