org.fcrepo.server.access.DefaultAccess.java Source code

Java tutorial

Introduction

Here is the source code for org.fcrepo.server.access.DefaultAccess.java

Source

/* The contents of this file are subject to the license and copyright terms
 * detailed in the license directory at the root of the source tree (also
 * available online at http://fedora-commons.org/license/).
 */
package org.fcrepo.server.access;

import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.util.DateUtil;
import org.apache.http.HttpHeaders;
import org.fcrepo.common.Constants;
import org.fcrepo.common.Models;
import org.fcrepo.server.Context;
import org.fcrepo.server.Module;
import org.fcrepo.server.Server;
import org.fcrepo.server.access.dissemination.DisseminationService;
import org.fcrepo.server.errors.DatastreamNotFoundException;
import org.fcrepo.server.errors.DisseminationException;
import org.fcrepo.server.errors.DisseminatorNotFoundException;
import org.fcrepo.server.errors.InvalidUserParmException;
import org.fcrepo.server.errors.MethodNotFoundException;
import org.fcrepo.server.errors.ModuleInitializationException;
import org.fcrepo.server.errors.ServerException;
import org.fcrepo.server.search.FieldSearchQuery;
import org.fcrepo.server.search.FieldSearchResult;
import org.fcrepo.server.security.Authorization;
import org.fcrepo.server.storage.ContentManagerParams;
import org.fcrepo.server.storage.DOManager;
import org.fcrepo.server.storage.DOReader;
import org.fcrepo.server.storage.ExternalContentManager;
import org.fcrepo.server.storage.ServiceDefinitionReader;
import org.fcrepo.server.storage.ServiceDeploymentReader;
import org.fcrepo.server.storage.types.Datastream;
import org.fcrepo.server.storage.types.DatastreamDef;
import org.fcrepo.server.storage.types.DatastreamReferencedContent;
import org.fcrepo.server.storage.types.DeploymentDSBindRule;
import org.fcrepo.server.storage.types.DeploymentDSBindSpec;
import org.fcrepo.server.storage.types.DisseminationBindingInfo;
import org.fcrepo.server.storage.types.MIMETypedStream;
import org.fcrepo.server.storage.types.MethodDef;
import org.fcrepo.server.storage.types.MethodDefOperationBind;
import org.fcrepo.server.storage.types.MethodParmDef;
import org.fcrepo.server.storage.types.ObjectMethodsDef;
import org.fcrepo.server.storage.types.Property;
import org.fcrepo.server.storage.types.RelationshipTuple;
import org.fcrepo.server.utilities.ServerUtility;
import org.fcrepo.utilities.DateUtility;
import org.fcrepo.utilities.io.NullInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Access Module, providing support for the Fedora Access subsystem.
 *
 * @author Ross Wayland
 * @version $Id$
 */
public class DefaultAccess extends Module implements Access {

    private static final Logger logger = LoggerFactory.getLogger(DefaultAccess.class);

    private static final MethodParmDef[] METHOD_PARM_DEF_TYPE = new MethodParmDef[0];

    /** Current DOManager of the Fedora server. */
    private DOManager m_manager;

    /** OAI Provider domain name, for the describe request's identifier info. */
    private String m_repositoryDomainName;

    /** Dynamic Access Module */
    // FIXME is this the right way to associate the dynamic access module???
    private DynamicAccessModule m_dynamicAccess;

    private ExternalContentManager m_externalContentManager;

    private Authorization m_authorizationModule;

    /**
     * <p>
     * Creates and initializes the Access Module. When the server is starting
     * up, this is invoked as part of the initialization process.
     * </p>
     *
     * @param moduleParameters
     *        A pre-loaded Map of name-value pairs comprising the intended
     *        configuration of this Module.
     * @param server
     *        The <code>Server</code> instance.
     * @param role
     *        The role this module fulfills, a java class name.
     * @throws ModuleInitializationException
     *         If initilization values are invalid or initialization fails for
     *         some other reason.
     */
    public DefaultAccess(Map<String, String> moduleParameters, Server server, String role)
            throws ModuleInitializationException {
        super(moduleParameters, server, role);
    }

    /**
     * <p>
     * Initializes the module.
     * </p>
     *
     * @throws ModuleInitializationException
     *         If the module cannot be initialized.
     */
    @Override
    public void initModule() throws ModuleInitializationException {

        String dsMediation = getParameter("doMediateDatastreams");
        if (dsMediation == null) {
            throw new ModuleInitializationException("doMediateDatastreams parameter must be specified.", getRole());
        }
    }

