/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.content.packager;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import edu.harvard.hul.ois.mets.AmdSec;
import edu.harvard.hul.ois.mets.BinData;
import edu.harvard.hul.ois.mets.Checksumtype;
import edu.harvard.hul.ois.mets.Div;
import edu.harvard.hul.ois.mets.DmdSec;
import edu.harvard.hul.ois.mets.MdRef;
import edu.harvard.hul.ois.mets.FLocat;
import edu.harvard.hul.ois.mets.FileGrp;
import edu.harvard.hul.ois.mets.FileSec;
import edu.harvard.hul.ois.mets.Fptr;
import edu.harvard.hul.ois.mets.Mptr;
import edu.harvard.hul.ois.mets.Loctype;
import edu.harvard.hul.ois.mets.MdWrap;
import edu.harvard.hul.ois.mets.Mdtype;
import edu.harvard.hul.ois.mets.Mets;
import edu.harvard.hul.ois.mets.MetsHdr;
import edu.harvard.hul.ois.mets.StructMap;
import edu.harvard.hul.ois.mets.TechMD;
import edu.harvard.hul.ois.mets.SourceMD;
import edu.harvard.hul.ois.mets.DigiprovMD;
import edu.harvard.hul.ois.mets.RightsMD;
import edu.harvard.hul.ois.mets.helper.MdSec;
import edu.harvard.hul.ois.mets.XmlData;
import edu.harvard.hul.ois.mets.helper.Base64;
import edu.harvard.hul.ois.mets.helper.MetsElement;
import edu.harvard.hul.ois.mets.helper.MetsException;
import edu.harvard.hul.ois.mets.helper.MetsValidator;
import edu.harvard.hul.ois.mets.helper.MetsWriter;
import edu.harvard.hul.ois.mets.helper.PreformedXML;
import java.io.File;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import org.apache.log4j.Logger;

import org.dspace.app.util.Util;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.AuthorizeManager;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Community;
import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.ItemIterator;
import org.dspace.content.crosswalk.AbstractPackagerWrappingCrosswalk;
import org.dspace.content.crosswalk.CrosswalkException;
import org.dspace.content.crosswalk.CrosswalkObjectNotSupported;
import org.dspace.content.crosswalk.DisseminationCrosswalk;
import org.dspace.content.crosswalk.StreamDisseminationCrosswalk;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.PluginManager;
import org.dspace.core.Utils;
import org.dspace.license.CreativeCommons;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

/**
 * Base class for disseminator of
 * METS (Metadata Encoding & Transmission Standard) Package.<br>
 *   See <a href="http://www.loc.gov/standards/mets/">http://www.loc.gov/standards/mets/</a>
 * <p>
 * This is a generic packager framework intended to be subclassed to create
 * packagers for more specific METS "profiles".   METS is an
 * abstract and flexible framework that can encompass many
 * different kinds of metadata and inner package structures.
 * <p>
 * <b>Package Parameters:</b><br>
 * <ul>
 * <li><code>manifestOnly</code> -- if true, generate a standalone XML
 * document of the METS manifest instead of a complete package.  Any
 * other metadata (such as licenses) will be encoded inline.
 * Default is <code>false</code>.</li>
 *
 * <li><code>unauthorized</code> -- this determines what is done when the
 * packager encounters a Bundle or Bitstream it is not authorized to
 * read.  By default, it just quits with an AuthorizeException.
 *   If this option is present, it must be one of the following values:
 *   <ul>
 *     <li><code>skip</code> -- simply exclude unreadable content from package.</li>
 *     <li><code>zero</code> -- include unreadable bitstreams as 0-length files;
 *       unreadable Bundles will still cause authorize errors.</li></ul></li>
 * </ul>
 *
 * @author Larry Stone
 * @author Robert Tansley
 * @author Tim Donohue
 * @version $Revision$
 */
