org.cytobank.acs.core.TableOfContents.java Source code

Java tutorial

Introduction

Here is the source code for org.cytobank.acs.core.TableOfContents.java

Source

/*
 *  This file is part the Cytobank ACS Library.
 *  Copyright (C) 2010 Cytobank, Inc.  All rights reserved.
 *
 *  The Cytobank ACS Library program is free software:
 *  you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.cytobank.acs.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.lang.StringUtils;
import org.cytobank.acs.core.exceptions.DuplicateFileResourceIdentifierException;
import org.cytobank.acs.core.exceptions.InvalidAssociationException;
import org.cytobank.acs.core.exceptions.InvalidFileResourceUriSchemeException;
import org.cytobank.acs.core.exceptions.InvalidIndexException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * This class represents and provides convenience methods for an ACS table of contents xml file found within
 * an ACS file.
 *
 * @author Chad Rosenberg <chad@cytobank.org>
 * @see <a href="http://flowcyt.sourceforge.net/acs/latest.pdf">Archival Cytometry Standard specification</a>
 */

public class TableOfContents extends AdditionalInfoElementWrapper {

    /** The version of table of contents that this instance represents. */
    protected int version;

    /** The <code>ACS</code> instance that this <code>TableOfContents</code> is owned by. */
    protected ACS acs;

    /** The xml <code>org.w3c.dom.Document</code> that this <code>TableOfContents</code> is associated with. */
    protected Document tableOfContentsDoc;

    /** The <code>FileResourceIdentifiers</code> that this <code>TableOfContents</code> contains. */
    protected Vector<FileResourceIdentifier> fileResourceIdentifiers;

    /** A <code>HashMap</code> indexing the list of <code>FileResourceIdentifier</code>s by a <String> uri. */
    protected HashMap<String, FileResourceIdentifier> fileResourceIdentifiersByUri;

    /**
     * Creates a <code>TableOfContents</code> instance from an ACS table of contents xml file.
     * <p>
     * NOTE: The preferred method for creating an instance of <code>TableOfContents</code> is to use the
     * {@link ACS#createNextTableOfContents()} method.
     *
     * @param tableOfContentsXml a <code>File</code> pointing to the ACS xml table of contents xml file
     * @param acs the <code>ACS</code> instance that the created <code>TableOfContents</code> will be owned by
     * @param version the version of the <code>TableOfContents</code> to be created
     * @throws InvalidIndexException If the file version could not be parsed, or does not conform to the spec.
     * @throws URISyntaxException If there is a problem with any of the URIs
     * @throws InvalidAssociationException if there is an invalid association
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws SAXException If there was a problem parsing the xml
     * @see ACS#createNextTableOfContents
     * @see <a href="http://flowcyt.sourceforge.net/acs/latest.pdf">Archival Cytometry Standard specification</a>
     */
    public TableOfContents(File tableOfContentsXml, ACS acs, int version)
            throws InvalidIndexException, URISyntaxException, InvalidAssociationException,
            DuplicateFileResourceIdentifierException, SAXException {
        this.acs = acs;
        this.version = version;
        FileInputStream fileInputStream;
        try {
            fileInputStream = new FileInputStream(tableOfContentsXml);
            parseXml(fileInputStream);
            fileInputStream.close();
        } catch (IOException ioe) {
            throw new InvalidIndexException(ioe.toString());
        }
    }

    /**
     * Creates a <code>TableOfContents</code> instance from an <code>InputStream</code> that contains an ACS table of contents xml.
     * <p>
     * NOTE: The preferred method for creating an instance of <code>TableOfContents</code> is to use the
     * {@link ACS#createNextTableOfContents()} method.
     *
     * @param tableOfContentsXmlStream an <code>InputStream</code> that contains an ACS table of contents xml file
     * @param version the version of the <code>TableOfContents</code> to be created
     * @throws InvalidIndexException If the file version could not be parsed, or does not conform to the spec.
     * @throws URISyntaxException If there is a problem with any of the URIs
     * @throws InvalidAssociationException if there is an invalid association
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws SAXException If there was a problem parsing the xml
     * @see ACS#createNextTableOfContents
     * @see <a href="http://flowcyt.sourceforge.net/acs/latest.pdf">Archival Cytometry Standard specification</a>
     */
    public TableOfContents(InputStream tableOfContentsXmlStream, int version)
            throws InvalidIndexException, IOException, URISyntaxException, InvalidAssociationException,
            DuplicateFileResourceIdentifierException, SAXException {
        this.version = version;
        parseXml(tableOfContentsXmlStream);
    }