    @Override
    public void postInitModule() throws ModuleInitializationException {
        // get ref to DOManager
        m_manager = (DOManager) getServer().getModule("org.fcrepo.server.storage.DOManager");
        if (m_manager == null) {
            throw new ModuleInitializationException("Can't get a DOManager " + "from Server.getModule", getRole());
        }
        // get ref to DynamicAccess module
        m_dynamicAccess = (DynamicAccessModule) getServer().getModule("org.fcrepo.server.access.DynamicAccess");

        // get ref to ExternalContentManager
        m_externalContentManager = (ExternalContentManager) getServer()
                .getModule("org.fcrepo.server.storage.ExternalContentManager");

        // get ref to OAIProvider, for repositoryDomainName param for oai info
        Module oaiProvider = getServer().getModule("org.fcrepo.oai.OAIProvider");
        if (oaiProvider == null) {
            throw new ModuleInitializationException("DefaultAccess module requires that the server "
                    + "has an OAIProvider module configured so that it can get the repositoryDomainName parameter.",
                    getRole());
        }
        m_repositoryDomainName = oaiProvider.getParameter("repositoryDomainName");
        if (m_repositoryDomainName == null) {
            throw new ModuleInitializationException("DefaultAccess module requires that the OAIProvider "
                    + "module has the repositoryDomainName parameter specified.", getRole());
        }

        m_authorizationModule = getServer().getBean("org.fcrepo.server.security.Authorization",
                Authorization.class);
        if (m_authorizationModule == null) {
            throw new ModuleInitializationException(
                    "Can't get an Authorization module (in default access) from Server.getModule", getRole());
        }

    }

    private static final Hashtable<String, String> accessActionAttributes = new Hashtable<String, String>();
    static {
        accessActionAttributes.put("api", "apia");
    }

    /**
     * <p>
     * Disseminates the content produced by executing the specified method of
     * the associated deployment object of the specified digital object.
     * </p>
     *
     * @param context
     *        The context of this request.
     * @param PID
     *        The persistent identifier of the digital object.
     * @param sDefPID
     *        The persistent identifier of the Service Definition object.
     * @param methodName
     *        The name of the method to be executed.
     * @param userParms
     *        An array of user-supplied method parameters consisting of
     *        name/value pairs.
     * @param asOfDateTime
     *        The versioning datetime stamp.
     * @return A MIME-typed stream containing the result of the dissemination.
     * @throws ServerException
     *         If any type of error occurred fulfilling the request.
     */
    @Override
    public MIMETypedStream getDissemination(Context context, String PID, String sDefPID, String methodName,
            Property[] userParms, Date asOfDateTime) throws ServerException {
        PID = Server.getPID(PID).toString();
        sDefPID = Server.getPID(sDefPID).toString();
        long initStartTime = (logger.isDebugEnabled()) ? System.currentTimeMillis() : 0;
        long startTime = initStartTime;
        long stopTime;
        long interval;
        ServiceDeploymentReader deploymentReader = null;

        DOReader reader = m_manager.getReader(asOfDateTime == null, context, PID);
        String authzAux_objState = reader.GetObjectState();

        // DYNAMIC!! If service deployment is defined as dynamic, then
        // perform the dissemination via the DynamicAccess module.
        if (m_dynamicAccess.isDynamicService(context, PID, sDefPID)) {
            m_authorizationModule.enforceGetDissemination(context, PID, sDefPID, methodName, asOfDateTime,
                    authzAux_objState, "A", "fedora-system:4", "A", "A");
            MIMETypedStream retVal = m_dynamicAccess.getDissemination(context, PID, sDefPID, methodName, userParms,
                    asOfDateTime);
            if (logger.isDebugEnabled()) {
                stopTime = System.currentTimeMillis();
                interval = stopTime - startTime;
                logger.debug("Roundtrip DynamicDisseminator: {} milliseconds.", interval);
            }
            return retVal;
        }

        /*
         * Find the service deployment that is contractor for a model this
         * object has, and deploys the requested service. If object<->model
         * mappings are ever stored in the registry, this may be simplified.
         */
        String serviceDeploymentPID = null;
        for (String cModelURI : reader.getContentModels()) {
            String cModelPID = cModelURI.substring("info:fedora/".length());

            String foundDeploymentPID = m_manager.lookupDeploymentForCModel(cModelPID, sDefPID);

            if (foundDeploymentPID != null) {
                if (serviceDeploymentPID != null && !foundDeploymentPID.equals(serviceDeploymentPID)) {
                    throw new DisseminationException("More than one deployment (" + foundDeploymentPID + ", "
                            + serviceDeploymentPID + ") found for service " + sDefPID + " in model " + cModelPID);

                }

                serviceDeploymentPID = foundDeploymentPID;
            } else {
                logger.debug("No deployment for ({}, {})", cModelPID, sDefPID);
            }
        }

        if (serviceDeploymentPID != null) {
            deploymentReader = m_manager.getServiceDeploymentReader(false, context, serviceDeploymentPID);
        }

        ServiceDefinitionReader sDefReader = m_manager.getServiceDefinitionReader(asOfDateTime == null, context,
                sDefPID);

        String authzAux_sdefState = sDefReader.GetObjectState();

        String authzAux_dissState = "unknown";

        /*
         * if reader is null, it means that no suitable deployments have been
         * found. This can happen if (a), the object does not have any models
         * that have that service, or (b) the object has a suitable model, but
         * no implementation of that service has been deployed. We do a bit of
         * checking here to determine which case this represents, as the error
         * message could be very useful.
         */
        if (deploymentReader == null) {

            boolean suitableModelFound = false;
            String cModelPID = null;
            String message = null;

            models: for (String cm : reader.getContentModels()) {
                cModelPID = cm.substring(12);

                /* Skip over system models */
                if (Models.contains("info:fedora/" + cModelPID)) {
                    continue;
                }

                /* Open up each model and peek at its sDefs for a match */
                for (RelationshipTuple r : m_manager.getReader(false, context, cModelPID)
                        .getRelationships(MODEL.HAS_SERVICE, null)) {
                    if (sDefPID.equals(r.getObjectPID())) {
                        suitableModelFound = true;
                        break models;
                    }
                }
            }

            if (suitableModelFound) {
                message = "Unable to find deployment for service " + sDefPID + " on " + reader.GetObjectPID()
                        + " in model " + cModelPID;
            } else {
                message = reader.GetObjectPID() + " does not have a model with service " + sDefPID;
            }
            throw new DisseminatorNotFoundException(message);
        }
        if (logger.isDebugEnabled()) {
            stopTime = System.currentTimeMillis();
            interval = stopTime - startTime;
            logger.debug("Roundtrip Looping Diss: {} milliseconds.", interval);
        }
        // Check deployment object state
        String authzAux_sDepState = deploymentReader.GetObjectState();
        String authzAux_sDepPID = deploymentReader.GetObjectPID();

        m_authorizationModule.enforceGetDissemination(context, PID, sDefPID, methodName, asOfDateTime,
                authzAux_objState, authzAux_sdefState, authzAux_sDepPID, authzAux_sDepState, authzAux_dissState);

        // Get method parms
        Hashtable<String, String> h_userParms = new Hashtable<String, String>();
        MIMETypedStream dissemination = null;
        MethodParmDef[] defaultMethodParms = null;

        startTime = new Date().getTime();
        // Put any user-supplied method parameters into hash table
        if (userParms != null) {
            for (Property element : userParms) {
                h_userParms.put(element.name, element.value);
            }
        }
        // Validate user-supplied parameters
        validateUserParms(context, PID, sDefPID, deploymentReader, methodName, h_userParms, asOfDateTime);

        if (logger.isDebugEnabled()) {
            stopTime = System.currentTimeMillis();
            interval = stopTime - startTime;
            logger.debug("Roundtrip Get/Validate User Parms:  milliseconds.", interval);

            startTime = System.currentTimeMillis();
        }
        // SDP: GET INFO FROM DEPLOYMENT READER:
        // Add any default method parameters to validated user parm list
        defaultMethodParms = deploymentReader.getServiceMethodParms(methodName, asOfDateTime);
        for (int i = 0; i < defaultMethodParms.length; i++) {
            if (!defaultMethodParms[i].parmType.equals(MethodParmDef.DATASTREAM_INPUT)) {
                if (!h_userParms.containsKey(defaultMethodParms[i].parmName)) {
                    logger.debug("addedDefaultName: {}", defaultMethodParms[i].parmName);
                    String pdv = defaultMethodParms[i].parmDefaultValue;
                    try {
                        // here we make sure the PID is decoded so that encoding
                        // later won't doubly-encode it
                        if (pdv.equalsIgnoreCase("$pid")) {
                            pdv = URLDecoder.decode(PID, "UTF-8");
                        } else if (pdv.equalsIgnoreCase("$objuri")) {
                            pdv = "info:fedora/" + URLDecoder.decode(PID, "UTF-8");
                        }
                    } catch (UnsupportedEncodingException uee) {
                    }
                    logger.debug("addedDefaultValue: {}", pdv);
                    h_userParms.put(defaultMethodParms[i].parmName, pdv);
                }
            }
        }

        if (logger.isDebugEnabled()) {
            stopTime = System.currentTimeMillis();
            interval = stopTime - startTime;
            logger.debug("Roundtrip Get Deployment Parms: {} milliseconds.", interval);
            startTime = System.currentTimeMillis();
        }

        DisseminationBindingInfo[] dissBindInfo;
        dissBindInfo = getDisseminationBindingInfo(context, reader, deploymentReader, methodName, asOfDateTime);

        // Assemble and execute the dissemination request from the binding info.
        DisseminationService dissService = new DisseminationService(getServer());
        dissemination = dissService.assembleDissemination(context, PID, h_userParms, dissBindInfo, authzAux_sDepPID,
                deploymentReader, methodName);

        if (logger.isDebugEnabled()) {
            stopTime = System.currentTimeMillis();
            interval = stopTime - startTime;
            logger.debug("Roundtrip Assemble Dissemination: {} milliseconds.", interval);

            interval = stopTime - initStartTime;
            logger.debug("Roundtrip GetDissemination: {} milliseconds.", interval);
        }
        return dissemination;
    }