public abstract class AbstractMETSDisseminator
    extends AbstractPackageDisseminator
{
    /** log4j category */
    private static Logger log = Logger.getLogger(AbstractMETSDisseminator.class);

    // JDOM xml output writer - indented format for readability.
    private static XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());

    // for gensym()
    private int idCounter = 1;

    /**
     * Default date/time (in milliseconds since epoch) to set for Zip Entries
     * for DSpace Objects which don't have a Last Modified date.  If we don't
     * set our own date/time, then it will default to current system date/time.
     * This is less than ideal, as it causes the md5 checksum of Zip file to
     * change whenever Zip is regenerated (even if compressed files are unchanged)
     * 1036368000 seconds * 1000 = Nov 4, 2002 GMT (the date DSpace 1.0 was released)
     */
    private static final int DEFAULT_MODIFIED_DATE = 1036368000 * 1000;

    /**
     * Suffix for Template objects (e.g. Item Templates)
     */
    protected static final String TEMPLATE_TYPE_SUFFIX = " Template";

    /**
     * Wrapper for a table of streams to add to the package, such as
     * mdRef'd metadata.  Key is relative pathname of file, value is
     * <code>InputStream</code> with contents to put in it.  Some
     * superclasses will put streams in this table when adding an mdRef
     * element to e.g. a rightsMD segment.
     */
    protected static class MdStreamCache
    {
        private Map<MdRef,InputStream> extraFiles = new HashMap<MdRef,InputStream>();

        public void addStream(MdRef key, InputStream md)
        {
            extraFiles.put(key, md);
        }

        public Map<MdRef,InputStream> getMap()
        {
            return extraFiles;
        }

        public void close()
            throws IOException
        {
            for (InputStream is : extraFiles.values())
            {
                is.close();
            }
        }
    }

    /**
     * Make a new unique ID symbol with specified prefix.
     * @param prefix the prefix of the identifier, constrained to XML ID schema
     * @return a new string identifier unique in this session (instance).
     */
    protected synchronized String gensym(String prefix)
    {
        return prefix + "_" + String.valueOf(idCounter++);
    }
    
    /**
     * Resets the unique ID counter used by gensym() method to
     * determine the @ID values of METS tags.
     */
    protected synchronized void resetCounter()
    {
        idCounter = 1;
    }

    @Override
    public String getMIMEType(PackageParameters params)
    {
        return (params != null &&
                (params.getBooleanProperty("manifestOnly", false))) ?
                "text/xml" : "application/zip";
    }

    /**
     * Export the object (Item, Collection, or Community) as a
     * "package" on the indicated OutputStream.  Package is any serialized
     * representation of the item, at the discretion of the implementing
     * class.  It does not have to include content bitstreams.
     * <p>
     * Use the <code>params</code> parameter list to adjust the way the
     * package is made, e.g. including a "<code>metadataOnly</code>"
     * parameter might make the package a bare manifest in XML
     * instead of a Zip file including manifest and contents.
     * <p>
     * Throws an exception of the chosen object is not acceptable or there is
     * a failure creating the package.
     *
     * @param context  DSpace context.
     * @param dso  DSpace object (item, collection, etc)
     * @param params Properties-style list of options specific to this packager
     * @param pkgFile File where export package should be written
     * @throws PackageValidationException if package cannot be created or there
     * is a fatal error in creating it.
     */
    @Override
    public void disseminate(Context context, DSpaceObject dso,
                            PackageParameters params, File pkgFile)
        throws PackageValidationException, CrosswalkException, AuthorizeException, SQLException, IOException
    {
        // Reset our 'unique' ID counter back to 1 (in case a previous dissemination was run)
        // This ensures that the @ID attributes of METS tags always begin at '1', which
        // also ensures that the Checksums don't change because of accidental @ID value changes.
        resetCounter();
        
        FileOutputStream outStream = null;
        try
        {
            //Make sure our package file exists
            if(!pkgFile.exists())
            {
                PackageUtils.createFile(pkgFile);
            }

            //Open up an output stream to write to package file
            outStream = new FileOutputStream(pkgFile);

            // Generate a true manifest-only "package", no external files/data & no need to zip up
            if (params != null && params.getBooleanProperty("manifestOnly", false))
            {
                Mets manifest = makeManifest(context, dso, params, null);
                //only validate METS if specified (default = true)
                if(params.getBooleanProperty("validate", true))
                {
                    manifest.validate(new MetsValidator());
                }
                manifest.write(new MetsWriter(outStream));
            }
            else
            {
                // make a Zip-based package
                writeZipPackage(context, dso, params, outStream);
            }//end if/else
        }//end try
        catch (MetsException e)
        {
            String errorMsg = "Error exporting METS for DSpace Object, type="
                            + Constants.typeText[dso.getType()] + ", handle="
                            + dso.getHandle() + ", dbID="
                            + String.valueOf(dso.getID());

            // We don't pass up a MetsException, so callers don't need to
            // know the details of the METS toolkit
            log.error(errorMsg,e);
            throw new PackageValidationException(errorMsg, e);
        }
        finally
        {
            //Close stream / stop writing to file
            if (outStream != null)
            {
                outStream.close();
            }
        }
    }


    /**
     * Make a Zipped up METS package for the given DSpace Object
     *
     * @param context DSpace Context
     * @param dso The DSpace Object
     * @param params Parameters to the Packager script
     * @param pkg Package output stream
     * @throws PackageValidationException
     * @throws AuthorizeException
     * @throws SQLException
     * @throws IOException
     */
    protected void writeZipPackage(Context context, DSpaceObject dso,
            PackageParameters params, OutputStream pkg)
            throws PackageValidationException, CrosswalkException, MetsException,
            AuthorizeException, SQLException, IOException
    {
        long lmTime = 0;
        if (dso.getType() == Constants.ITEM)
        {
            lmTime = ((Item) dso).getLastModified().getTime();
        }

        // map of extra streams to put in Zip (these are located during makeManifest())
        MdStreamCache extraStreams = new MdStreamCache();
        ZipOutputStream zip = new ZipOutputStream(pkg);
        zip.setComment("METS archive created by DSpace " + Util.getSourceVersion());
        Mets manifest = makeManifest(context, dso, params, extraStreams);

        // copy extra (metadata, license, etc) bitstreams into zip, update manifest
        if (extraStreams != null)
        {
            for (Map.Entry<MdRef, InputStream> ment : extraStreams.getMap().entrySet())
            {
                MdRef ref = ment.getKey();

                // Both Deposit Licenses & CC Licenses which are referenced as "extra streams" may already be
                // included in our Package (if their bundles are already included in the <filSec> section of manifest).
                // So, do a special check to see if we need to link up extra License <mdRef> entries to the bitstream in the <fileSec>.
                // (this ensures that we don't accidentally add the same License file to our package twice)
                linkLicenseRefsToBitstreams(context, params, dso, ref);

                //If this 'mdRef' is NOT already linked up to a file in the package,
                // then its file must be missing.  So, we are going to add a new
                // file to the Zip package.
                if(ref.getXlinkHref()==null || ref.getXlinkHref().isEmpty())
                {
                    InputStream is = ment.getValue();

                    // create a hopefully unique filename within the Zip
                    String fname = gensym("metadata");
                    // link up this 'mdRef' to point to that file
                    ref.setXlinkHref(fname);
                    if (log.isDebugEnabled())
                    {
                        log.debug("Writing EXTRA stream to Zip: " + fname);
                    }
                    //actually add the file to the Zip package
                    ZipEntry ze = new ZipEntry(fname);
                    if (lmTime != 0)
                    {
                        ze.setTime(lmTime);
                    }
                    else //Set a default modified date so that checksum of Zip doesn't change if Zip contents are unchanged
                    {
                        ze.setTime(DEFAULT_MODIFIED_DATE);
                    }
                    zip.putNextEntry(ze);
                    Utils.copy(is, zip);
                    zip.closeEntry();

                    is.close();
                }
            }
        }

        // write manifest after metadata.
        ZipEntry me = new ZipEntry(METSManifest.MANIFEST_FILE);
        if (lmTime != 0)
        {
            me.setTime(lmTime);
        }
        else //Set a default modified date so that checksum of Zip doesn't change if Zip contents are unchanged
        {
            me.setTime(DEFAULT_MODIFIED_DATE);
        }

        zip.putNextEntry(me);

        // can only validate now after fixing up extraStreams
        // note: only validate METS if specified (default = true)
        if(params.getBooleanProperty("validate", true))
        {
            manifest.validate(new MetsValidator());
        }
        manifest.write(new MetsWriter(zip));
        zip.closeEntry();

        //write any bitstreams associated with DSpace object to zip package
        addBitstreamsToZip(context, dso, params, zip);

        zip.close();

    }
    /**
     * Add Bitstreams associated with a given DSpace Object into an
     * existing ZipOutputStream
     * @param context DSpace Context
     * @param dso The DSpace Object
     * @param params Parameters to the Packager script
     * @param zip Zip output
     */
    protected void addBitstreamsToZip(Context context, DSpaceObject dso,
            PackageParameters params, ZipOutputStream zip)
            throws PackageValidationException, AuthorizeException, SQLException,
            IOException
    {
        // how to handle unauthorized bundle/bitstream:
        String unauth = (params == null) ? null : params.getProperty("unauthorized");

        // copy all non-meta bitstreams into zip
        if (dso.getType() == Constants.ITEM)
        {
            Item item = (Item)dso;

            //get last modified time
            long lmTime = ((Item)dso).getLastModified().getTime();

            Bundle bundles[] = item.getBundles();
            for (int i = 0; i < bundles.length; i++)
            {
                if (includeBundle(bundles[i]))
                {
                    // unauthorized bundle?
                    if (!AuthorizeManager.authorizeActionBoolean(context,
                                bundles[i], Constants.READ))
                    {
                        if (unauth != null &&
                            (unauth.equalsIgnoreCase("skip")))
                        {
                            log.warn("Skipping Bundle[\""+bundles[i].getName()+"\"] because you are not authorized to read it.");
                            continue;
                        }
                        else
                        {
                            throw new AuthorizeException("Not authorized to read Bundle named \"" + bundles[i].getName() + "\"");
                        }
                    }
                    Bitstream[] bitstreams = bundles[i].getBitstreams();
                    for (int k = 0; k < bitstreams.length; k++)
                    {
                        boolean auth = AuthorizeManager.authorizeActionBoolean(context,
                                bitstreams[k], Constants.READ);
                        if (auth ||
                            (unauth != null && unauth.equalsIgnoreCase("zero")))
                        {
                            String zname = makeBitstreamURL(bitstreams[k], params);
                            ZipEntry ze = new ZipEntry(zname);
                            if (log.isDebugEnabled())
                            {
                                log.debug(new StringBuilder().append("Writing CONTENT stream of bitstream(").append(bitstreams[k].getID()).append(") to Zip: ").append(zname).append(", size=").append(bitstreams[k].getSize()).toString());
                            }
                            if (lmTime != 0)
                            {
                                ze.setTime(lmTime);
                            }
                            else //Set a default modified date so that checksum of Zip doesn't change if Zip contents are unchanged
                            {
                                ze.setTime(DEFAULT_MODIFIED_DATE);
                            }
                            ze.setSize(auth ? bitstreams[k].getSize() : 0);
                            zip.putNextEntry(ze);
                            if (auth)
                            {
                                InputStream input = bitstreams[k].retrieve();
                                Utils.copy(input, zip);
                                input.close();
                            }
                            else
                            {
                                log.warn("Adding zero-length file for Bitstream, SID="
                                        + String.valueOf(bitstreams[k].getSequenceID())
                                        + ", not authorized for READ.");
                            }
                            zip.closeEntry();
                        }
                        else if (unauth != null &&
                                 unauth.equalsIgnoreCase("skip"))
                        {
                            log.warn("Skipping Bitstream, SID="+String.valueOf(bitstreams[k].getSequenceID())+", not authorized for READ.");
                        }
                        else
                        {
                            throw new AuthorizeException("Not authorized to read Bitstream, SID="+String.valueOf(bitstreams[k].getSequenceID()));
                        }
                    }
                }
            }
        }

        // Coll, Comm just add logo bitstream to content if there is one
        else if (dso.getType() == Constants.COLLECTION ||
                 dso.getType() == Constants.COMMUNITY)
        {
            Bitstream logoBs = dso.getType() == Constants.COLLECTION ?
                                 ((Collection)dso).getLogo() :
                                 ((Community)dso).getLogo();
            if (logoBs != null)
            {
                String zname = makeBitstreamURL(logoBs, params);
                ZipEntry ze = new ZipEntry(zname);
                if (log.isDebugEnabled())
                {
                    log.debug("Writing CONTENT stream of bitstream(" + String.valueOf(logoBs.getID()) + ") to Zip: " + zname + ", size=" + String.valueOf(logoBs.getSize()));
                }
                ze.setSize(logoBs.getSize());
                //Set a default modified date so that checksum of Zip doesn't change if Zip contents are unchanged
                ze.setTime(DEFAULT_MODIFIED_DATE);
                zip.putNextEntry(ze);
                Utils.copy(logoBs.retrieve(), zip);
                zip.closeEntry();
            }
        }
    }

    // set metadata type - if Mdtype.parse() gets exception,
    // that means it's not in the MDTYPE vocabulary, so use OTHER.
    protected void setMdType(MdWrap mdWrap, String mdtype)
    {
        try
        {
            mdWrap.setMDTYPE(Mdtype.parse(mdtype));
        }
        catch (MetsException e)
        {
            mdWrap.setMDTYPE(Mdtype.OTHER);
            mdWrap.setOTHERMDTYPE(mdtype);
        }
    }

    // set metadata type - if Mdtype.parse() gets exception,
    // that means it's not in the MDTYPE vocabulary, so use OTHER.
    protected void setMdType(MdRef  mdRef, String mdtype)
    {
        try
        {
            mdRef.setMDTYPE(Mdtype.parse(mdtype));
        }
        catch (MetsException e)
        {
            mdRef.setMDTYPE(Mdtype.OTHER);
            mdRef.setOTHERMDTYPE(mdtype);
        }
    }


    /**
     * Create an element wrapped around a metadata reference (either mdWrap
     * or mdRef); i.e. dmdSec, techMd, sourceMd, etc.  Checks for
     * XML-DOM oriented crosswalk first, then if not found looks for
     * stream crosswalk of the same name.
     *
     * @param context DSpace Context
     * @param dso DSpace Object we are generating METS manifest for
     * @param mdSecClass class of mdSec (TechMD, RightsMD, DigiProvMD, etc)
     * @param typeSpec Type of metadata going into this mdSec (e.g. MODS, DC, PREMIS, etc)
     * @param params the PackageParameters
     * @param extraStreams list of extra files which need to be added to final dissemination package
     * 
     * @return mdSec element or null if xwalk returns empty results.
     * 
     * @throws SQLException
     * @throws PackageValidationException
     * @throws CrosswalkException
     * @throws IOException
     * @throws AuthorizeException
     */
    protected MdSec makeMdSec(Context context, DSpaceObject dso, Class mdSecClass,
                              String typeSpec, PackageParameters params,
                              MdStreamCache extraStreams)
        throws SQLException, PackageValidationException, CrosswalkException,
               IOException, AuthorizeException
    {
        try
        {
            //create our metadata element (dmdSec, techMd, sourceMd, rightsMD etc.)
            MdSec mdSec = (MdSec) mdSecClass.newInstance();
            mdSec.setID(gensym(mdSec.getLocalName()));
            String parts[] = typeSpec.split(":", 2);
            String xwalkName, metsName;

            //determine the name of the crosswalk to use to generate metadata
            // for dmdSecs this is the part *after* the colon in the 'type' (see getDmdTypes())
            // for all other mdSecs this is usually just corresponds to type name.
            if (parts.length > 1)
            {
                metsName = parts[0];
                xwalkName = parts[1];
            }
            else
            {
                metsName = typeSpec;
                xwalkName = typeSpec; 
            }

            // First, check to see if the crosswalk we are using is a normal DisseminationCrosswalk
            boolean xwalkFound = PluginManager.hasNamedPlugin(DisseminationCrosswalk.class, xwalkName);

            if(xwalkFound)
            {
                // Find the crosswalk we will be using to generate the metadata for this mdSec
                DisseminationCrosswalk xwalk = (DisseminationCrosswalk)
                    PluginManager.getNamedPlugin(DisseminationCrosswalk.class, xwalkName);

                if (xwalk.canDisseminate(dso))
                {
                    // Check if our Crosswalk actually wraps another Packager Plugin
                    if(xwalk instanceof AbstractPackagerWrappingCrosswalk)
                    {
                        // If this crosswalk wraps another Packager Plugin, we can pass it our Packaging Parameters
                        // (which essentially allow us to customize the output of the crosswalk)
                        AbstractPackagerWrappingCrosswalk wrapper = (AbstractPackagerWrappingCrosswalk) xwalk;
                        wrapper.setPackagingParameters(params);
                    }

                    //For a normal DisseminationCrosswalk, we will be expecting an XML (DOM) based result.
                    // So, we are going to wrap this XML result in an <mdWrap> element
                    MdWrap mdWrap = new MdWrap();
                    setMdType(mdWrap, metsName);
                    XmlData xmlData = new XmlData();
                    if (crosswalkToMetsElement(xwalk, dso, xmlData) != null)
                    {
                        mdWrap.getContent().add(xmlData);
                        mdSec.getContent().add(mdWrap);
                        return mdSec;
                    }
                    else
                    {
                        return null;
                    }
                }
                else
                {
                    return null;
                }
            }
            // If we didn't find the correct crosswalk, we will check to see if this is
            // a StreamDisseminationCrosswalk -- a Stream crosswalk disseminates to an OutputStream
            else
            {
                StreamDisseminationCrosswalk sxwalk = (StreamDisseminationCrosswalk)
                  PluginManager.getNamedPlugin(StreamDisseminationCrosswalk.class, xwalkName);
                if (sxwalk != null)
                {
                    if (sxwalk.canDisseminate(context, dso))
                    {
                        // Check if our Crosswalk actually wraps another Packager Plugin
                        if(sxwalk instanceof AbstractPackagerWrappingCrosswalk)
                        {
                            // If this crosswalk wraps another Packager Plugin, we can pass it our Packaging Parameters
                            // (which essentially allow us to customize the output of the crosswalk)
                            AbstractPackagerWrappingCrosswalk wrapper = (AbstractPackagerWrappingCrosswalk) sxwalk;
                            wrapper.setPackagingParameters(params);
                        }

                        // Disseminate crosswalk output to an outputstream
                        ByteArrayOutputStream disseminateOutput = new ByteArrayOutputStream();
                        sxwalk.disseminate(context, dso, disseminateOutput);
                        // Convert output to an inputstream, so we can write to manifest or Zip file
                        ByteArrayInputStream crosswalkedStream = new ByteArrayInputStream(disseminateOutput.toByteArray());

                        //If we are capturing extra files to put into a Zip package
                        if(extraStreams!=null)
                        {
                            //Create an <mdRef> -- we'll just reference the file by name in Zip package
                            MdRef mdRef = new MdRef();
                            //add the crosswalked Stream to list of files to add to Zip package later
                            extraStreams.addStream(mdRef, crosswalkedStream);

                            //set properties on <mdRef>
                            // Note, filename will get set on this <mdRef> later,
                            // when we process all the 'extraStreams'
                            mdRef.setMIMETYPE(sxwalk.getMIMEType());
                            setMdType(mdRef, metsName);
                            mdRef.setLOCTYPE(Loctype.URL);
                            mdSec.getContent().add(mdRef);
                        }
                        else
                        {
                            //If we are *not* capturing extra streams to add to Zip package later,
                            // that means we are likely only generating a METS manifest
                            // (i.e. manifestOnly = true)
                            // In this case, the best we can do is take the crosswalked
                            // Stream, base64 encode it, and add in an <mdWrap> field

                            // First, create our <mdWrap>
                            MdWrap mdWrap = new MdWrap();
                            mdWrap.setMIMETYPE(sxwalk.getMIMEType());
                            setMdType(mdWrap, metsName);

                            // Now, create our <binData> and add base64 encoded contents to it.
                            BinData binData = new BinData();
                            Base64 base64 = new Base64(crosswalkedStream);
                            binData.getContent().add(base64);
                            mdWrap.getContent().add(binData);
                            mdSec.getContent().add(mdWrap);
                        }

                        return mdSec;
                    }
                    else
                    {
                        return null;
                    }
                }
                else
                {
                    throw new PackageValidationException("Cannot find " + xwalkName + " crosswalk plugin, either DisseminationCrosswalk or StreamDisseminationCrosswalk");
                }
            }
        }
        catch (InstantiationException e)
        {
            throw new PackageValidationException("Error instantiating Mdsec object: "+ e.toString(), e);
        }
        catch (IllegalAccessException e)
        {
            throw new PackageValidationException("Error instantiating Mdsec object: "+ e.toString(), e);
        }
    }

    // add either a techMd or sourceMd element to amdSec.
    // mdSecClass determines which type.
    // mdTypes[] is array of "[metsName:]PluginName" strings, maybe empty.
    protected void addToAmdSec(AmdSec fAmdSec, String mdTypes[], Class mdSecClass,
                             Context context, DSpaceObject dso,
                             PackageParameters params,
                             MdStreamCache extraStreams)
        throws SQLException, PackageValidationException, CrosswalkException,
               IOException, AuthorizeException
    {
        for (int i = 0; i < mdTypes.length; ++i)
        {
            MdSec md = makeMdSec(context, dso, mdSecClass, mdTypes[i], params, extraStreams);
            if (md != null)
            {
                fAmdSec.getContent().add(md);
            }
        }
    }

    // Create amdSec for any tech md's, return its ID attribute.
    protected String addAmdSec(Context context, DSpaceObject dso, PackageParameters params,
                             Mets mets, MdStreamCache extraStreams)
        throws SQLException, PackageValidationException, CrosswalkException,
               IOException, AuthorizeException
    {
        String techMdTypes[] = getTechMdTypes(context, dso, params);
        String rightsMdTypes[] = getRightsMdTypes(context, dso, params);
        String sourceMdTypes[] = getSourceMdTypes(context, dso, params);
        String digiprovMdTypes[] = getDigiprovMdTypes(context, dso, params);

        // only bother if there are any sections to add
        if ((techMdTypes.length+sourceMdTypes.length+
             digiprovMdTypes.length+rightsMdTypes.length) > 0)
        {
            String result = gensym("amd");
            AmdSec fAmdSec = new AmdSec();
            fAmdSec.setID(result);
            addToAmdSec(fAmdSec, techMdTypes, TechMD.class, context, dso, params, extraStreams);
            addToAmdSec(fAmdSec, rightsMdTypes, RightsMD.class, context, dso, params, extraStreams);
            addToAmdSec(fAmdSec, sourceMdTypes, SourceMD.class, context, dso, params, extraStreams);
            addToAmdSec(fAmdSec, digiprovMdTypes, DigiprovMD.class, context, dso, params, extraStreams);

            mets.getContent().add(fAmdSec);
            return result;
        }
        else
        {
            return null;
        }
    }

    // make the most "persistent" identifier possible, preferably a URN
    // based on the Handle.
    protected String makePersistentID(DSpaceObject dso)
    {
        String handle = dso.getHandle();

        // If no Handle, punt to much-less-satisfactory database ID and type..
        if (handle == null)
        {
            return "DSpace_DB_" + Constants.typeText[dso.getType()] + "_" + String.valueOf(dso.getID());
        }
        else
        {
            return getHandleURN(handle);
        }
    }

    /**
     * Write out a METS manifest.
     * Mostly lifted from Rob Tansley's METS exporter.
     */
    protected Mets makeManifest(Context context, DSpaceObject dso,
                              PackageParameters params,
                              MdStreamCache extraStreams)
        throws MetsException, PackageValidationException, CrosswalkException, AuthorizeException, SQLException, IOException

    {
        // Create the METS manifest in memory
        Mets mets = new Mets();
        
        String identifier = "DB-ID-" + dso.getID();
        if(dso.getHandle()!=null)
        {
            identifier = dso.getHandle().replace('/', '-');
        }
        
        // this ID should be globally unique (format: DSpace_[objType]_[handle with slash replaced with a dash])
        mets.setID("DSpace_" + Constants.typeText[dso.getType()] + "_" + identifier);

        // identifies the object described by this document
        mets.setOBJID(makePersistentID(dso));
        mets.setTYPE(getObjectTypeString(dso));

        // this is the signature by which the ingester will recognize
        // a document it can expect to interpret.
        mets.setPROFILE(getProfile());

        MetsHdr metsHdr = makeMetsHdr(context, dso, params);
        if (metsHdr != null)
        {
            mets.getContent().add(metsHdr);
        }

        // add DMD sections
        // Each type element MAY be either just a MODS-and-crosswalk name, OR
        // a combination "MODS-name:crosswalk-name" (e.g. "DC:qDC").
        String dmdTypes[] = getDmdTypes(context, dso, params);

        // record of ID of each dmdsec to make DMDID in structmap.
        String dmdId[] = new String[dmdTypes.length];
        for (int i = 0; i < dmdTypes.length; ++i)
        {
            MdSec dmdSec = makeMdSec(context, dso, DmdSec.class, dmdTypes[i], params, extraStreams);
            if (dmdSec != null)
            {
                mets.getContent().add(dmdSec);
                dmdId[i] = dmdSec.getID();
            }
        }

        // add object-wide technical/source MD segments, get ID string:
        // Put that ID in ADMID of first div in structmap.
        String objectAMDID = addAmdSec(context, dso, params, mets, extraStreams);

        // Create simple structMap: initial div represents the Object's
        // contents, its children are e.g. Item bitstreams (content only),
        // Collection's members, or Community's members.
        StructMap structMap = new StructMap();
        structMap.setID(gensym("struct"));
        structMap.setTYPE("LOGICAL");
        structMap.setLABEL("DSpace Object");
        Div div0 = new Div();
        div0.setID(gensym("div"));
        div0.setTYPE("DSpace Object Contents");
        structMap.getContent().add(div0);

        // fileSec is optional, let object type create it if needed.
        FileSec fileSec = null;

        // Item-specific manifest - license, bitstreams as Files, etc.
        if (dso.getType() == Constants.ITEM)
        {
            // this tags file ID and group identifiers for bitstreams.
            String bitstreamIDstart = "bitstream_";
            Item item = (Item)dso;

            // how to handle unauthorized bundle/bitstream:
            String unauth = (params == null) ? null : params.getProperty("unauthorized");

            // fileSec - all non-metadata bundles go into fileGrp,
            // and each bitstream therein into a file.
            // Create the bitstream-level techMd and div's for structmap
            // at the same time so we can connect the IDREFs to IDs.
            fileSec = new FileSec();
            Bundle[] bundles = item.getBundles();
            for (int i = 0; i < bundles.length; i++)
            {
                if (!includeBundle(bundles[i]))
                {
                    continue;
                }

                // unauthorized bundle?
                // NOTE: This must match the logic in disseminate()
                if (!AuthorizeManager.authorizeActionBoolean(context,
                            bundles[i], Constants.READ))
                {
                    if (unauth != null &&
                        (unauth.equalsIgnoreCase("skip")))
                    {
                        continue;
                    }
                    else
                    {
                        throw new AuthorizeException("Not authorized to read Bundle named \"" + bundles[i].getName() + "\"");
                    }
                }

                Bitstream[] bitstreams = bundles[i].getBitstreams();

                // Create a fileGrp, USE = permuted Bundle name
                FileGrp fileGrp = new FileGrp();
                String bName = bundles[i].getName();
                if ((bName != null) && !bName.equals(""))
                {
                    fileGrp.setUSE(bundleToFileGrp(bName));
                }

                // add technical metadata for a bundle
                String techBundID = addAmdSec(context, bundles[i], params, mets, extraStreams);
                if (techBundID != null)
                {
                    fileGrp.setADMID(techBundID);
                }

                // watch for primary bitstream
                int primaryBitstreamID = -1;
                boolean isContentBundle = false;
                if ((bName != null) && bName.equals("ORIGINAL"))
                {
                    isContentBundle = true;
                    primaryBitstreamID = bundles[i].getPrimaryBitstreamID();
                }

                // For each bitstream, add to METS manifest
                for (int bits = 0; bits < bitstreams.length; bits++)
                {
                    // Check for authorization.  Handle unauthorized
                    // bitstreams to match the logic in disseminate(),
                    // i.e. "unauth=zero" means include a 0-length bitstream,
                    // "unauth=skip" means to ignore it (and exclude from
                    // manifest).
                    boolean auth = AuthorizeManager.authorizeActionBoolean(context,
                            bitstreams[bits], Constants.READ);
                    if (!auth)
                    {
                        if (unauth != null && unauth.equalsIgnoreCase("skip"))
                        {
                            continue;
                        }
                        else if (!(unauth != null && unauth.equalsIgnoreCase("zero")))
                        {
                            throw new AuthorizeException("Not authorized to read Bitstream, SID=" + String.valueOf(bitstreams[bits].getSequenceID()));
                        }
                    }

                    String sid = String.valueOf(bitstreams[bits].getSequenceID());
                    String fileID = bitstreamIDstart + sid;
                    edu.harvard.hul.ois.mets.File file = new edu.harvard.hul.ois.mets.File();
                    file.setID(fileID);
                    file.setSEQ(bitstreams[bits].getSequenceID());
                    fileGrp.getContent().add(file);

                    // set primary bitstream in structMap
                    if (bitstreams[bits].getID() == primaryBitstreamID)
                    {
                        Fptr fptr = new Fptr();
                        fptr.setFILEID(fileID);
                        div0.getContent().add(0, fptr);
                    }

                    // if this is content, add to structmap too:
                    if (isContentBundle)
                    {
                        div0.getContent().add(makeFileDiv(fileID, getObjectTypeString(bitstreams[bits])));
                    }

                    /*
                     * If we're in THUMBNAIL or TEXT bundles, the bitstream is
                     * extracted text or a thumbnail, so we use the name to work
                     * out which bitstream to be in the same group as
                     */
                    String groupID = "GROUP_" + bitstreamIDstart + sid;
                    if ((bundles[i].getName() != null)
                            && (bundles[i].getName().equals("THUMBNAIL") ||
                                bundles[i].getName().startsWith("TEXT")))
                    {
                        // Try and find the original bitstream, and chuck the
                        // derived bitstream in the same group
                        Bitstream original = findOriginalBitstream(item,
                                bitstreams[bits]);
                        if (original != null)
                        {
                                groupID = "GROUP_" + bitstreamIDstart
                                    + original.getSequenceID();
                        }
                    }
                    file.setGROUPID(groupID);
                    file.setMIMETYPE(bitstreams[bits].getFormat().getMIMEType());
                    file.setSIZE(auth ? bitstreams[bits].getSize() : 0);

                    // Translate checksum and type to METS
                    String csType = bitstreams[bits].getChecksumAlgorithm();
                    String cs = bitstreams[bits].getChecksum();
                    if (auth && cs != null && csType != null)
                    {
                        try
                        {
                            file.setCHECKSUMTYPE(Checksumtype.parse(csType));
                            file.setCHECKSUM(cs);
                        }
                        catch (MetsException e)
                        {
                            log.warn("Cannot set bitstream checksum type="+csType+" in METS.");
                        }
                    }

                    // FLocat: point to location of bitstream contents.
                    FLocat flocat = new FLocat();
                    flocat.setLOCTYPE(Loctype.URL);
                    flocat.setXlinkHref(makeBitstreamURL(bitstreams[bits], params));
                    file.getContent().add(flocat);

                    // technical metadata for bitstream
                    String techID = addAmdSec(context, bitstreams[bits], params, mets, extraStreams);
                    if (techID != null)
                    {
                        file.setADMID(techID);
                    }
                }
                fileSec.getContent().add(fileGrp);
            }
        }
        else if (dso.getType() == Constants.COLLECTION)
        {
            Collection collection = (Collection)dso;
            ItemIterator ii = collection.getItems();
            while (ii.hasNext())
            {
                //add a child <div> for each item in collection
                Item item = ii.next();
                Div childDiv = makeChildDiv(getObjectTypeString(item), item, params);
                if(childDiv!=null)
                {
                    div0.getContent().add(childDiv);
                }
            }

            // add metadata & info for Template Item, if exists
            Item templateItem = collection.getTemplateItem();
            if(templateItem!=null)
            {
                String templateDmdId[] = new String[dmdTypes.length];
                // index where we should add the first template item <dmdSec>.
                // Index = number of <dmdSecs> already added + number of <metsHdr> = # of dmdSecs + 1
                // (Note: in order to be a valid METS file, all dmdSecs must be before the 1st amdSec)
                int dmdIndex = dmdTypes.length + 1;
                //For each type of dmdSec specified,
                // add a new dmdSec which contains the Template Item metadata
                // (Note: Template Items are only metadata -- they have no content files)
                for (int i = 0; i < dmdTypes.length; ++i)
                {
                    MdSec templateDmdSec = makeMdSec(context, templateItem, DmdSec.class, dmdTypes[i], params, extraStreams);
                    if (templateDmdSec != null)
                    {
                        mets.getContent().add(dmdIndex, templateDmdSec);
                        dmdIndex++;
                        templateDmdId[i] = templateDmdSec.getID();
                    }
                }

                //Now add a child <div> in structMap to represent that Template Item
                Div templateItemDiv = new Div();
                templateItemDiv.setID(gensym("div"));
                templateItemDiv.setTYPE(getObjectTypeString(templateItem) + TEMPLATE_TYPE_SUFFIX);
                //Link up the dmdSec(s) for the Template Item to this <div>
                StringBuilder templateDmdIds = new StringBuilder();
                for (String currdmdId : templateDmdId)
                {
                    templateDmdIds.append(" ").append(currdmdId);
                }
                templateItemDiv.setDMDID(templateDmdIds.substring(1));
                //add this child <div> before the listing of normal Items
                div0.getContent().add(0, templateItemDiv);
            }

            // add link to Collection Logo, if one exists
            Bitstream logoBs = collection.getLogo();
            if (logoBs != null)
            {
                fileSec = new FileSec();
                addLogoBitstream(logoBs, fileSec, div0, params);
            }
        }
        else if (dso.getType() == Constants.COMMUNITY)
        {
            // Subcommunities are directly under "DSpace Object Contents" <div>,
            // but are labeled as Communities.
            Community subcomms[] = ((Community)dso).getSubcommunities();
            for (int i = 0; i < subcomms.length; ++i)
            {
                //add a child <div> for each subcommunity in this community
                Div childDiv = makeChildDiv(getObjectTypeString(subcomms[i]), subcomms[i], params);
                if(childDiv!=null)
                {
                    div0.getContent().add(childDiv);
                }
            }
            // Collections are also directly under "DSpace Object Contents" <div>,
            // but are labeled as Collections.
            Collection colls[] = ((Community)dso).getCollections();
            for (int i = 0; i < colls.length; ++i)
            {
                //add a child <div> for each collection in this community
                Div childDiv = makeChildDiv(getObjectTypeString(colls[i]), colls[i], params);
                if(childDiv!=null)
                {
                    div0.getContent().add(childDiv);
                }
            }
            //add Community logo bitstream
            Bitstream logoBs = ((Community)dso).getLogo();
            if (logoBs != null)
            {
                fileSec = new FileSec();
                addLogoBitstream(logoBs, fileSec, div0, params);
            }
        }
        else if (dso.getType() == Constants.SITE)
        {
            // This is a site-wide <structMap>, which just lists the top-level
            // communities.  Each top level community is referenced by a div.
            Community comms[] = Community.findAllTop(context);
            for (int i = 0; i < comms.length; ++i)
            {
                //add a child <div> for each top level community in this site
                Div childDiv = makeChildDiv(getObjectTypeString(comms[i]),
                        comms[i], params);
                if(childDiv!=null)
                {
                    div0.getContent().add(childDiv);
                }
            }
        }

        //Only add the <fileSec> to the METS file if it has content.  A <fileSec> must have content.
        if (fileSec != null && fileSec.getContent()!=null && !fileSec.getContent().isEmpty())
        {
            mets.getContent().add(fileSec);
        }
        
        mets.getContent().add(structMap);

        // set links to metadata for object -- after type-specific
        // code since that can add to the object metadata.
        StringBuilder dmdIds = new StringBuilder();
        for (String currdmdId : dmdId)
        {
            dmdIds.append(" ").append(currdmdId);
        }

        div0.setDMDID(dmdIds.substring(1));
        if (objectAMDID != null)
        {
            div0.setADMID(objectAMDID);
        }

        // Does subclass have something to add to structMap?
        addStructMap(context, dso, params, mets);

        return mets;
    }

    // Install logo bitstream into METS for Community, Collection.
    // Add a file element, and refer to it from an fptr in the first div
    // of the main structMap.
    protected void addLogoBitstream(Bitstream logoBs, FileSec fileSec, Div div0, PackageParameters params)
    {
        edu.harvard.hul.ois.mets.File file = new edu.harvard.hul.ois.mets.File();
        String fileID = gensym("logo");
        file.setID(fileID);
        file.setMIMETYPE(logoBs.getFormat().getMIMEType());
        file.setSIZE(logoBs.getSize());

        // Translate checksum and type to METS
        String csType = logoBs.getChecksumAlgorithm();
        String cs = logoBs.getChecksum();
        if (cs != null && csType != null)
        {
            try
            {
                file.setCHECKSUMTYPE(Checksumtype.parse(csType));
                file.setCHECKSUM(cs);
            }
            catch (MetsException e)
            {
                log.warn("Cannot set bitstream checksum type="+csType+" in METS.");
            }
        }

        //Create <fileGroup USE="LOGO"> with a <FLocat> pointing at bitstream
        FLocat flocat = new FLocat();
        flocat.setLOCTYPE(Loctype.URL);
        flocat.setXlinkHref(makeBitstreamURL(logoBs, params));
        file.getContent().add(flocat);
        FileGrp fileGrp = new FileGrp();
        fileGrp.setUSE("LOGO");
        fileGrp.getContent().add(file);
        fileSec.getContent().add(fileGrp);

        // add fptr directly to div0 of structMap
        Fptr fptr = new Fptr();
        fptr.setFILEID(fileID);
        div0.getContent().add(0, fptr);
    }

    // create <div> element pointing to a file
    protected Div makeFileDiv(String fileID, String type)
    {
        Div div = new Div();
        div.setID(gensym("div"));
        div.setTYPE(type);
        Fptr fptr = new Fptr();
        fptr.setFILEID(fileID);
        div.getContent().add(fptr);
        return div;
    }

    /**
     * Create a <div> element with <mptr> which references a child
     * object via its handle (and via a local file name, when recursively disseminating
     * all child objects).
     * @param type - type attr value for the <div>
     * @param dso - object for which to create the div
     * @param params
     * @return a new {@code Div} with {@code dso} as child.
     */
    protected Div makeChildDiv(String type, DSpaceObject dso, PackageParameters params)
    {
        String handle = dso.getHandle();

        //start <div>
        Div div = new Div();
        div.setID(gensym("div"));
        div.setTYPE(type);
        
        //make sure we have a handle
        if (handle == null || handle.length()==0)
        {
            log.warn("METS Disseminator is skipping "+type+" without handle: " + dso.toString());
        }
        else
        {
            //create <mptr> with handle reference
            Mptr mptr = new Mptr();
            mptr.setID(gensym("mptr"));
            mptr.setLOCTYPE(Loctype.HANDLE);
            mptr.setXlinkHref(handle);
            div.getContent().add(mptr);
        }

        //determine file extension of child references,
        //based on whether we are exporting just a manifest or a full Zip pkg
        String childFileExtension  = (params.getBooleanProperty("manifestOnly", false)) ? "xml" : "zip";

        // Always create <mptr> with file-name reference to child package
        // This is what DSpace will expect the child package to be named during ingestion
        // (NOTE: without this reference, DSpace will be unable to restore any child objects during ingestion)
        Mptr mptr2 = new Mptr();
        mptr2.setID(gensym("mptr"));
        mptr2.setLOCTYPE(Loctype.URL);
        //we get the name of the child package from the Packager -- as it is what will actually create this child pkg file
        mptr2.setXlinkHref(PackageUtils.getPackageName(dso, childFileExtension));
        div.getContent().add(mptr2);

        return div;
    }

    // put handle in canonical URN format -- note that HandleManager's
    // canonicalize currently returns HTTP URL format.
    protected String getHandleURN(String handle)
    {
        if (handle.startsWith("hdl:"))
        {
            return handle;
        }
        return "hdl:"+handle;
    }

    /**
     * For a bitstream that's a thumbnail or extracted text, find the
     * corresponding bitstream it was derived from, in the ORIGINAL bundle.
     *
     * @param item
     *            the item we're dealing with
     * @param derived
     *            the derived bitstream
     *
     * @return the corresponding original bitstream (or null)
     */
    protected static Bitstream findOriginalBitstream(Item item, Bitstream derived)
        throws SQLException
    {
        Bundle[] bundles = item.getBundles();

        // Filename of original will be filename of the derived bitstream
        // minus the extension (last 4 chars - .jpg or .txt)
        String originalFilename = derived.getName().substring(0,
                derived.getName().length() - 4);

        // First find "original" bundle
        for (int i = 0; i < bundles.length; i++)
        {
            if ((bundles[i].getName() != null)
                    && bundles[i].getName().equals("ORIGINAL"))
            {
                // Now find the corresponding bitstream
                Bitstream[] bitstreams = bundles[i].getBitstreams();

                for (int bsnum = 0; bsnum < bitstreams.length; bsnum++)
                {
                    if (bitstreams[bsnum].getName().equals(originalFilename))
                    {
                        return bitstreams[bsnum];
                    }
                }
            }
        }

        // Didn't find it
        return null;
    }

    // Get result from crosswalk plugin and add it to the document,
    // including namespaces and schema.
    // returns the new/modified element upon success.
    private MetsElement crosswalkToMetsElement(DisseminationCrosswalk xwalk,
                                 DSpaceObject dso, MetsElement me)
        throws CrosswalkException,
               IOException, SQLException, AuthorizeException
    {
        try
        {
            // add crosswalk's namespaces and schemaLocation to this element:
            String raw = xwalk.getSchemaLocation();
            String sloc[] = raw == null ? null : raw.split("\\s+");
            Namespace ns[] = xwalk.getNamespaces();
            for (int i = 0; i < ns.length; ++i)
            {
                String uri = ns[i].getURI();
                if (sloc != null && sloc.length > 1 && uri.equals(sloc[0]))
                {
                    me.setSchema(ns[i].getPrefix(), uri, sloc[1]);
                }
                else
                {
                    me.setSchema(ns[i].getPrefix(), uri);
                }
            }

            // add result of crosswalk
            PreformedXML pXML = null;
            if (xwalk.preferList())
            {
                List<Element> res = xwalk.disseminateList(dso);
                if (!(res == null || res.isEmpty()))
                {
                    pXML = new PreformedXML(outputter.outputString(res));
                }
            }
            else
            {
                Element res = xwalk.disseminateElement(dso);
                if (res != null)
                {
                    pXML = new PreformedXML(outputter.outputString(res));
                }
            }
            if (pXML != null)
            {
                me.getContent().add(pXML);
                return me;
            }
            return null;
        }
        catch (CrosswalkObjectNotSupported e)
        {
            // ignore this xwalk if object is unsupported.
            if (log.isDebugEnabled())
            {
                log.debug("Skipping MDsec because of CrosswalkObjectNotSupported: dso=" + dso.toString() + ", xwalk=" + xwalk.getClass().getName());
            }
            return null;
        }
    }

    /**
     * Cleanup our license file reference links, as Deposit Licenses & CC Licenses can be
     * added two ways (and we only want to add them to zip package *once*):
     * (1) Added as a normal Bitstream (assuming LICENSE and CC_LICENSE bundles will be included in pkg)
     * (2) Added via a 'rightsMD' crosswalk (as they are rights information/metadata on an Item)
     * <p>
     * So, if they are being added by *both*, then we want to just link the rightsMD <mdRef> entry so
     * that it points to the Bitstream location.  This implementation is a bit 'hackish', but it's
     * the best we can do, as the Harvard METS API doesn't allow us to go back and crawl an entire
     * METS file to look for these inconsistencies/duplications.
     *
     * @param context current DSpace Context
     * @param params current Packager Parameters
     * @param dso current DSpace Object
     * @param mdRef the rightsMD <mdRef> element
     * @throws SQLException
     * @throws IOException
     * @throws AuthorizeException
     */
    protected void linkLicenseRefsToBitstreams(Context context, PackageParameters params,
            DSpaceObject dso, MdRef mdRef)
            throws SQLException, IOException, AuthorizeException
    {
        //If this <mdRef> is a reference to a DSpace Deposit License
        if(mdRef.getMDTYPE()!=null && mdRef.getMDTYPE()==Mdtype.OTHER &&
           mdRef.getOTHERMDTYPE()!=null && mdRef.getOTHERMDTYPE().equals("DSpaceDepositLicense"))
        {
            //Locate the LICENSE bundle
            Item i = (Item)dso;
            Bundle license[] = i.getBundles(Constants.LICENSE_BUNDLE_NAME);

            //Are we already including the LICENSE bundle's bitstreams in this package?
            if(license!=null && license.length>0 && includeBundle(license[0]))
            {
                //Since we are including the LICENSE bitstreams, lets find our LICENSE bitstream path & link to it.
                Bitstream licenseBs = PackageUtils.findDepositLicense(context, (Item)dso);
                mdRef.setXlinkHref(makeBitstreamURL(licenseBs, params));
            }
        }
        //If this <mdRef> is a reference to a Creative Commons Textual License
        else if(mdRef.getMDTYPE() != null && mdRef.getMDTYPE() == Mdtype.OTHER &&
                mdRef.getOTHERMDTYPE()!=null && mdRef.getOTHERMDTYPE().equals("CreativeCommonsText"))
        {
            //Locate the CC-LICENSE bundle
            Item i = (Item)dso;
            Bundle license[] = i.getBundles(CreativeCommons.CC_BUNDLE_NAME);

            //Are we already including the CC-LICENSE bundle's bitstreams in this package?
            if(license!=null && license.length>0 && includeBundle(license[0]))
            {
                //Since we are including the CC-LICENSE bitstreams, lets find our CC-LICENSE (textual) bitstream path & link to it.
                Bitstream ccText = CreativeCommons.getLicenseTextBitstream(i);
                mdRef.setXlinkHref(makeBitstreamURL(ccText, params));
            }
        }
        //If this <mdRef> is a reference to a Creative Commons RDF License
        else if(mdRef.getMDTYPE() != null && mdRef.getMDTYPE() == Mdtype.OTHER &&
                mdRef.getOTHERMDTYPE()!=null && mdRef.getOTHERMDTYPE().equals("CreativeCommonsRDF"))
        {
            //Locate the CC-LICENSE bundle
            Item i = (Item)dso;
            Bundle license[] = i.getBundles(CreativeCommons.CC_BUNDLE_NAME);

            //Are we already including the CC-LICENSE bundle's bitstreams in this package?
            if(license!=null && license.length>0 && includeBundle(license[0]))
            {
                //Since we are including the CC-LICENSE bitstreams, lets find our CC-LICENSE (RDF) bitstream path & link to it.
                Bitstream ccRdf = CreativeCommons.getLicenseRdfBitstream(i);
                mdRef.setXlinkHref(makeBitstreamURL(ccRdf, params));
            }
        }
    }

    /**
     * Build a string which will be used as the "Type" of this object in
     * the METS manifest.
     * <P>
     * Default format is "DSpace [Type-as-string]".
     *
     * @param dso DSpaceObject to create type-string for
     * @return a string which will represent this object Type in METS
     * @see org.dspace.core.Constants
     */
    public String getObjectTypeString(DSpaceObject dso)
    {
        //Format: "DSpace <Type-as-string>" (e.g. "DSpace ITEM", "DSpace COLLECTION", etc)
        return  "DSpace " + Constants.typeText[dso.getType()];
    }


    /**
     * Returns a user help string which should describe the
     * additional valid command-line options that this packager
     * implementation will accept when using the <code>-o</code> or
     * <code>--option</code> flags with the Packager script.
     *
     * @return a string describing additional command-line options available
     * with this packager
     */
    @Override
    public String getParameterHelp()
    {
        return  "* manifestOnly=[boolean]      " +
                   "If true, only export the METS manifest (mets.xml) and don't export content files (defaults to false)." +
                "\n\n" +
                "* unauthorized=[value]      " +
                   "If 'skip', skip over any files which the user doesn't have authorization to read. " +
                   "If 'zero', create a zero-length file for any files the user doesn't have authorization to read. " +
                   "By default, an AuthorizationException will be thrown for any files the user cannot read.";
    }

    /**
     * Get the URL by which the METS manifest refers to a Bitstream
     * member within the same package.  In other words, this is generally
     * a relative path link to where the Bitstream file is within the Zipped
     * up package.
     * <p>
     * For a manifest-only METS, this is a reference to an HTTP URL where
     * the bitstream should be able to be downloaded from.
     * 
     * @param bitstream  the Bitstream
     * @param params Packager Parameters
     * @return String in URL format naming path to bitstream.
     */
    public String makeBitstreamURL(Bitstream bitstream, PackageParameters params)
    {
        // if bare manifest, use external "persistent" URI for bitstreams
        if (params != null && (params.getBooleanProperty("manifestOnly", false)))
        {
            // Try to build a persistent(-ish) URI for bitstream
            // Format: {site-base-url}/bitstream/{item-handle}/{sequence-id}/{bitstream-name}
            try
            {
                // get handle of parent Item of this bitstream, if there is one:
                String handle = null;
                Bundle[] bn = bitstream.getBundles();
                if (bn.length > 0)
                {
                    Item bi[] = bn[0].getItems();
                    if (bi.length > 0)
                    {
                        handle = bi[0].getHandle();
                    }
                }
                if (handle != null)
                {
                    return ConfigurationManager
                                    .getProperty("dspace.url")
                            + "/bitstream/"
                            + handle
                            + "/"
                            + String.valueOf(bitstream.getSequenceID())
                            + "/"
                            + URLEncoder.encode(bitstream.getName(), "UTF-8");
                }
                else
                {   //no Handle assigned, so persistent(-ish) URI for bitstream is
                    // Format: {site-base-url}/retrieve/{bitstream-internal-id}
                    return ConfigurationManager
                                    .getProperty("dspace.url")
                            + "/retrieve/"
                            + String.valueOf(bitstream.getID());
                }
            }
            catch (SQLException e)
            {
                log.error("Database problem", e);
            }
            catch (UnsupportedEncodingException e)
            {
                log.error("Unknown character set", e);
            }

            // We should only get here if we failed to build a nice URL above
            // so, by default, we're just going to return the bitstream name.
            return bitstream.getName();
        }
        else
        {
            String base = "bitstream_"+String.valueOf(bitstream.getID());
            String ext[] = bitstream.getFormat().getExtensions();
            return (ext.length > 0) ? base+"."+ext[0] : base;
        }
    }

    /**
     * Create metsHdr element - separate so subclasses can override.
     */
    public abstract MetsHdr makeMetsHdr(Context context, DSpaceObject dso,
                               PackageParameters params);
    /**
     * Returns name of METS profile to which this package conforms, e.g.
     *  "DSpace METS DIP Profile 1.0"
     * @return string name of profile.
     */
    public abstract String getProfile();

    /**
     * Returns fileGrp's USE attribute value corresponding to a DSpace bundle name.
     *
     * @param bname name of DSpace bundle.
     * @return string name of fileGrp
     */
    public abstract String bundleToFileGrp(String bname);

    /**
     * Get the types of Item-wide DMD to include in package.
     * Each element of the returned array is a String, which
     * MAY be just a simple name, naming both the Crosswalk Plugin and
     * the METS "MDTYPE", <em>or</em> a colon-separated pair consisting of
     * the METS name followed by a colon and the Crosswalk Plugin name.
     * E.g. the type string <code>"DC:qualifiedDublinCore"</code> tells it to
     * create a METS section with <code>MDTYPE="DC"</code> and use the plugin
     * named "qualifiedDublinCore" to obtain the data.
     * @param params the PackageParameters passed to the disseminator.
     * @return array of metadata type strings, never null.
     */
    public abstract String [] getDmdTypes(Context context, DSpaceObject dso, PackageParameters params)
        throws SQLException, IOException, AuthorizeException;

    /**
     * Get the type string of the technical metadata to create for each
     * object and each Bitstream in an Item.  The type string may be a
     * simple name or colon-separated compound as specified for
     *  <code>getDmdTypes()</code> above.
     * @param params the PackageParameters passed to the disseminator.
     * @return array of metadata type strings, never null.
     */
    public abstract String[] getTechMdTypes(Context context, DSpaceObject dso, PackageParameters params)
        throws SQLException, IOException, AuthorizeException;

    /**
     * Get the type string of the source metadata to create for each
     * object and each Bitstream in an Item.  The type string may be a
     * simple name or colon-separated compound as specified for
     * <code>getDmdTypes()</code> above.
     * @param params the PackageParameters passed to the disseminator.
     * @return array of metadata type strings, never null.
     */
    public abstract String[] getSourceMdTypes(Context context, DSpaceObject dso, PackageParameters params)
        throws SQLException, IOException, AuthorizeException;

    /**
     * Get the type string of the "digiprov" (digital provenance)
     * metadata to create for each object and each Bitstream in an Item.
     * The type string may be a simple name or colon-separated compound
     * as specified for <code>getDmdTypes()</code> above.
     *
     * @param params the PackageParameters passed to the disseminator.
     * @return array of metadata type strings, never null.
     */
    public abstract String[] getDigiprovMdTypes(Context context, DSpaceObject dso, PackageParameters params)
        throws SQLException, IOException, AuthorizeException;

    /**
     * Get the type string of the "rights" (permission and/or license)
     * metadata to create for each object and each Bitstream in an Item.
     * The type string may be a simple name or colon-separated compound
     * as specified for <code>getDmdTypes()</code> above.
     *
     * @param params the PackageParameters passed to the disseminator.
     * @return array of metadata type strings, never null.
     */
    public abstract String[] getRightsMdTypes(Context context, DSpaceObject dso, PackageParameters params)
        throws SQLException, IOException, AuthorizeException;

    /**
     * Add any additional <code>structMap</code> elements to the
     * METS document, as required by this subclass.  A simple default
     * structure map which fulfills the minimal DSpace METS DIP/SIP
     * requirements is already present, so this does not need to do anything.
     * @param mets the METS document to which to add structMaps
     */
    public abstract void addStructMap(Context context, DSpaceObject dso,
                               PackageParameters params, Mets mets)
        throws SQLException, IOException, AuthorizeException, MetsException;

    /**
     * @return true when this bundle should be included as "content"
     *  in the package.. e.g. DSpace SIP does not include metadata bundles.
     */
    public abstract boolean includeBundle(Bundle bundle);
}