    /**
     * Creates a copy of a <code>TableOfContents</code> from another while allowing a new version to be specified.
     *
     * @param tableOfContents the <code>TableOfContents</code> to copy
     * @param version the version of the new instance
     * @throws InvalidIndexException If the file version could not be parsed, or does not conform to the spec.
     * @throws URISyntaxException If there is a problem with any of the URIs
     * @throws InvalidAssociationException if there is an invalid association
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws SAXException If there was a problem parsing the xml
     */
    public TableOfContents(TableOfContents tableOfContents, int version)
            throws InvalidIndexException, URISyntaxException, InvalidAssociationException,
            DuplicateFileResourceIdentifierException, SAXException {
        this.acs = tableOfContents.acs;
        this.version = version;
        FileInputStream fileInputStream;
        try {
            File previousXml = acs.tempFile();
            tableOfContents.writeXml(previousXml);
            fileInputStream = new FileInputStream(previousXml);
            parseXml(fileInputStream);
            fileInputStream.close();
        } catch (IOException ioe) {
            throw new InvalidIndexException(ioe.toString());
        }
    }

    /**
     * Parses an ACS table of contents xml from an <code>InputStream</code> and sets the <code>tableOfContentsDoc</code> and <code>element</code>
     * element variables for this instance from the xml parser.
     *
     * @param tableOfContentsXmlStream the table of contents
     * @throws InvalidIndexException If the file version could not be parsed, or does not conform to the spec.
     * @throws IOException If an input or output exception occurred
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws SAXException If there was a problem parsing the xml
     */
    protected void parseXml(InputStream tableOfContentsXmlStream)
            throws InvalidIndexException, IOException, URISyntaxException, InvalidAssociationException,
            DuplicateFileResourceIdentifierException, SAXException {
        try {

            DocumentBuilder docBuilder = getDocumentBuilder();

            tableOfContentsDoc = docBuilder.parse(tableOfContentsXmlStream);
            element = tableOfContentsDoc.getDocumentElement();
            setupFileResourceIdentifiers();
            setupAdditionalInfo();

        } catch (ParserConfigurationException pce) {
            throw new InvalidIndexException(pce.toString());
        }

    }

    /**
     * <p>Creates a DocumentBuilder with Cytobank's preferred security settings
     * applied to it. Specifically turning off external entities and external
     * DTDs to prevent External Entity Exploits (XXE)</p>
     *
     * @throws ParserConfigurationException
     * @return DocumentBuilder
     */
    protected DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

        DocumentBuilder db = null;

        String FEATURE = null;

        // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
        // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
        FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
        dbf.setFeature(FEATURE, true);

        // If you can't completely disable DTDs, then at least do the following:
        // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
        // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
        // JDK7+ - http://xml.org/sax/features/external-general-entities
        FEATURE = "http://xml.org/sax/features/external-general-entities";
        dbf.setFeature(FEATURE, false);

        // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
        // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
        // JDK7+ - http://xml.org/sax/features/external-parameter-entities
        FEATURE = "http://xml.org/sax/features/external-parameter-entities";
        dbf.setFeature(FEATURE, false);

        // Disable external DTDs as well
        FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
        dbf.setFeature(FEATURE, false);

        // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" (see reference below)
        dbf.setXIncludeAware(false);
        dbf.setExpandEntityReferences(false);

        // And, per Timothy Morgan: "If for some reason support for inline DOCTYPEs are a requirement, then
        // ensure the entity settings are disabled (as shown above) and beware that SSRF attacks
        // (http://cwe.mitre.org/data/definitions/918.html) and denial
        // of service attacks (such as billion laughs or decompression bombs via "jar:") are a risk."

        boolean namespaceAware = true;
        boolean xsdValidate = false;
        boolean ignoreWhitespace = false;
        boolean ignoreComments = false;
        boolean putCDATAIntoText = false;
        boolean createEntityRefs = false;

        dbf.setNamespaceAware(namespaceAware);
        dbf.setValidating(xsdValidate);
        dbf.setIgnoringComments(ignoreComments);
        dbf.setIgnoringElementContentWhitespace(ignoreWhitespace);
        dbf.setCoalescing(putCDATAIntoText);
        dbf.setExpandEntityReferences(createEntityRefs);

        db = dbf.newDocumentBuilder();