    private DisseminationBindingInfo[] getDisseminationBindingInfo(Context context, DOReader dObj,
            ServiceDeploymentReader bmReader, String methodName, Date versDateTime)
            throws MethodNotFoundException, ServerException {

        // The sDep reader provides information about the service and params.
        MethodParmDef[] methodParms = bmReader.getServiceMethodParms(methodName, versDateTime);
        // Find the operation bindings for the method in question
        MethodDefOperationBind[] opBindings = bmReader.getServiceMethodBindings(versDateTime);

        String addressLocation = null;
        String operationLocation = null;
        String protocolType = null;
        boolean foundMethod = false;
        for (MethodDefOperationBind element : opBindings) {
            if (element.methodName.equals(methodName)) {
                foundMethod = true;
                addressLocation = element.serviceBindingAddress;
                operationLocation = element.operationLocation;
                protocolType = element.protocolType;
            }
        }
        if (!foundMethod) {
            throw new MethodNotFoundException("Method " + methodName + " was not found in "
                    + bmReader.GetObjectPID() + "'s operation " + " binding.");
        }

        DeploymentDSBindSpec dsBindSpec = bmReader.getServiceDSInputSpec(versDateTime);
        DeploymentDSBindRule[] dsBindRules = dsBindSpec.dsBindRules == null ? DeploymentDSBindRule.ARRAY_TYPE
                : dsBindSpec.dsBindRules;

        // Results will be returned in this list, one item per *existing*
        // datastream. If a datastream mentioned in the dsBindRules is not
        // present in the object, it will not be present in this list.
        // If the datastream is *really* required in order to invoke the
        // dissemination method in question, rest assured it will fail later.
        List<DisseminationBindingInfo> bindingInfoList = new ArrayList<DisseminationBindingInfo>(
                dsBindRules.length);

        for (int i = 0; i < dsBindRules.length; i++) {
            DeploymentDSBindRule dsBindRule = dsBindRules[i];
            String dsPid = dsBindRule.pid == null ? dObj.GetObjectPID() : dsBindRule.pid;
            String dsId = dsBindRule.bindingKeyName;

            DOReader reader = m_manager.getReader(false, context, dsPid);
            Datastream ds = reader.GetDatastream(dsId, versDateTime);

            if (ds != null) {
                DisseminationBindingInfo bindingInfo = new DisseminationBindingInfo();
                bindingInfo.DSBindKey = dsId;
                bindingInfo.dsLocation = ds.DSLocation;
                bindingInfo.dsControlGroupType = ds.DSControlGrp;
                bindingInfo.dsID = ds.DatastreamID;
                bindingInfo.dsVersionID = ds.DSVersionID;
                bindingInfo.dsState = ds.DSState;
                bindingInfo.dsCreateDT = ds.DSCreateDT;
                // these will be the same for all elements of the array
                bindingInfo.methodParms = methodParms;
                bindingInfo.AddressLocation = addressLocation;
                bindingInfo.OperationLocation = operationLocation;
                bindingInfo.ProtocolType = protocolType;
                bindingInfoList.add(bindingInfo);
            }
        }
        return bindingInfoList.toArray(DisseminationBindingInfo.ARRAY_TYPE);
    }

