uk.ac.ebi.jmzml.xml.io.MzMLUnmarshaller.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ebi.jmzml.xml.io.MzMLUnmarshaller.java

Source

/*
 * Date: 22/7/2008
 * Author: rcote
 * File: uk.ac.ebi.jmzml.xml.io.MzMLUnmarshaller
 *
 * jmzml is Copyright 2008 The European Bioinformatics Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package uk.ac.ebi.jmzml.xml.io;

import org.apache.commons.collections.buffer.CircularFifoBuffer;
import org.apache.log4j.Logger;
import org.xml.sax.InputSource;
import psidev.psi.tools.xxindex.index.IndexElement;
import uk.ac.ebi.jmzml.MzMLElement;
import uk.ac.ebi.jmzml.model.mzml.*;
import uk.ac.ebi.jmzml.model.mzml.utilities.ModelConstants;
import uk.ac.ebi.jmzml.xml.jaxb.unmarshaller.UnmarshallerFactory;
import uk.ac.ebi.jmzml.xml.jaxb.unmarshaller.filters.MzMLNamespaceFilter;
import uk.ac.ebi.jmzml.xml.util.EscapingXMLUtilities;
import uk.ac.ebi.jmzml.xml.xxindex.FileUtils;
import uk.ac.ebi.jmzml.xml.xxindex.MzMLIndexer;
import uk.ac.ebi.jmzml.xml.xxindex.MzMLIndexerFactory;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.sax.SAXSource;
import java.io.*;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MzMLUnmarshaller {

    private static final Logger logger = Logger.getLogger(MzMLUnmarshaller.class);
    private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();

    private final File mzMLFile;
    private final MzMLIndexer index;
    private final boolean useSpectrumCache;
    //    private final AdapterObjectCache cache = new AdapterObjectCache();
    private final MzMLObjectCache cache;

    private IndexList indexList = null;
    private boolean fileCorrupted = false;

    private final Pattern ID_PATTERN = Pattern.compile("id *= *\"([^\"]*)?\"", Pattern.CASE_INSENSITIVE);
    private final Pattern AC_PATTERN = Pattern.compile("accession *= *\"([^\"]*)?\"", Pattern.CASE_INSENSITIVE);
    private final Pattern VERSION_PATTERN = Pattern.compile("version *= *\"([^\"]*)?\"", Pattern.CASE_INSENSITIVE);
    private static final Pattern XML_ATT_PATTERN = Pattern.compile("\\s+([A-Za-z:]+)\\s*=\\s*[\"']([^\"'>]+?)[\"']",
            Pattern.DOTALL);

    /**
     * Creates a new MzMLUnmarshaller object from a URL
     *
     * @param mzMLFileURL the URL to unmarshall
     */
    public MzMLUnmarshaller(URL mzMLFileURL) {
        this(mzMLFileURL, true);
    }

    /**
     * Creates a new MzMLUnmarshaller object from a file
     *
     * @param mzMLFile the file to unmarshall
     */
    public MzMLUnmarshaller(File mzMLFile) {
        this(mzMLFile, true, null);
    }

    /**
     * Creates a new MzMLUnmarshaller object from a URL
     *
     * @param mzMLFileURL       the URL to unmarshall
     * @param aUseSpectrumCache if true the spectra are cached
     */
    public MzMLUnmarshaller(URL mzMLFileURL, boolean aUseSpectrumCache) {
        this(FileUtils.getFileFromURL(mzMLFileURL), aUseSpectrumCache, null);
    }

    /**
     * Creates a new MzMLUnmarshaller object from a file
     *
     * @param mzMLFile          the file to unmarshall
     * @param aUseSpectrumCache if true the spectra are cached
     */
    public MzMLUnmarshaller(File mzMLFile, boolean aUseSpectrumCache, MzMLObjectCache cache) {
        this.mzMLFile = mzMLFile;
        index = MzMLIndexerFactory.getInstance().buildIndex(mzMLFile);
        useSpectrumCache = aUseSpectrumCache;
        this.cache = cache;
    }

    /**
     * USE WITH CAUTION - This will unmarshall a complete MzML object and
     * will likely cause an OutOfMemoryError for very large files.
     *
     * @return an MzML object
     */
    public MzML unmarshall() {
        return unmarshalFromXpath("", MzML.class);
    }

    /**
     * Returns the mzML version.
     *
     * @return the mzML version, null if not found
     */
    public String getMzMLVersion() {

        Matcher match = VERSION_PATTERN.matcher(index.getMzMLAttributeXMLString());

        if (match.find()) {
            return match.group(1);
        } else {
            return null;
        }
    }

    /**
     * Returns the mzML accession number.
     *
     * @return the mzML accession number, null if not found
     */
    public String getMzMLAccession() {

        Matcher match = AC_PATTERN.matcher(index.getMzMLAttributeXMLString());

        if (match.find()) {
            return match.group(1);
        } else {
            return null;
        }
    }

    /**
     * Returns the mzML ID.
     *
     * @return the mzML ID, null if not found
     */
    public String getMzMLId() {

        Matcher match = ID_PATTERN.matcher(index.getMzMLAttributeXMLString());

        if (match.find()) {
            return match.group(1);
        } else {
            return null;
        }
    }

    public Map<String, String> getSingleElementAttributes(String xpath) {
        Map<String, String> attributes = new HashMap<String, String>();
        // retrieve the start tag of the corresponding XML element
        //single Element should appear once in the file
        String tag = index.getStartTag(xpath);
        if (tag == null) {
            return null;
        }

        // parse the tag for attributes
        Matcher match = XML_ATT_PATTERN.matcher(tag);
        while (match.find()) {
            if (match.groupCount() == 2) {
                // found name - value pair
                String name = match.group(1);
                String value = match.group(2);
                // stick the found attributes in the map
                attributes.put(name, value);
            } else {
                // not a name - value pair, something is wrong!
                System.out.println(
                        "Unexpected number of groups for XML attribute: " + match.groupCount() + " in tag: " + tag);
            }

        }
        return attributes;
    }

    /**
     * Method to retrieve attribute name-value pairs for a XML element
     * defined by it's id and Class.
     *
     * @param id    the value of the 'id' attribute of the XML element.
     * @param clazz the Class representing the XML element.
     * @return A map of all the found name-value attribute pairs or
     *         null if no element with the specified id was found.
     */
    public Map<String, String> getElementAttributes(String id, Class clazz) {
        Map<String, String> attributes = new HashMap<String, String>();
        // retrieve the start tag of the corresponding XML element
        String tag = index.getStartTag(id, clazz);
        if (tag == null) {
            return null;
        }

        // parse the tag for attributes
        Matcher match = XML_ATT_PATTERN.matcher(tag);
        while (match.find()) {
            if (match.groupCount() == 2) {
                // found name - value pair
                String name = match.group(1);
                String value = match.group(2);
                // stick the found attributes in the map
                attributes.put(name, value);
            } else {
                // not a name - value pair, something is wrong!
                // ToDo: proper handling! exception
                System.out.println(
                        "Unexpected number of groups for XML attribute: " + match.groupCount() + " in tag: " + tag);
            }

        }
        return attributes;
    }

    /**
     * Returns the number of elements for a given path.
     *
     * @param xpath the path to look up
     * @return the number of elements for a given path
     */
    public int getObjectCountForXpath(String xpath) {
        return index.getCount(xpath);
    }

    /**
     * Retrieves the list of elements of the given class at the selected path.
     * <p/>
     * Exp.: CVList cvList = unmarshaller.unmarshallFromXPath("/cvList", CVList.class);
     * Retrieves the cvList from element of the mzML file, given it's XPath
     *
     * @param <T>
     * @param xpath the path to search
     * @param cls   the class type to retrieve
     * @return the list of elements of the given class at the selected path
     */
    public <T extends MzMLObject> T unmarshalFromXpath(String xpath, Class cls) {
        // ToDo: only unmarshalls first element in xxindex!! Document this!
        T retval = null;
        try {
            //we want to unmarshal the whole file
            if (xpath.equals("")) {
                xpath = MzMLElement.MzML.getXpath();
                if (isIndexedmzML()) {
                    xpath = MzMLElement.IndexedmzML.getXpath().concat(xpath);
                }

            }
            Iterator<String> xpathIter = index.getXmlStringIterator(xpath);

            if (xpathIter.hasNext()) {

                String xmlSt = xpathIter.next();

                //need to clean up XML to ensure that there are no weird control characters
                String cleanXML = EscapingXMLUtilities.escapeCharacters(xmlSt);

                if (logger.isDebugEnabled()) {
                    logger.trace("XML to unmarshal: " + cleanXML);
                }

                //required for the addition of namespaces to top-level objects
                MzMLNamespaceFilter xmlFilter = new MzMLNamespaceFilter();
                //initializeUnmarshaller will assign the proper reader to the xmlFilter
                Unmarshaller unmarshaller = UnmarshallerFactory.getInstance().initializeUnmarshaller(index,
                        xmlFilter, cache, useSpectrumCache);
                //unmarshall the desired object
                JAXBElement<T> holder = unmarshaller
                        .unmarshal(new SAXSource(xmlFilter, new InputSource(new StringReader(cleanXML))), cls);
                retval = holder.getValue();

                if (logger.isDebugEnabled()) {
                    logger.debug("unmarshalled object = " + retval);
                }
            }

        } catch (JAXBException e) {
            logger.error("MzMLUnmarshaller.unmarshalFromXpath", e);
            throw new IllegalStateException("Could not unmarshal object at xpath:" + xpath);
        }

        return retval;
    }

    /**
     * Retrieves a collection of elements of the given class at the selected path
     *
     * @param <T>
     * @param xpath the path to search
     * @param cls   the class type to retrieve
     * @return the collection of elements of the given class at the selected path
     */
    public <T extends MzMLObject> MzMLObjectIterator<T> unmarshalCollectionFromXpath(String xpath, Class cls) {
        return new MzMLObjectIterator<T>(xpath, cls, index, cache, useSpectrumCache);
    }

    ///// ///// ///// ///// ///// ///// ///// ///// ///// //////
    // additional unmarshal operations for indexedmzML

    // ToDo: add schema validation step or implicit validation with the marshaller/unmarshaller

    /**
     * Returns true of the mzML file is indexed.
     *
     * @return true of the mzML file is indexed
     */
    public boolean isIndexedmzML() {
        // ToDo: find better way to check this?
        // ToDo: maybe change log level in StandardXpathAccess class
        // this check will log an ERROR if it is not an indexedmzML file, since we
        // are trying to retrieve an entry that will not be in the XML
        Iterator iter = index.getXmlStringIterator("/indexedmzML/indexList");
        return iter.hasNext();
    }

    /**
     * Returns true if the mzML file's check sum is ok.
     *
     * @return true if the mzML file's check sum is ok
     * @throws MzMLUnmarshallerException
     */
    public boolean isOkFileChecksum() throws MzMLUnmarshallerException {
        // if we already have established that the checksum has changed, then don't check again
        if (fileCorrupted) {
            return false;
        }

        // if it is not even an indexedmzML, then we throw an exception right away
        if (!isIndexedmzML()) {
            throw new MzMLUnmarshallerException("Attempted check of file checksum on un-indexed mzML file.");
        }

        // ok, now compare the two checksums (provided and calculated)
        String indexChecksum = getFileChecksumFromIndex();
        logger.info("provided checksum (index)  : " + indexChecksum);
        String calcChecksum = calculateChecksum();
        logger.info("calculated checksum (jmzml): " + calcChecksum);
        boolean checkSumOK = indexChecksum.equals(calcChecksum);
        //        boolean checkSumOK = true;

        // if the checksums don't match, mark the file as corrupted
        if (!checkSumOK) {
            fileCorrupted = true;
        }

        return checkSumOK;
    }

    /**
     * Returns the mzML index.
     *
     * @return the mzML index
     * @throws MzMLUnmarshallerException
     */
    public IndexList getMzMLIndex() throws MzMLUnmarshallerException {
        IndexList retval;
        // check if already cached
        if (indexList == null) {
            // not yet cached, so we have to unmarshal it
            if (isOkFileChecksum()) {
                retval = unmarshalFromXpath("/indexedmzML/indexList", IndexList.class);
                indexList = retval; // save, so we don't have to generate it again
            } else {
                throw new MzMLUnmarshallerException(
                        "File checksum did not match! This file has been changed after the index was created. The index is invalid.");
            }
        } else {
            retval = indexList;
        }

        return retval;
    }

    /**
     * Returns the spectrum corresponding to the provided spectrum ID.
     *
     * @param aID the ID of the spectrum to get
     * @return the spectrum corresponding to the provided spectrum ID, null if no matching spectrum is found
     * @throws MzMLUnmarshallerException
     */
    public Spectrum getSpectrumById(String aID) throws MzMLUnmarshallerException {
        Spectrum result = null;
        String xml = index.getXmlString(aID, Spectrum.class);
        try {
            //need to clean up XML to ensure that there are no weird control characters
            String cleanXML = EscapingXMLUtilities.escapeCharacters(xml);
            //required for the addition of namespaces to top-level objects
            MzMLNamespaceFilter xmlFilter = new MzMLNamespaceFilter();
            //initializeUnmarshaller will assign the proper reader to the xmlFilter
            Unmarshaller unmarshaller = UnmarshallerFactory.getInstance().initializeUnmarshaller(index, xmlFilter,
                    cache, useSpectrumCache);
            //unmarshall the desired object
            JAXBElement<Spectrum> holder = unmarshaller.unmarshal(
                    new SAXSource(xmlFilter, new InputSource(new StringReader(cleanXML))), Spectrum.class);
            result = holder.getValue();
        } catch (JAXBException je) {
            logger.error("MzMLUnmarshaller.getSpectrumByID", je);
            throw new IllegalStateException("Could not unmarshal spectrum with ID: " + aID);
        }
        return result;
    }

    /**
     * Returns the chromatogram corresponding to the provided chromatogram ID.
     *
     * @param aID the ID of the chromatogram to get
     * @return the chromatogram corresponding to the provided chromatogram ID, null if no matching chromatogram is found
     * @throws MzMLUnmarshallerException
     */
    public Chromatogram getChromatogramById(String aID) throws MzMLUnmarshallerException {
        Chromatogram result = null;
        String xml = index.getXmlString(aID, Chromatogram.class);
        try {
            //need to clean up XML to ensure that there are no weird control characters
            String cleanXML = EscapingXMLUtilities.escapeCharacters(xml);
            //required for the addition of namespaces to top-level objects
            MzMLNamespaceFilter xmlFilter = new MzMLNamespaceFilter();
            //initializeUnmarshaller will assign the proper reader to the xmlFilter
            Unmarshaller unmarshaller = UnmarshallerFactory.getInstance().initializeUnmarshaller(index, xmlFilter,
                    cache, useSpectrumCache);
            //unmarshall the desired object
            JAXBElement<Chromatogram> holder = unmarshaller.unmarshal(
                    new SAXSource(xmlFilter, new InputSource(new StringReader(cleanXML))), Chromatogram.class);
            result = holder.getValue();
        } catch (JAXBException je) {
            logger.error("MzMLUnmarshaller.getChromatogramByID", je);
            throw new IllegalStateException("Could not unmarshal chromatogram with ID: " + aID);
        }
        return result;
    }

    /**
     * Returns the spectrum corresponding to a given refId.
     *
     * @param refId the refId of the spectrum to retrieve
     * @return the corresponding spectrum, or null if no spectrum is found
     * @throws MzMLUnmarshallerException
     */
    public Spectrum getSpectrumByRefId(String refId) throws MzMLUnmarshallerException {
        // get the index entry for 'spectrum'
        Index aIndexEntry = getIndex("spectrum");

        // find the offset for the specified refId
        for (Offset offset : aIndexEntry.getOffset()) {
            if (offset.getIdRef().equalsIgnoreCase(refId)) {
                return getElementByOffset("spectrum", offset.getValue());
            }
        }

        return null;
    }

    /**
     * Returns the spectrum with a given spotId.
     *
     * @param spotId the spotId of the spectrum to retrieve
     * @return the corresponding spectrum, or null if no spectrum is found
     * @throws MzMLUnmarshallerException
     */
    public Spectrum getSpectrumBySpotId(String spotId) throws MzMLUnmarshallerException {
        // get the index entry for 'chromatogram'
        Index aIndexEntry = getIndex("spectrum");

        // find the offset for the specified spotId
        for (Offset offset : aIndexEntry.getOffset()) {
            if (offset.getSpotID() != null && offset.getSpotID().equalsIgnoreCase(spotId)) {
                return getElementByOffset("spectrum", offset.getValue());
            }
        }

        return null;
    }

    /**
     * Returns the spectrum with a given scan time.
     *
     * @param scanTime the scan time for the wanted spectrum
     * @return the spectrum with a given scan time, null of no spectrum is found
     * @throws MzMLUnmarshallerException
     */
    public Spectrum getSpectrumByScanTime(double scanTime) throws MzMLUnmarshallerException {
        // get the index entry for 'chromatogram'
        Index aIndexEntry = getIndex("spectrum");

        // find the offset for the specified scanTime
        for (Offset offset : aIndexEntry.getOffset()) {
            if (offset.getScanTime() != null && offset.getScanTime() == scanTime) {
                return getElementByOffset("spectrum", offset.getValue());
            }
        }

        return null;
    }

    /**
     * Returns the spectrum with a given index.
     *
     * @param aIndex Integer with the index for the desired spectrum
     * @return the spectrum with the given index, or 'null' if none is found.
     * @throws MzMLUnmarshallerException
     */
    public Spectrum getSpectrumByScanTime(Integer aIndex) throws MzMLUnmarshallerException {
        // Resolve the index to an ID.
        String specID = index.getSpectrumIDFromSpectrumIndex(aIndex);

        return this.getSpectrumById(specID);
    }

    /**
     * Returns the chromatogram corresponding to a given refId.
     *
     * @param refId the refId of the chromatogram to retrieve
     * @return the chromatogram corresponding to a given refId, null if no chromatogram found
     * @throws MzMLUnmarshallerException
     */
    public Chromatogram getChromatogramByRefId(String refId) throws MzMLUnmarshallerException {
        // get the index entry for 'chromatogram'
        Index aIndexEntry = getIndex("chromatogram");

        // find the offset for the specified refId
        for (Offset offset : aIndexEntry.getOffset()) {
            // we are only interested in a particular refId
            if (offset.getIdRef().equalsIgnoreCase(refId)) {
                return getElementByOffset("chromatogram", offset.getValue());
            }
        }

        return null;
    }

    /**
     * Returns a set containing all spectrum IDs
     *
     * @return a set containing all spectrum IDs
     */
    public Set<String> getSpectrumIDs() {
        return this.index.getSpectrumIDs();
    }

    /**
     * Returns a set containing all spectrum indexes
     *
     * @return a set containing all spectrum indexes
     */
    public Set<Integer> getSpectrumIndexes() {
        return this.index.getSpectrumIndexes();
    }

    /**
     * This method returns the spectrum ID for a given spectrum index, or 'null'
     * if the specified index could not be found.
     *
     * @param aIndex Integer with the spectrum index to retrieve
     *               the spectrum ID for
     * @return String  with the spectrum ID, or 'null' if the index could not be found.
     */
    public String getSpectrumIDFromSpectrumIndex(Integer aIndex) {
        return index.getSpectrumIDFromSpectrumIndex(aIndex);
    }

    /**
     * Returns a set containing all chromatogram IDs
     *
     * @return a set containing all chromatogram IDs
     */
    public Set<String> getChromatogramIDs() {
        return this.index.getChromatogramIDs();
    }

    ///// ///// ///// ///// ///// ///// ///// ///// ///// //////
    // private helper method primarily for indexedmzML stuff

    /**
     * Returns the file's check sum from the index.
     *
     * @return the file's check sum from the index
     * @throws MzMLUnmarshallerException
     */
    private String getFileChecksumFromIndex() throws MzMLUnmarshallerException {
        // there will only be a fileChecksum tag it is a indexedmzML
        if (!isIndexedmzML()) {
            throw new MzMLUnmarshallerException("Can not retrieve fileChecksum from a non indexed mzML file!");
        }

        // now fetch the fileChecksum stored in the indexedmzML
        String checksum;
        Iterator<String> snipIter = index.getXmlStringIterator("/indexedmzML/fileChecksum");
        if (snipIter.hasNext()) {
            String snippet = snipIter.next();
            // we need to cut of the start and stop tag
            //            checksum = snippet.substring(14, snippet.length()-15).intern();
            String test = snippet.replace("<fileChecksum>", "");
            checksum = test.replace("</fileChecksum>", "").trim().intern();
        } else {
            throw new IllegalStateException(
                    "Could not find fileChecksum tag in indexedmzML: " + mzMLFile.getName());
        }

        return checksum;
    }

    /**
     * Calcultes the check sum.
     *
     * @return the check sum as hexidecimal
     */
    private String calculateChecksum() {
        // we have to create the checksum for the mzML file (from its beginning to the
        // end of the fileChecksum start tag).
        // Since this stop location is very near the end of the file, we skip everything
        // until we come within a certain limit of the end of the file
        long limit = mzMLFile.length() - 200L;
        logger.debug("Looking for fileChecksum tag between byte " + limit + " and byte " + mzMLFile.length()
                + " (the end) of the mzML file.");

        // initialize the hash algorithm
        MessageDigest hash;
        try {
            hash = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-1 not recognized as Secure Hash Algorithm.", e);
        }

        // create the input stream that will calculate the checksum
        FileInputStream fis;
        try {
            fis = new FileInputStream(mzMLFile);
        } catch (FileNotFoundException e) {
            throw new IllegalStateException("File " + mzMLFile.getAbsoluteFile() + " could not be found!", e);
        }
        BufferedInputStream bis = new BufferedInputStream(fis);
        DigestInputStream dis = new DigestInputStream(bis, hash);

        // prepare for input stream processing
        // we read through the file until we reach a specified limit before the end of the file
        // from there we populate a buffer with the read bytes (characters) and check if we have
        // already reached the position up to where we have to calculate the hash.
        CircularFifoBuffer bBuf = new CircularFifoBuffer(15);
        long cnt = 0; // counter to keep track of our position
        byte[] b = new byte[1]; // we only read one byte at a time
        try {
            while (dis.read(b) >= 0) {
                bBuf.add(b[0]);
                cnt++;
                // check if we have already reached the last bit of the file, where we have
                // to find the right position to stop (after the 'fileChecksum' start tag)
                if (cnt > limit) {
                    // we should have reached the start of the <fileChecksum> tag,
                    // now we have to find the end
                    String readBuffer = convert2String(bBuf);
                    if (readBuffer.endsWith("<fileChecksum>")) {
                        // we have found the end of the fileChecksum start tag, we have to stop the hash
                        if (b[0] != '>') { // check that we are really at the last character of the tag
                            throw new IllegalStateException("We are not at the end of <fileChecksum> tag!");
                        }
                        break;
                    }
                } // else if not yet near the end of the file, just keep on going
            }
            dis.close();
        } catch (IOException e) {
            throw new IllegalStateException(
                    "Could not read from file '" + mzMLFile.getAbsolutePath() + "' while trying ot calculate hash.",
                    e);
        }
        logger.debug("Read over " + cnt + " bytes while calculating the file hash.");

        byte[] bytesDigest = dis.getMessageDigest().digest();

        return asHex(bytesDigest);
    }

    /**
     * TODO: Javadoc missing
     *
     * @param bBuf
     * @return
     */
    private String convert2String(CircularFifoBuffer bBuf) {
        byte[] tmp = new byte[bBuf.size()];
        int tmpCnt = 0;
        for (Object aBBuf : bBuf) {
            tmp[tmpCnt++] = (Byte) aBBuf;
        }
        return new String(tmp);
    }

    /**
     * TODO: Javadoc missing
     *
     * @param buf
     * @return
     */
    public static String asHex(byte[] buf) {
        // from: http://forums.xkcd.com/viewtopic.php?f=11&t=16666&p=553936
        char[] chars = new char[2 * buf.length];
        for (int i = 0; i < buf.length; ++i) {
            chars[2 * i] = HEX_CHARS[(buf[i] & 0xF0) >>> 4];
            chars[2 * i + 1] = HEX_CHARS[buf[i] & 0x0F];
        }
        return new String(chars);
    }

    /**
     * TODO: Javadoc missing
     *
     * @param elementName
     * @return
     * @throws MzMLUnmarshallerException
     */
    private Index getIndex(String elementName) throws MzMLUnmarshallerException {
        IndexList list = getMzMLIndex();

        for (Index entry : list.getIndex()) {
            if (entry.getName().equalsIgnoreCase(elementName)) {
                return entry;
            }
        }

        return null;
    }

    /**
     * TODO: Javadoc missing
     *
     * @param <T>
     * @param elementName
     * @param offset
     * @return
     * @throws MzMLUnmarshallerException
     */
    private <T extends MzMLObject> T getElementByOffset(String elementName, long offset)
            throws MzMLUnmarshallerException {
        // now check if we can map the elementName to a xpath from the xxindex
        String aXpath = null;
        for (String xxindexPath : index.getXpath()) {
            // we are looking for a xpath that ends in the elementName (e.g. points to
            // an element with the requested name)
            if (xxindexPath.endsWith(elementName)) {
                aXpath = xxindexPath;
            }
        }
        // if we don't have the xpath, then this method has been used incorrectly!
        if (aXpath == null) {
            throw new MzMLUnmarshallerException("Could not find a valid xpath "
                    + "(in xxindex) for the requested mzML index element '" + elementName + "'!");
        }

        // now that we have the xpath to use for the requested element, check if the xxindex
        // contains an element start position that matches the offset of the desired element
        String xmlSnippet = index.getXmlString(aXpath, offset);
        if (xmlSnippet == null) {
            throw new MzMLUnmarshallerException("No element '" + elementName + "' with the specified " + "offset ("
                    + offset + ") could be found (xpath: '" + aXpath + "')! Perhaps the "
                    + "mzML index containing the offset was corrupted.");
        }

        T retval;
        try {
            // ToDo: check this!! try to replace with standard unmarshaller!
            //need to clean up XML to ensure that there are no weird control characters
            String cleanXML = EscapingXMLUtilities.escapeCharacters(xmlSnippet);
            MzMLNamespaceFilter xmlFilter = new MzMLNamespaceFilter();
            // initializeUnmarshaller will assign the proper reader to the xmlFilter
            Unmarshaller unmarshaller = UnmarshallerFactory.getInstance().initializeUnmarshaller(index, xmlFilter,
                    cache, useSpectrumCache);
            // unmarshall the desired object
            Class cls = ModelConstants.getClassForElementName(elementName);
            JAXBElement<T> holder = unmarshaller
                    .unmarshal(new SAXSource(xmlFilter, new InputSource(new StringReader(cleanXML))), cls);
            retval = holder.getValue();
        } catch (JAXBException e) {
            logger.error("MzMLUnmarshaller.getObjectFromXml", e);
            throw new IllegalStateException("Could not unmarshal object from XML string:" + xmlSnippet);
        }

        return retval;
    }

    /**
     * TODO: Javadoc missing
     *
     * @param <T>
     * @param element
     * @return
     * @throws MzMLUnmarshallerException
     */
    public <T extends MzMLObject> T unmarshalFromIndexElement(IndexElement element, Class cls)
            throws MzMLUnmarshallerException {

        // now that we have the xpath to use for the requested element, check if the xxindex
        // contains an element start position that matches the offset of the desired element
        String xmlSnippet = index.getXmlString(element);

        T retval;
        try {
            // ToDo: check this!! try to replace with standard unmarshaller!
            String cleanXML = EscapingXMLUtilities.escapeCharacters(xmlSnippet);
            MzMLNamespaceFilter xmlFilter = new MzMLNamespaceFilter();
            // initializeUnmarshaller will assign the proper reader to the xmlFilter
            Unmarshaller unmarshaller = UnmarshallerFactory.getInstance().initializeUnmarshaller(index, xmlFilter,
                    cache, useSpectrumCache);
            // unmarshall the desired object
            JAXBElement<T> holder = unmarshaller
                    .unmarshal(new SAXSource(xmlFilter, new InputSource(new StringReader(cleanXML))), cls);
            retval = holder.getValue();
        } catch (JAXBException e) {
            logger.error("MzMLUnmarshaller.getObjectFromXml", e);
            throw new IllegalStateException("Could not unmarshal object from XML string:" + xmlSnippet);
        }

        return retval;
    }

    /**
     * Returns the mzML XXIndex Wrapper for raw access. This is usually a developer-level method.
     *
     * @return the mzML XXIndex Wrapper for raw acces
     */
    public MzMLIndexer getMzMLIndexer() {
        return index;
    }
}