        return db;

    }

    /**
     * Returns the <code>ACS</code> instance that this <code>TableOfContents</code> is owned by.
     *
     * @return the <code>ACS</code> that this <code>TableOfContents</code> is owned by
     */
    public ACS getAcs() {
        return acs;
    }

    /**
     * Sets the <code>ACS</code> instance that this <code>TableOfContents</code> is owned by.
     *
     * @param acs the <code>ACS</code> instance that this <code>TableOfContents</code>
     */
    public void setAcs(ACS acs) {
        this.acs = acs;
    }

    /**
     * Gets the ACS index file version that this <code>TableOfContents</code> represents.
     *
     * @return index file version
     * @see <a href="http://flowcyt.sourceforge.net/acs/latest.pdf">Archival Cytometry Standard specification</a>
     */
    public int getVersion() {
        return version;
    }

    /**
     * Returns the <code>TableOfContents</code> that is parent to this one.
     * <p>
     * NOTE: The data that the <code>TableOfContents</code> uses does not use inheritance and
     * the parent <code>TableOfContents</code> instance is not necessary to derive any
     * of the data represented by this instance.
     *
     * @return the parent <code>TableOfContents</code> to this one
     * @see <a href="http://flowcyt.sourceforge.net/acs/latest.pdf">Archival Cytometry Standard specification</a>
     */
    public TableOfContents getParentTableOfContents() {
        // TODO
        return null;
    }

    /**
     * Returns the number of <code>FileResourceIdentifier</code>s contained within this <code>TableOfContents</code> instance.
     *
     * @return the number of <code>FileResourceIdentifier</code>s
     */
    public int getNumberOfFileResourceIdentifiers() {
        return fileResourceIdentifiers.size();
    }

    /**
     * Returns an array of all the <code>FileResourceIdentifier</code>s contained within this <code>TableOfContents</code> instance.
     *
     * @return an array of all the <code>FileResourceIdentifier</code>s
     */
    public FileResourceIdentifier[] getFileResourceIdentifiers() {
        FileResourceIdentifier[] results = new FileResourceIdentifier[fileResourceIdentifiers.size()];
        fileResourceIdentifiers.toArray(results);
        return results;
    }

    /**
     * Returns an array of all the <code>FileResourceIdentifier</code>s contained within this <code>TableOfContents</code> instance that are associated to
     * another FileResourceIdentifier.
     * <p>
     * This method essentially takes all <code>FileResourceIdentifier</code>s and filters them down to the results that have an association with the specified
     * <code>associatedTo</code> <code>FileResourceIdentifier</code>.  This is particularly useful if all <code>FileResourceIdentifier</code>s are needed for a specific workspace.
     *
     * @return an array of all the <code>FileResourceIdentifier</code>s
     */
    public FileResourceIdentifier[] getFileResourceIdentifiersAssociatedTo(FileResourceIdentifier associatedTo)
            throws InvalidAssociationException, InvalidIndexException, URISyntaxException {
        if (associatedTo == null)
            return null;

        Vector<FileResourceIdentifier> associatedFileResourceIdentifiers = new Vector<FileResourceIdentifier>();

        for (FileResourceIdentifier fileResource : fileResourceIdentifiers) {
            for (Association association : fileResource.associations) {
                if (associatedTo.equals(association.getAssociatedTo())) {
                    associatedFileResourceIdentifiers.add(fileResource);
                    break;
                }
            }
        }

        FileResourceIdentifier[] results = new FileResourceIdentifier[associatedFileResourceIdentifiers.size()];
        associatedFileResourceIdentifiers.toArray(results);
        return results;
    }

    /**
     * Returns an array of all the <code>FileResourceIdentifier</code>s contained within this <code>TableOfContents</code> instance
     * that represent FCS files.
     *
     * @return an array of all the <code>FileResourceIdentifier</code>s that are fcs files.
     */
    public FileResourceIdentifier[] getFcsFiles() {
        Vector<FileResourceIdentifier> fcsFiles = new Vector<FileResourceIdentifier>();

        for (FileResourceIdentifier fileResource : fileResourceIdentifiers) {
            if (fileResource.isFcsFile()) {
                fcsFiles.add(fileResource);
            }
        }

        FileResourceIdentifier[] results = new FileResourceIdentifier[fcsFiles.size()];
        fileResourceIdentifiers.toArray(results);

        return results;
    }

    /**
     * Returns an array of all the <code>FileResourceIdentifier</code>s contained within this <code>TableOfContents</code> instance
     * that represent FCS files with an association to a particular <code>FileResourceIdentifier</code>.
     * <p>
     * This method is particularly useful when all FCS files associated with a given workspace FileResourceIdentifier are needed.
     *
     * @return an array of all the <code>FileResourceIdentifier</code>s that are FCS files, or <code>null</code> if <code>associatedTo</code> parameter
     *         is <code>null</code>.
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     */
    public FileResourceIdentifier[] getFcsFilesAssociatedTo(FileResourceIdentifier associatedTo)
            throws InvalidAssociationException, InvalidIndexException, URISyntaxException {
        if (associatedTo == null)
            return null;

        Vector<FileResourceIdentifier> fcsFiles = new Vector<FileResourceIdentifier>();

        for (FileResourceIdentifier fileResource : fileResourceIdentifiers) {
            if (fileResource.isFcsFile()) {
                for (Association association : fileResource.associations) {
                    if (associatedTo.equals(association.getAssociatedTo())) {
                        fcsFiles.add(fileResource);
                        break;
                    }
                }
            }
        }

        FileResourceIdentifier[] results = new FileResourceIdentifier[fcsFiles.size()];
        fcsFiles.toArray(results);

        return results;
    }

    /**
     * Returns an  <code>FileResourceIdentifier</code> contained within this <code>TableOfContents</code> instance
     * that represent a gating files (optionally with an association to a particular <code>FileResourceIdentifier</code>).
     * <p>
     * This method is particularly useful when finding a gating ml associated with an experiment
     * and if the file does not use a constant, unassociated, unversioned name (i.e. when importing newer, more compliant acs files)
     *
     * @return an <code>FileResourceIdentifier</code>s that is a gating ML
     * @param  associatedTo if <code>associatedTo</code> parameter is <code>null</code>, will return first xml file of type "gating description" otherwise toc:with must match
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     */
    public FileResourceIdentifier getGatingFileAssociatedTo(FileResourceIdentifier associatedTo)
            throws InvalidAssociationException, InvalidIndexException, URISyntaxException {

        new Vector<FileResourceIdentifier>();

        for (FileResourceIdentifier fileResource : fileResourceIdentifiers) {
            if (fileResource.isXMLFile()) {
                for (Association association : fileResource.associations) {
                    if (association.getRelationship().equals(RelationshipTypes.GATING_DESCRIPTION)) {
                        if (associatedTo == null) {
                            return fileResource; // first anon match wins
                        } else if (associatedTo.equals(association.getAssociatedTo()) == true) {
                            return fileResource; // exact match (if provided) wins
                        }
                    }
                }
            }
        }

        // still here?  return null
        return null;
    }

    /**
     * Returns an array of all unique <code>FileResourceIdentifier</code>s contained within this <code>TableOfContents</code> instance
     * that have been identified as project workspaces.
     *
     * @return an array of all the <code>FileResourceIdentifier</code>s that are project workspaces
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     * @see RelationshipTypes#isProjectWorkspace
     */
    public FileResourceIdentifier[] getProjectWorkspaces()
            throws InvalidAssociationException, InvalidIndexException, URISyntaxException {
        HashSet<FileResourceIdentifier> projectWorkspaces = new HashSet<FileResourceIdentifier>();

        // Find all fileResource associations and add the associated file to projectWorkspaces
        // if that association relationship is a project workspace.
        for (FileResourceIdentifier fileResource : fileResourceIdentifiers) {
            for (Association association : fileResource.associations) {
                if (RelationshipTypes.isProjectWorkspace(association.getRelationship())) {
                    projectWorkspaces.add(association.getAssociatedTo());
                }
            }
        }

        FileResourceIdentifier[] results = new FileResourceIdentifier[projectWorkspaces.size()];
        projectWorkspaces.toArray(results);

        return results;
    }

    /**
     * Returns a <code>FileResourceIdentifier</code> specified by the given <code>String</code> uri.
     *
     * @param uri A case-insensitive string uri. A scheme type is expected (file://, http://, etc).
     * @return a <code>FileResourceIdentifier</code>s or null if it could not be found
     */
    public FileResourceIdentifier getFileResourceIdentifierByUri(String uri) {
        return fileResourceIdentifiersByUri.get(uri.toLowerCase());
    }

    /**
     * Returns a <code>FileResourceIdentifier</code> specified by the given <code>URI</code>.
     *
     * @param uri A case-insensitive <code>URI</code>
     * @return a <code>FileResourceIdentifier</code>s or null if it could not be found
     */
    public FileResourceIdentifier getFileResourceIdentifierByUri(URI uri) {
        return getFileResourceIdentifierByUri(uri.toString());
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local file.
     *
     * @param resourcePath the <code>String</code> uri of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourceFilePath the <code>String</code> path to the file that the <code>FileResourceIdentifier</code> will represent
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     */
    public FileResourceIdentifier createFileResourceIdentifier(String resourcePath, String sourceFilePath)
            throws InvalidIndexException, URISyntaxException, IOException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        return createFileResourceIdentifier(new URI(resourcePath), new File(sourceFilePath));
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local file.
     *
     * @param resourcePath the <code>String</code> uri of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourceFile the <code>File</code> that the <code>FileResourceIdentifier</code> will represent
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     */
    public FileResourceIdentifier createFileResourceIdentifier(String resourcePath, File sourceFile)
            throws InvalidIndexException, URISyntaxException, IOException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        return createFileResourceIdentifier(new URI(resourcePath), sourceFile);
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local file with a specified MIME type.
     * <p>
     * NOTE: "application/vnd.isac.fcs" is the expected MIME type to be used in the case of FCS files.  (Available in {@link Constants#FCS_FILE_MIME_TYPE}.)
     *
     * @param resourcePath the <code>String</code> uri of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourceFile the <code>File</code> that the <code>FileResourceIdentifier</code> will represent
     * @param mimeType the MIME type to be associated with the file
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     */
    public FileResourceIdentifier createFileResourceIdentifier(String resourcePath, File sourceFile,
            String mimeType)
            throws InvalidIndexException, URISyntaxException, IOException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        return createFileResourceIdentifier(new URI(resourcePath), sourceFile, mimeType);
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local file.
     *
     * @param resourcePath the case-insensitive <code>URI</code> of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourceFile the <code>File</code> that the <code>FileResourceIdentifier</code> will represent
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     */
    public FileResourceIdentifier createFileResourceIdentifier(URI resourcePath, File sourceFile)
            throws InvalidIndexException, URISyntaxException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException, IOException {
        FileResourceIdentifier fileResource = new FileResourceIdentifier(this, sourceFile);
        fileResource.setUri(resourcePath);
        trackFileResourceIdentifier(fileResource);

        // Now add the xml element that fileResource represents to this instance of TableOfContents's xml element
        element.appendChild(fileResource.element);
        return fileResource;
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from an <code>InputStream</code>. The
     * <code>sourceFileStream</code> parameter will not be used until {@link FileResourceIdentifier#writeRepresentedFile} is called, which happens on
     * a {@link ACS#writeAcsContainer} call.
     *
     * @param resourcePath the case-insensitive <code>URI</code> of the resource, which must be unique within this <code>TableOfContents</code>.
     * @param sourceFileStream the <code>InputStream</code> that the <code>FileResourceIdentifier</code> will represent
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws IOException If an input or output exception occurred
     */
    public FileResourceIdentifier createFileResourceIdentifier(URI resourcePath, InputStream sourceFileStream)
            throws InvalidIndexException, URISyntaxException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        FileResourceIdentifier fileResource = new FileResourceIdentifier(this, sourceFileStream);
        fileResource.setUri(resourcePath);
        trackFileResourceIdentifier(fileResource);

        // Now add the xml element that fileResource represents to this instance of TableOfContents's xml element
        element.appendChild(fileResource.element);
        return fileResource;
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local fcs.
     * <p>
     * NOTE: "application/vnd.isac.fcs" is the expected MIME type to be used in the case of FCS files. (Available in {@link Constants#FCS_FILE_MIME_TYPE}.)
     *
     * @param resourcePath the case-insensitive <code>URI</code> of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourceFile the <code>File</code> that the <code>FileResourceIdentifier</code> will represent
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @see Constants#FCS_FILE_MIME_TYPE
     */
    public FileResourceIdentifier createFcsFileResourceIdentifier(URI resourcePath, File sourceFile)
            throws InvalidIndexException, URISyntaxException, IOException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        return createFileResourceIdentifier(resourcePath, sourceFile, Constants.FCS_FILE_MIME_TYPE);
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local fcs file.
     * <p>
     * NOTE: "application/vnd.isac.fcs" is the expected MIME type to be used in the case of FCS files. (Available in {@link Constants#FCS_FILE_MIME_TYPE}.)
     *
     * @param resourcePath the case-insensitive <code>String</code> uri of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourcePath the <code>String</code> file path to the file that the <code>FileResourceIdentifier</code> will represent
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @see Constants#FCS_FILE_MIME_TYPE
     */
    public FileResourceIdentifier createFcsFileResourceIdentifier(String resourcePath, String sourcePath)
            throws InvalidIndexException, URISyntaxException, IOException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        URI resourcePathUri = new URI(resourcePath);
        File sourceFile = new File(sourcePath);
        return createFileResourceIdentifier(resourcePathUri, sourceFile, Constants.FCS_FILE_MIME_TYPE);
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local file with a specified MIME type.
     * <p>
     * NOTE: "application/vnd.isac.fcs" is the expected MIME type to be used in the case of FCS files. (Available in {@link Constants#FCS_FILE_MIME_TYPE}.)
     *
     * @param resourcePath the case-insensitive <code>String</code> uri of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourcePath the <code>String</code> file path to the file that the <code>FileResourceIdentifier</code> will represent
     * @param mimeType the MIME type to be associated with the file
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @see Constants#FCS_FILE_MIME_TYPE
     */
    public FileResourceIdentifier createFileResourceIdentifier(String resourcePath, String sourcePath,
            String mimeType)
            throws InvalidIndexException, URISyntaxException, IOException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        URI resourcePathUri = new URI(resourcePath);
        File sourceFile = new File(sourcePath);
        return createFileResourceIdentifier(resourcePathUri, sourceFile, mimeType);
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from a local file with a specified MIME type.
     * <p>
     * NOTE: "application/vnd.isac.fcs" is the expected MIME type to be used in the case of FCS files. (Available in {@link Constants#FCS_FILE_MIME_TYPE}.)
     *
     * @param resourcePath the case-insensitive <code>URI</code> of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourceFile the <code>File</code> that the <code>FileResourceIdentifier</code> will represent
     * @param mimeType the MIME type to be associated with the file
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws IOException If an input or output exception occurred
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @see Constants#FCS_FILE_MIME_TYPE
     */
    public FileResourceIdentifier createFileResourceIdentifier(URI resourcePath, File sourceFile, String mimeType)
            throws InvalidIndexException, URISyntaxException, IOException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        FileResourceIdentifier fileResource = createFileResourceIdentifier(resourcePath, sourceFile);
        if (!StringUtils.isBlank(mimeType))
            fileResource.setMimeType(mimeType);
        return fileResource;
    }

    /**
     * Creates a new <code>FileResourceIdentifier</code> that will be owned by this <code>TableOfContents</code> from an <code>InputStream</code> with a specified MIME type.  The
     * <code>sourceFileStream</code> parameter will not be used until {@link FileResourceIdentifier#writeRepresentedFile} is called, which happens on
     * a {@link ACS#writeAcsContainer} call. The <code>sourceFileStream</code> parameter will remain open until {@link ACS#writeAcsContainer}
     * or {@link FileResourceIdentifier#close} is called.
     * <p>
     * NOTE: "application/vnd.isac.fcs" is the expected MIME type to be used in the case of FCS files.  (Available in {@link Constants#FCS_FILE_MIME_TYPE}.)
     *
     * @param resourcePath the case-insensitive <code>URI</code> of the resource, which must be unique within this <code>TableOfContents</code>
     * @param sourceFileStream the <code>InputStream</code> that the <code>FileResourceIdentifier</code> will represent
     * @param mimeType the MIME type to be associated with the file
     * @return a new <code>FileResourceIdentifier</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     * @throws InvalidFileResourceUriSchemeException if the resourcePath <code>URI</code> contains a scheme that is not allowed according to the ACS specification.
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws IOException If an input or output exception occurred
     * @see Constants#FCS_FILE_MIME_TYPE
     */
    public FileResourceIdentifier createFileResourceIdentifier(URI resourcePath, InputStream sourceFileStream,
            String mimeType) throws InvalidIndexException, URISyntaxException, InvalidAssociationException,
            InvalidFileResourceUriSchemeException, DuplicateFileResourceIdentifierException {
        FileResourceIdentifier fileResource = createFileResourceIdentifier(resourcePath, sourceFileStream);
        fileResource.setMimeType(mimeType);
        return fileResource;
    }

    /**
     * Gets the xml file name of this <code>TableOfContents</code>, conforming to the ACS specification.
      *
     * @return the xml file name of this <code>TableOfContents</code>
     * @see <a href="http://flowcyt.sourceforge.net/acs/latest.pdf">Archival Cytometry Standard specification</a>
     */
    public String getFileName() {
        return Constants.TOC_PREFIX + this.version + Constants.TOC_SUFFIX;
    }

    /**
     * Tracks a <code>FileResourceIdentifier</code> against this <code>TableOfContents</code> instance so the xml doesn't have to be parsed and <code>FileResourceIdentifier</code>
     * rebuilt every time it is needed.  <code>FileResourceIdentifier</code> with duplicate URIs will be blocked.
     *
     * @param fileResource the <code>FileResourceIdentifier</code> to add
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     */
    protected void trackFileResourceIdentifier(FileResourceIdentifier fileResource)
            throws InvalidIndexException, URISyntaxException, DuplicateFileResourceIdentifierException {
        String uriKey = uriKey(fileResource);

        if (fileResourceIdentifiersByUri.containsKey(uriKey))
            throw new DuplicateFileResourceIdentifierException("Cannot add duplicate URI " + fileResource.getUri());

        fileResourceIdentifiers.add(fileResource);
        fileResourceIdentifiersByUri.put(uriKey, fileResource);
    }

    /**
     * No longer tracks a <code>FileResourceIdentifier</code> against this <code>TableOfContents</code> instance.
     *
     * @param fileResource the <code>FileResourceIdentifier</code> to remove
     * @return <code>true</code> if the <code>FileResourceIdentifier</code> instance was successfully removed, <code>false</code> otherwise
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     */
    protected boolean untrackFileResourceIdentifier(FileResourceIdentifier fileResource)
            throws InvalidIndexException, URISyntaxException {
        String uriKey = uriKey(fileResource);

        boolean success = fileResourceIdentifiers.removeElement(fileResource);
        if (success)
            fileResourceIdentifiersByUri.remove(uriKey);

        return success;
    }

    /**
     * Removes a <code>FileResourceIdentifier</code> from this <code>TableOfContents</code> instance.
     *
     * @param fileResourceIdentifier the <code>FileResourceIdentifier</code> to remove
     * @return <code>true</code> if the <code>FileResourceIdentifier</code> instance was successfully removed, <code>false</code> otherwise
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code>
     */
    public boolean removeFileResourceIdentifier(FileResourceIdentifier fileResourceIdentifier)
            throws InvalidIndexException, URISyntaxException {
        boolean success = untrackFileResourceIdentifier(fileResourceIdentifier);

        if (success) {
            // Remove the fileResourceIdentifier from the xml
            success &= removeElementWrapper(fileResourceIdentifier);
        }

        return success;
    }

    /**
     * Creates a new <code>TableOfContents</code> containing all the contents of this one with a version one greater than this ones.
     * <p>
     * NOTE: This method does not add the newly created <code>TableOfContents</code> back to this <code>TableOfContents</code>'s
     * owning <code>ACS</code> instance.  If you need to do that, it is suggested that you use the {@link ACS#createNextTableOfContents()} method
     * instead.
     *
     * @return a new <code>TableOfContents</code> containing all the contents of this one with a version one greater than this ones
     * @throws IOException If an input or output exception occurred
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws SAXException If there was a problem parsing the xml
     * @see ACS#createNextTableOfContents()
     */
    public TableOfContents nextVersion()
            throws IOException, InvalidIndexException, InvalidIndexException, URISyntaxException,
            InvalidAssociationException, DuplicateFileResourceIdentifierException, SAXException {
        File tableOfContentsXmlFile = acs.tempFile();
        writeXml(tableOfContentsXmlFile);

        // Use the copy constructor to create a new version of this instance.
        TableOfContents results = new TableOfContents(this, version + 1);

        return results;
    }

    /**
     * Returns a <code>String</code> containing the XML that represents this <code>TableOfContents</code>.
     *
     * @return XML that represents this <code>TableOfContents</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     */
    public String toXml() throws InvalidIndexException {
        try {
            Source source = new DOMSource(tableOfContentsDoc);
            StringWriter stringWriter = new StringWriter();
            Result result = new StreamResult(stringWriter);
            TransformerFactory factory = TransformerFactory.newInstance();
            Transformer transformer = factory.newTransformer();
            transformer.transform(source, result);
            String xmlResult = stringWriter.getBuffer().toString();

            return xmlResult;
        } catch (TransformerException te) {
            throw new InvalidIndexException(te.toString());
        }
    }

    /**
     * Writes out the XML that represents this <code>TableOfContents</code> to a <code>File</code>.
     *
     * @param targetFile the <code>File</code> to write out the xml to
     * @throws IOException If an input or output exception occurred
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     */
    public void writeXml(File targetFile) throws IOException, InvalidIndexException {
        FileOutputStream fileOutputStream = new FileOutputStream(targetFile);
        try {
            writeXml(fileOutputStream);
        } finally {
            fileOutputStream.close();
        }
    }

    /**
     * Writes out the XML that represents this <code>TableOfContents</code> to an <code>OutputStream</code>.
     *
     * @param outputStream the <code>OutputStream</code> to write out the xml to
     * @throws IOException If an input or output exception occurred
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     */
    public void writeXml(OutputStream outputStream) throws IOException, InvalidIndexException {
        PrintStream out = new PrintStream(outputStream, true);
        out.print(toXml());
    }

    /**
     * Sets up <code>fileResourceIdentifiers</code>, <code>fileResourceIdentifiersByUri</code> from the <code>element</code>
     *
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws URISyntaxException If there is a problem with any of the URIs
     * @throws InvalidAssociationException if there is an invalid association
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     */
    protected void setupFileResourceIdentifiers() throws InvalidIndexException, URISyntaxException,
            InvalidAssociationException, DuplicateFileResourceIdentifierException {
        NodeList fileNodes = element.getElementsByTagName(Constants.FILE_ELEMENT);
        int numberOfFiles = (fileNodes == null) ? 0 : fileNodes.getLength();

        fileResourceIdentifiers = new Vector<FileResourceIdentifier>(numberOfFiles);
        fileResourceIdentifiersByUri = new HashMap<String, FileResourceIdentifier>(numberOfFiles);

        for (int i = 0; i < numberOfFiles; i++) {
            Element fileResourceElement = (Element) fileNodes.item(i);
            FileResourceIdentifier fileResource = new FileResourceIdentifier(this, fileResourceElement);
            trackFileResourceIdentifier(fileResource);
        }

    }

    /**
     * Renames a <code>FileResourceIdentifier</code> from an old uri to a new uri.  This should be done publicly through {@link FileResourceIdentifier#setUri(URI)}.
     *
     * @param oldUri the old <code>URI</code>
     * @param newUri the new <code>URI</code>
     * @throws DuplicateFileResourceIdentifierException if a <code>FileResourceIdentifier</code> with the same <code>URI</code> already exists in
     * this <code>TableOfContents</code> instance
     * @throws URISyntaxException
     * @throws InvalidIndexException
     */
    protected void renameFileResourceIdentifier(URI oldUri, URI newUri)
            throws DuplicateFileResourceIdentifierException, InvalidIndexException, URISyntaxException {
        String oldUriKey = uriKey(oldUri);
        String newUriKey = uriKey(newUri);

        if (fileResourceIdentifiersByUri.containsKey(newUriKey))
            throw new DuplicateFileResourceIdentifierException("Cannot add duplicate URI " + newUri);

        FileResourceIdentifier fileResourceIdentifier = fileResourceIdentifiersByUri.remove(oldUriKey);
        if (fileResourceIdentifier != null)
            fileResourceIdentifiersByUri.put(newUriKey, fileResourceIdentifier);
    }

    /**
     * Closes any open streams in any <code>FileResourceIdentifiers</code>.
     * @see ACS#close()
     * @see FileResourceIdentifier#close()
     */
    public void close() {
        for (FileResourceIdentifier fileResourceIdentifier : fileResourceIdentifiers) {
            fileResourceIdentifier.close();
        }
    }

    /**
     * Creates a new blank instance of <code>TableOfContents</code> with no associated <code>ACS</code> file.
     *
     * <p>
     * NOTE: The preferred method for creating an instance of <code>TableOfContents</code> is to use the
     * {@link ACS#createNextTableOfContents()} method.  This is just a convenience method to build a
     * <code>TableOfContents</code> instance without worrying about the constructor requirements.
     *
     * @return a new blank instance of <code>TableOfContents</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     * @throws IOException If an input or output exception occurred
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code> or if the URI is a duplicate
     * @throws InvalidAssociationException if there is an invalid association
     * @throws DuplicateFileResourceIdentifierException If any of the URIs contained within the <code>TableOfContents</code> is a duplicate
     * @throws SAXException If there was a problem parsing the xml
     * @see ACS#createNextTableOfContents
     */
    public static TableOfContents newInstance() throws InvalidIndexException, IOException, URISyntaxException,
            InvalidAssociationException, DuplicateFileResourceIdentifierException, SAXException {
        InputStream tableOfContentsXmlStream = TableOfContents.class
                .getResourceAsStream(Constants.NEW_TOC_TEMPLATE);
        TableOfContents tableOfContents = new TableOfContents(tableOfContentsXmlStream, 1);
        tableOfContentsXmlStream.close();
        return tableOfContents;
    }

    /**
     * Returns a lower case version of a <code>URI</code> for hash keys or the like from a <code>FileResourceIdentifier</code>.
     *
     * @param fileResourceIdentifier the <code>FileResourceIdentifier</code> to return the uri key from
     * @return the lower case <code>String</code> version of the <code>FileResourceIdentifier</code> URI
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     */
    protected static String uriKey(FileResourceIdentifier fileResourceIdentifier)
            throws InvalidIndexException, URISyntaxException {
        return uriKey(fileResourceIdentifier.getUri());
    }

    /**
     * Returns a lower case version of a <code>URI</code> for hash keys or the like from a <code>URI</code>.
     *
     * @param uri the <code>URI</code> to return the uri key from
     * @return the lower case <code>String</code> version of a URI
     * @throws URISyntaxException If there is a problem with any of the URIs contained within the <code>TableOfContents</code>
     * @throws InvalidIndexException If there is a problem with the <code>TableOfContents</code>
     */
    protected static String uriKey(URI uri) throws InvalidIndexException, URISyntaxException {
        return uri.toString().toLowerCase();
    }

}