    @Override
    public ObjectMethodsDef[] listMethods(Context context, String PID, Date asOfDateTime) throws ServerException {
        long startTime = logger.isDebugEnabled() ? System.currentTimeMillis() : 0;
        PID = Server.getPID(PID).toString();
        m_authorizationModule.enforceListMethods(context, PID, asOfDateTime);
        DOReader reader = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, PID);

        ObjectMethodsDef[] methodDefs = reader.listMethods(asOfDateTime);
        if (logger.isDebugEnabled()) {
            long stopTime = System.currentTimeMillis();
            long interval = stopTime - startTime;
            logger.debug("Roundtrip listMethods: {} milliseconds.", interval);
        }

        // DYNAMIC!! Grab any dynamic method definitions and merge them with
        // the statically bound method definitions
        ObjectMethodsDef[] dynamicMethodDefs = m_dynamicAccess.listMethods(context, PID, asOfDateTime);
        ObjectMethodsDef[] result = new ObjectMethodsDef[methodDefs.length + dynamicMethodDefs.length];
        System.arraycopy(methodDefs, 0, result, 0, methodDefs.length);
        System.arraycopy(dynamicMethodDefs, 0, result, methodDefs.length, dynamicMethodDefs.length);
        return result;
    }

    @Override
    public DatastreamDef[] listDatastreams(Context context, String PID, Date asOfDateTime) throws ServerException {
        long startTime = logger.isDebugEnabled() ? new Date().getTime() : 0;
        PID = Server.getPID(PID).toString();
        m_authorizationModule.enforceListDatastreams(context, PID, asOfDateTime);
        DOReader reader = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, PID);

        Datastream[] datastreams = reader.GetDatastreams(asOfDateTime, null);
        DatastreamDef[] dsDefs = new DatastreamDef[datastreams.length];
        for (int i = 0; i < datastreams.length; i++) {
            dsDefs[i] = new DatastreamDef(datastreams[i].DatastreamID, datastreams[i].DSLabel,
                    datastreams[i].DSMIME);
        }

        if (logger.isDebugEnabled()) {
            long stopTime = new Date().getTime();
            long interval = stopTime - startTime;
            logger.debug("Roundtrip listDatastreams: {} milliseconds.", interval);
        }
        return dsDefs;
    }

    @Override
    public ObjectProfile getObjectProfile(Context context, String PID, Date asOfDateTime) throws ServerException {
        PID = Server.getPID(PID).toString();
        m_authorizationModule.enforceGetObjectProfile(context, PID, asOfDateTime);
        DOReader reader = m_manager.getReader(asOfDateTime == null, context, PID);

        Date versDateTime = asOfDateTime;
        ObjectProfile profile = new ObjectProfile();
        profile.PID = reader.GetObjectPID();
        profile.objectLabel = reader.GetObjectLabel();
        profile.objectOwnerId = reader.getOwnerId();
        profile.objectModels = new HashSet<String>();
        profile.objectCreateDate = reader.getCreateDate();
        profile.objectLastModDate = reader.getLastModDate();
        profile.objectState = reader.GetObjectState();

        profile.objectModels.addAll(reader.getContentModels());

        // "bootstrap" context won't have the uri to determine security
        String securityUri = context.getEnvironmentValue(Constants.HTTP_REQUEST.SECURITY.attributeId);

        if (securityUri != null) {
            String reposBaseURL = getReposBaseURL(
                    securityUri.equals(Constants.HTTP_REQUEST.SECURE.uri) ? "https" : "http",
                    context.getEnvironmentValue(Constants.HTTP_REQUEST.SERVER_PORT.attributeId));
            profile.dissIndexViewURL = getDissIndexViewURL(reposBaseURL,
                    context.getEnvironmentValue(Constants.FEDORA_APP_CONTEXT_NAME), reader.GetObjectPID(),
                    versDateTime);
            profile.itemIndexViewURL = getItemIndexViewURL(reposBaseURL,
                    context.getEnvironmentValue(Constants.FEDORA_APP_CONTEXT_NAME), reader.GetObjectPID(),
                    versDateTime);
        }
        return profile;
    }

    /**
     * <p>
     * Lists the specified fields of each object matching the given criteria.
     * </p>
     *
     * @param context
     *        the context of this request
     * @param resultFields
     *        the names of the fields to return
     * @param maxResults
     *        the maximum number of results to return at a time
     * @param query
     *        the query
     * @return the results of te field search
     * @throws ServerException
     *         If any type of error occurred fulfilling the request.
     */
    @Override
    public FieldSearchResult findObjects(Context context, String[] resultFields, int maxResults,
            FieldSearchQuery query) throws ServerException {
        m_authorizationModule.enforceFindObjects(context);
        return m_manager.findObjects(context, resultFields, maxResults, query);
    }

    /**
     * <p>
     * Resumes an in-progress listing of object fields.
     * </p>
     *
     * @param context
     *        the context of this request
     * @param sessionToken
     *        the token of the session in which the remaining results can be
     *        obtained
     * @return the next set of results from the initial field search
     * @throws ServerException
     *         If any type of error occurred fulfilling the request.
     */
    @Override
    public FieldSearchResult resumeFindObjects(Context context, String sessionToken) throws ServerException {
        m_authorizationModule.enforceFindObjects(context);
        return m_manager.resumeFindObjects(context, sessionToken);
    }

    /**
     * <p>
     * Gets information that describes the repository.
     * </p>
     *
     * @param context
     *        the context of this request
     * @return information that describes the repository.
     * @throws ServerException
     *         If any type of error occurred fulfilling the request.
     */
    @Override
    public RepositoryInfo describeRepository(Context context) throws ServerException {
        m_authorizationModule.enforceDescribeRepository(context);
        RepositoryInfo repositoryInfo = new RepositoryInfo();
        repositoryInfo.repositoryName = getServer().getParameter("repositoryName");
        String reposBaseURL = getReposBaseURL(
                context.getEnvironmentValue(Constants.HTTP_REQUEST.SECURITY.attributeId)
                        .equals(Constants.HTTP_REQUEST.SECURE.uri) ? "https" : "http",
                context.getEnvironmentValue(Constants.HTTP_REQUEST.SERVER_PORT.attributeId));
        repositoryInfo.repositoryBaseURL = reposBaseURL + "/"
                + context.getEnvironmentValue(Constants.FEDORA_APP_CONTEXT_NAME);

        repositoryInfo.repositoryVersion = Server.VERSION;
        Module domgr = getServer().getModule("org.fcrepo.server.storage.DOManager");
        repositoryInfo.repositoryPIDNamespace = domgr.getParameter("pidNamespace");
        repositoryInfo.defaultExportFormat = domgr.getParameter("defaultExportFormat");
        repositoryInfo.OAINamespace = m_repositoryDomainName;
        repositoryInfo.adminEmailList = getAdminEmails();
        repositoryInfo.samplePID = repositoryInfo.repositoryPIDNamespace + ":100";
        repositoryInfo.sampleOAIIdentifer = "oai:" + repositoryInfo.OAINamespace + ":" + repositoryInfo.samplePID;
        repositoryInfo.sampleSearchURL = repositoryInfo.repositoryBaseURL + "/objects";
        repositoryInfo.sampleAccessURL = repositoryInfo.repositoryBaseURL + "/objects/" + "demo:5"; // PID should be URL-encoded, but nothing to encode in this case
        repositoryInfo.sampleOAIURL = repositoryInfo.repositoryBaseURL + "/oai?verb=Identify";
        repositoryInfo.retainPIDs = getRetainPIDs();
        return repositoryInfo;
    }

    /**
     * <p>
     * Gets the change history of an object by returning a list of timestamps
     * that correspond to modification dates of components. This currently
     * includes changes to datastreams and disseminators.
     * </p>
     *
     * @param context
     *        The context of this request.
     * @param PID
     *        The persistent identifier of the digitla object.
     * @return An Array containing the list of timestamps indicating when
     *         changes were made to the object.
     * @throws ServerException
     *         If any type of error occurred fulfilling the request.
     */
    @Override
    public String[] getObjectHistory(Context context, String PID) throws ServerException {
        PID = Server.getPID(PID).toString();
        m_authorizationModule.enforceGetObjectHistory(context, PID);
        DOReader reader = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, PID);

        return reader.getObjectHistory(PID);
    }

    private String[] getAdminEmails() {
        String emailsCSV = convertToCSV(getServer().getParameter("adminEmailList"));
        Vector<Object> emails = new Vector<Object>();
        StringTokenizer st = new StringTokenizer(emailsCSV, ",");
        while (st.hasMoreElements()) {
            emails.add(st.nextElement());
        }
        return emails.toArray(EMPTY_STRING_ARRAY);
    }

    private String[] getRetainPIDs() {
        String retainPIDsCSV = convertToCSV(
                getServer().getModule("org.fcrepo.server.storage.DOManager").getParameter("retainPIDs"));
        Vector<Object> retainPIDs = new Vector<Object>();
        StringTokenizer st = new StringTokenizer(retainPIDsCSV, ",");
        while (st.hasMoreElements()) {
            retainPIDs.add(st.nextElement());
        }
        return retainPIDs.toArray(EMPTY_STRING_ARRAY);
    }

    private String convertToCSV(String list) {
        // make sure values in the list are comma delimited
        if (list == null) {
            return "*";
        }
        String original = list.trim();
        Pattern spaces = Pattern.compile(" ++");
        Matcher m = spaces.matcher(original);
        String interim = m.replaceAll(",");
        Pattern multcommas = Pattern.compile(",++");
        Matcher m2 = multcommas.matcher(interim);
        String csv = m2.replaceAll(",");
        return csv;
    }

    /**
     * <p>
     * Validates user-supplied method parameters against values in the
     * corresponding Service Definition object. The method will validate for:
     * </p>
     * <ol>
     * <li>Valid name - each name must match a valid method parameter name</li>
     * <li>DefaultValue - any specified parameters with valid default values
     * will have the default value substituted if the user-supplied value is
     * null</li>
     * <li>Required name - each required method parameter name must be present
     * </ol>
     *
     * @param context
     *        The context of this request.
     * @param PID
     *        The persistent identifier of the digital object.
     * @param sDefPID
     *        The persistent identifier of the Service Definition object.
     * @param methodName
     *        The name of the method.
     * @param h_userParms
     *        A hashtable of user-supplied method parameter name/value pairs.
     * @param versDateTime
     *        The version datetime stamp of the digital object.
     * @throws ServerException
     *         If any type of error occurred fulfilling the request.
     */
    private void validateUserParms(Context context, String PID, String sDefPID, ServiceDeploymentReader sdepreader,
            String methodName, Hashtable<String, String> h_userParms, Date versDateTime) throws ServerException {
        PID = Server.getPID(PID).toString();
        sDefPID = Server.getPID(sDefPID).toString();
        MethodParmDef[] methodParms = null;
        MethodParmDef methodParm = null;
        StringBuffer sb = new StringBuffer();
        Hashtable<String, MethodParmDef> h_validParms = new Hashtable<String, MethodParmDef>();
        boolean isValid = true;

        if (sdepreader != null) // this code will be used for the CMDA example
        {
            MethodDef[] methods = sdepreader.getServiceMethods(versDateTime);
            // Filter out parms that are internal to the mechanism and not part
            // of the abstract method definition.  We just want user parms.
            for (MethodDef element : methods) {
                if (element.methodName.equalsIgnoreCase(methodName)) {
                    ArrayList<MethodParmDef> filteredParms = new ArrayList<MethodParmDef>();
                    MethodParmDef[] parms = element.methodParms;
                    for (MethodParmDef element2 : parms) {
                        if (element2.parmType.equalsIgnoreCase(MethodParmDef.USER_INPUT)) {
                            filteredParms.add(element2);
                        }
                    }
                    methodParms = filteredParms.toArray(METHOD_PARM_DEF_TYPE);
                }
            }
        } else {
            String message = "[DefaultAccess] Old-style disseminators are no longer supported ";
            throw new DisseminatorNotFoundException(message);
            //        reader = m_manager.getReader(Server.GLOBAL_CHOICE, context, PID);
            //        methodParms = reader.getObjectMethodParms(sDefPID, methodName, versDateTime);
        }

        // Put valid method parameters and their attributes into hashtable
        if (methodParms != null) {
            for (int i = 0; i < methodParms.length; i++) {
                methodParm = methodParms[i];
                h_validParms.put(methodParm.parmName, methodParm);
                if (logger.isDebugEnabled()) {
                    logger.debug("methodParms[" + i + "]: " + methodParms[i].parmName + "\nlabel: "
                            + methodParms[i].parmLabel + "\ndefault: " + methodParms[i].parmDefaultValue
                            + "\nrequired: " + methodParms[i].parmRequired + "\ntype: " + methodParms[i].parmType);
                    for (String element : methodParms[i].parmDomainValues) {
                        logger.debug("domainValue: {}", element);
                    }
                }
            }
        }

        if (!h_validParms.isEmpty()) {
            // Iterate over valid parmameters to check for any missing required parms.
            Enumeration<String> e = h_validParms.keys();
            while (e.hasMoreElements()) {
                String validName = e.nextElement();
                MethodParmDef mp = h_validParms.get(validName);
                if (mp.parmRequired && h_userParms.get(validName) == null) {
                    // This is a fatal error. A required method parameter does not
                    // appear in the list of user supplied parameters.
                    sb.append("The required parameter \"" + validName + "\" was not found in the "
                            + "user-supplied parameter list.");
                    throw new InvalidUserParmException("[Invalid User Parameters] " + sb.toString());
                }
            }

            // Iterate over each user supplied parameter name
            Enumeration<String> parmNames = h_userParms.keys();
            while (parmNames.hasMoreElements()) {
                String parmName = parmNames.nextElement();
                methodParm = h_validParms.get(parmName);
                if (methodParm != null && methodParm.parmName != null) {
                    // Method has one or more parameters defined
                    // Check for default value if user-supplied value is null or empty
                    String value = h_userParms.get(methodParm.parmName);
                    if (value == null || value.equalsIgnoreCase("")) {
                        // Value of user-supplied parameter is  null or empty
                        if (methodParm.parmDefaultValue != null) {
                            // Default value is specified for this parameter.
                            // Substitute default value.
                            h_userParms.put(methodParm.parmName, methodParm.parmDefaultValue);
                        } else {
                            // This is a non-fatal error. There is no default specified
                            // for this parameter and the user has supplied no value for
                            // the parameter. The value of the empty string will be used
                            // as the value of the parameter.
                            logger.warn("The method parameter \"" + methodParm.parmName
                                    + "\" has no default value and no " + "value was specified by the user.  "
                                    + "The value of the empty string has " + "been assigned to this parameter.");
                        }
                    } else {
                        // Value of user-supplied parameter contains a value.
                        // Validate the supplied value against the parmDomainValues list.
                        String[] parmDomainValues = methodParm.parmDomainValues;
                        if (parmDomainValues.length > 0) {
                            if (!parmDomainValues[0].equalsIgnoreCase("null")) {
                                boolean isValidValue = false;
                                String userValue = h_userParms.get(methodParm.parmName);
                                for (String element : parmDomainValues) {
                                    if (userValue.equalsIgnoreCase(element) || element.equalsIgnoreCase("null")) {
                                        isValidValue = true;
                                    }
                                }
                                if (!isValidValue) {
                                    for (int i = 0; i < parmDomainValues.length; i++) {
                                        sb.append(parmDomainValues[i]);
                                        if (i != parmDomainValues.length - 1) {
                                            sb.append(", ");
                                        }
                                    }
                                    sb.append("The method parameter \"" + methodParm.parmName
                                            + "\" with a value of \"" + h_userParms.get(methodParm.parmName)
                                            + "\" is not allowed for the method \"" + methodName
                                            + "\". Allowed values for this " + "method include \"" + sb.toString()
                                            + "\".");
                                    isValid = false;
                                }
                            }
                        }
                    }
                } else {
                    // This is a fatal error. A user-supplied parameter name does
                    // not match any valid parameter names for this method.
                    sb.append("The method parameter \"" + parmName + "\" is not valid for the method \""
                            + methodName + "\".");
                    isValid = false;
                }
            }
        } else {
            // There are no method parameters define for this method.
            if (!h_userParms.isEmpty()) {
                // This is an error. There are no method parameters defined for
                // this method and user parameters are specified in the
                // dissemination request.
                Enumeration<String> e = h_userParms.keys();
                while (e.hasMoreElements()) {
                    sb.append("The method parameter \"" + e.nextElement() + "\" is not valid for the method \""
                            + methodName + "\"." + "The method \"" + methodName
                            + "\" defines no method parameters.");
                }
                throw new InvalidUserParmException("[Invalid User Parameters] " + sb.toString());
            }
        }
        if (!isValid) {
            throw new InvalidUserParmException("[Invalid User Parameter] " + sb.toString());
        }
        return;
    }

    private String getDissIndexViewURL(String reposBaseURL, String fedoraContext, String PID, Date versDateTime) {
        String dissIndexURL = null;

        try {
            if (versDateTime == null) {
                dissIndexURL = reposBaseURL + "/" + fedoraContext + "/objects/" + URLEncoder.encode(PID, "UTF-8")
                        + "/methods/" + URLEncoder.encode("fedora-system:3", "UTF-8") + "/viewMethodIndex";
            } else {
                dissIndexURL = reposBaseURL + "/" + fedoraContext + "/objects/" + URLEncoder.encode(PID, "UTF-8")
                        + "/methods" + URLEncoder.encode("fedora-system:3", "UTF-8")
                        + "/viewMethodIndex?asOfDateTime=" + DateUtility.convertDateToString(versDateTime);
            }
        } catch (UnsupportedEncodingException e) {
            // should never happen...
            throw new RuntimeException(e);
        }
        return dissIndexURL;
    }

    // FIXIT!! Consider implications of hard-coding the default dissemination
    // aspects of the URL (e.g. fedora-system3 as the PID and viewItemIndex.
    private String getItemIndexViewURL(String reposBaseURL, String fedoraContext, String PID, Date versDateTime) {
        String itemIndexURL = null;

        try {
            if (versDateTime == null) {
                itemIndexURL = reposBaseURL + "/" + fedoraContext + "/objects/" + URLEncoder.encode(PID, "UTF-8")
                        + "/methods/" + URLEncoder.encode("fedora-system:3", "UTF-8") + "/viewItemIndex";
            } else {
                itemIndexURL = reposBaseURL + "/" + fedoraContext + "/objects/" + URLEncoder.encode(PID, "UTF-8")
                        + "/methods/" + URLEncoder.encode("fedora-system:3", "UTF-8")
                        + "/viewItemIndex?asOfDateTime=" + DateUtility.convertDateToString(versDateTime);
            }
        } catch (UnsupportedEncodingException e) {
            // should never happen...
            throw new RuntimeException(e);
        }
        return itemIndexURL;
    }

    private String getReposBaseURL(String protocol, String port) {
        String reposBaseURL = null;
        String fedoraServerHost = getServer().getParameter("fedoraServerHost");
        if (fedoraServerHost == null || fedoraServerHost.isEmpty()) {
            logger.warn("Configuration parameter fedoraServerHost is empty.");
            try {
                InetAddress hostIP = InetAddress.getLocalHost();
                fedoraServerHost = hostIP.getHostName();
            } catch (UnknownHostException e) {
                logger.error("Unable to resolve host of Fedora server", e);
                fedoraServerHost = "localhost";
            }
        }
        reposBaseURL = protocol + "://" + fedoraServerHost + ":" + port;
        return reposBaseURL;
    }

    @Override
    public MIMETypedStream getDatastreamDissemination(Context context, String PID, String dsID, Date asOfDateTime)
            throws ServerException {
        PID = Server.getPID(PID).toString();
        m_authorizationModule.enforceGetDatastreamDissemination(context, PID, dsID, asOfDateTime);
        MIMETypedStream mimeTypedStream = null;
        long startTime = (logger.isDebugEnabled()) ? new Date().getTime() : 0;
        DOReader reader = m_manager.getReader(Server.USE_DEFINITIVE_STORE, context, PID);

        Datastream ds = reader.GetDatastream(dsID, asOfDateTime);
        if (ds == null) {
            String message = "[DefaulAccess] No datastream could be returned. "
                    + "Either there is no datastream for the digital " + "object \"" + PID
                    + "\" with datastream ID of \"" + dsID
                    + " \"  OR  there are no datastreams that match the specified " + "date/time value of \""
                    + DateUtility.convertDateToString(asOfDateTime) + " \"  .";
            throw new DatastreamNotFoundException(message);
        }

        if (ds.isRepositoryManaged()) {
            if (ds.DSSize <= 0) {
                ds.DSSize = ds.getContentSize(context);
            }
            Property[] dsHeaders = getDatastreamHeaders(PID, ds);
            if (ServerUtility.isStaleCache(context, dsHeaders)) {
                if (isHEADRequest(context)) {
                    mimeTypedStream = new MIMETypedStream(ds.DSMIME, NullInputStream.NULL_STREAM,
                            getDatastreamHeaders(PID, ds), ds.DSSize);
                } else {
                    mimeTypedStream = new MIMETypedStream(ds.DSMIME, ds.getContentStream(context), dsHeaders,
                            ds.DSSize);
                }
            } else {
                mimeTypedStream = MIMETypedStream.getNotModified(dsHeaders);
            }
            String rangeHdr = context.getHeaderValue(HttpHeaders.RANGE);
            // delimit Content-Range if necessary
            if (rangeHdr != null)
                mimeTypedStream.setRange(rangeHdr);
        } else if (ds.DSControlGrp.equalsIgnoreCase("E")) {
            DatastreamReferencedContent drc = (DatastreamReferencedContent) ds;
            ContentManagerParams params = new ContentManagerParams(drc.DSLocation, drc.DSMIME, null, null);
            params.setContext(context);
            mimeTypedStream = m_externalContentManager.getExternalContent(params);
        } else if (ds.DSControlGrp.equalsIgnoreCase("R")) {
            DatastreamReferencedContent drc = (DatastreamReferencedContent) ds;
            // The dsControlGroupType of Redirect("R") is a special control type
            // used primarily for streaming media. Datastreams of this type are
            // not mediated (proxied by Fedora) and their physical dsLocation is
            // simply redirected back to the client. Therefore, the contents
            // of the MIMETypedStream returned for dissemination requests will
            // contain the raw URL of the dsLocation and will be assigned a
            // special fedora-specific MIME type to identify the stream as
            // a MIMETypedStream whose contents contain a URL to which the client
            // should be redirected.
            mimeTypedStream = MIMETypedStream.getRedirect(drc.DSLocation);
        }
        if (logger.isDebugEnabled()) {
            long stopTime = System.currentTimeMillis();
            long interval = stopTime - startTime;
            logger.debug("Roundtrip getDatastreamDissemination: {} milliseconds.", interval);
        }
        return mimeTypedStream;
    }

    /**
     * Content-Length is determined elsewhere
     * Content-Type is determined elsewhere
     * Last-Modified
     * ETag
     * @param ds
     * @return
     */
    private static Property[] getDatastreamHeaders(String pid, Datastream ds) {
        Property[] result = new Property[3];
        result[0] = new Property(HttpHeaders.ACCEPT_RANGES, "bytes");
        result[1] = new Property(HttpHeaders.ETAG, Datastream.defaultETag(pid, ds));
        result[2] = new Property(HttpHeaders.LAST_MODIFIED, DateUtil.formatDate(ds.DSCreateDT));
        return result;
    }

    /**
     * determine whether the context is a HEAD http request
     */
    private static boolean isHEADRequest(Context context) {
        if (context != null) {
            String method = context.getEnvironmentValue(Constants.HTTP_REQUEST.METHOD.attributeId);
            return "HEAD".equalsIgnoreCase(method);

        }
        return false;
    }

}