FileDocCategorySizeDatePackage
AbstractID3v2Tag.javaAPI DocJaudiotagger 2.0.491386Wed Dec 07 11:08:26 GMT 2011org.jaudiotagger.tag.id3

AbstractID3v2Tag.java

/*
 *  MusicTag Copyright (C)2003,2004
 *
 *  This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser
 *  General Public  License as published by the Free Software Foundation; either version 2.1 of the License,
 *  or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 *  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *  See the GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License along with this library; if not,
 *  you can getFields a copy from http://www.opensource.org/licenses/lgpl-license.php or write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package org.jaudiotagger.tag.id3;

import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.exceptions.UnableToCreateFileException;
import org.jaudiotagger.audio.exceptions.UnableToModifyFileException;
import org.jaudiotagger.audio.exceptions.UnableToRenameFileException;
import org.jaudiotagger.audio.generic.Utils;
import org.jaudiotagger.audio.mp3.MP3File;
import org.jaudiotagger.logging.ErrorMessage;
import org.jaudiotagger.logging.FileSystemMessage;
import org.jaudiotagger.tag.*;
import org.jaudiotagger.tag.datatype.*;
import org.jaudiotagger.tag.id3.framebody.*;
import org.jaudiotagger.tag.id3.valuepair.TextEncoding;
import org.jaudiotagger.tag.images.Artwork;
import org.jaudiotagger.tag.reference.Languages;
import org.jaudiotagger.tag.reference.PictureTypes;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.WritableByteChannel;
import java.util.*;
import java.util.logging.Level;

/**
 * This is the abstract base class for all ID3v2 tags.
 *
 * @author : Paul Taylor
 * @author : Eric Farng
 * @version $Id: AbstractID3v2Tag.java 1011 2011-12-07 11:08:21Z paultaylor $
 */
public abstract class AbstractID3v2Tag extends AbstractID3Tag implements Tag
{
    protected static final String TYPE_HEADER = "header";
    protected static final String TYPE_BODY = "body";

    //Tag ID as held in file
    protected static final byte[] TAG_ID = {'I', 'D', '3'};

    //The tag header is the same for ID3v2 versions
    public static final int TAG_HEADER_LENGTH = 10;
    protected static final int FIELD_TAGID_LENGTH = 3;
    protected static final int FIELD_TAG_MAJOR_VERSION_LENGTH = 1;
    protected static final int FIELD_TAG_MINOR_VERSION_LENGTH = 1;
    protected static final int FIELD_TAG_FLAG_LENGTH = 1;
    protected static final int FIELD_TAG_SIZE_LENGTH = 4;

    protected static final int FIELD_TAGID_POS = 0;
    protected static final int FIELD_TAG_MAJOR_VERSION_POS = 3;
    protected static final int FIELD_TAG_MINOR_VERSION_POS = 4;
    protected static final int FIELD_TAG_FLAG_POS = 5;
    protected static final int FIELD_TAG_SIZE_POS = 6;

    protected static final int TAG_SIZE_INCREMENT = 100;

    //The max size we try to write in one go to avoid out of memory errors (10mb)
    private static final long MAXIMUM_WRITABLE_CHUNK_SIZE = 10000000;

    /**
     * Map of all frames for this tag
     */
    public HashMap frameMap = null;

    /**
     * Map of all encrypted frames, these cannot be unencrypted by jaudiotagger
     */
    public HashMap encryptedFrameMap = null;

    /**
     * Holds the ids of invalid duplicate frames
     */
    protected static final String TYPE_DUPLICATEFRAMEID = "duplicateFrameId";
    protected String duplicateFrameId = "";

    /**
     * Holds count the number of bytes used up by invalid duplicate frames
     */
    protected static final String TYPE_DUPLICATEBYTES = "duplicateBytes";
    protected int duplicateBytes = 0;

    /**
     * Holds count the number bytes used up by empty frames
     */
    protected static final String TYPE_EMPTYFRAMEBYTES = "emptyFrameBytes";
    protected int emptyFrameBytes = 0;

    /**
     * Holds the size of the tag as reported by the tag header
     */
    protected static final String TYPE_FILEREADSIZE = "fileReadSize";
    protected int fileReadSize = 0;

    /**
     * Holds count of invalid frames, (frames that could not be read)
     */
    protected static final String TYPE_INVALIDFRAMES = "invalidFrames";
    protected int invalidFrames = 0;

    /**
     * True if files has a ID3v2 header
     *
     * @param raf
     * @return
     * @throws IOException
     */
    private static boolean isID3V2Header(RandomAccessFile raf) throws IOException
    {
        long start = raf.getFilePointer();
        byte[] tagIdentifier = new byte[FIELD_TAGID_LENGTH];
        raf.read(tagIdentifier);
        raf.seek(start);
        if (!(Arrays.equals(tagIdentifier, TAG_ID)))
        {
            return false;
        }
        return true;
    }

    /**
     * Determines if file contain an id3 tag and if so positions the file pointer just after the end
     * of the tag.
     *
     * This method is used by non mp3s (such as .ogg and .flac) to determine if they contain an id3 tag
     *
     * @param raf
     * @return
     * @throws IOException
     */
    public static boolean isId3Tag(RandomAccessFile raf) throws IOException
    {
        if(!isID3V2Header(raf))
        {
            return false;
        }
        //So we have a tag
        byte[] tagHeader = new byte[FIELD_TAG_SIZE_LENGTH];
        raf.seek(raf.getFilePointer() + 6);
        raf.read(tagHeader);
        ByteBuffer bb = ByteBuffer.wrap(tagHeader);

        int size = ID3SyncSafeInteger.bufferToValue(bb);
        raf.seek(size + TAG_HEADER_LENGTH);
        return true;
    }

    /**
     * Empty Constructor
     */
    public AbstractID3v2Tag()
    {
    }

    /**
     * This constructor is used when a tag is created as a duplicate of another
     * tag of the same type and version.
     *
     * @param copyObject
     */
    protected AbstractID3v2Tag(AbstractID3v2Tag copyObject)
    {
    }

    /**
     * Copy primitives apply to all tags
     *
     * @param copyObject
     */
    protected void copyPrimitives(AbstractID3v2Tag copyObject)
    {
        logger.config("Copying Primitives");
        //Primitives type variables common to all IDv2 Tags
        this.duplicateFrameId = copyObject.duplicateFrameId;
        this.duplicateBytes = copyObject.duplicateBytes;
        this.emptyFrameBytes = copyObject.emptyFrameBytes;
        this.fileReadSize = copyObject.fileReadSize;
        this.invalidFrames = copyObject.invalidFrames;
    }

    /**
     * Copy frames from another tag,
     *
     * @param copyObject
     */
    //TODO Copy Encrypted frames needs implementing
    protected void copyFrames(AbstractID3v2Tag copyObject)
    {
        frameMap = new LinkedHashMap();
        encryptedFrameMap = new LinkedHashMap();
        //Copy Frames that are a valid 2.4 type

        for (Object o1 : copyObject.frameMap.keySet())
        {
            String id = (String) o1;
            Object o = copyObject.frameMap.get(id);
            //SingleFrames
            if (o instanceof AbstractID3v2Frame)
            {
                addFrame((AbstractID3v2Frame) o);
            }
            //MultiFrames
            else if (o instanceof ArrayList)
            {
                for (AbstractID3v2Frame frame : (ArrayList<AbstractID3v2Frame>) o)
                {
                    addFrame(frame);
                }
            }
        }
    }

    protected abstract void addFrame(AbstractID3v2Frame frame);

    /**
     * Returns the number of bytes which come from duplicate frames
     *
     * @return the number of bytes which come from duplicate frames
     */
    public int getDuplicateBytes()
    {
        return duplicateBytes;
    }

    /**
     * Return the string which holds the ids of all
     * duplicate frames.
     *
     * @return the string which holds the ids of all duplicate frames.
     */
    public String getDuplicateFrameId()
    {
        return duplicateFrameId;
    }

    /**
     * Returns the number of bytes which come from empty frames
     *
     * @return the number of bytes which come from empty frames
     */
    public int getEmptyFrameBytes()
    {
        return emptyFrameBytes;
    }

    /**
     * Return  byte count of invalid frames
     *
     * @return byte count of invalid frames
     */
    public int getInvalidFrames()
    {
        return invalidFrames;
    }

    /**
     * Returns the tag size as reported by the tag header
     *
     * @return the tag size as reported by the tag header
     */
    public int getFileReadBytes()
    {
        return fileReadSize;
    }

    /**
     * Return whether tag has frame with this identifier
     * <p/>
     * Warning the match is only done against the identifier so if a tag contains a frame with an unsupported body
     * but happens to have an identifier that is valid for another version of the tag it will return true
     *
     * @param identifier frameId to lookup
     * @return true if tag has frame with this identifier
     */
    public boolean hasFrame(String identifier)
    {
        return frameMap.containsKey(identifier);
    }


    /**
     * Return whether tag has frame with this identifier and a related body. This is required to protect
     * against circumstances whereby a tag contains a frame with an unsupported body
     * but happens to have an identifier that is valid for another version of the tag which it has been converted to
     * <p/>
     * e.g TDRC is an invalid frame in a v23 tag but if somehow a v23tag has been created by another application
     * with a TDRC frame we construct an UnsupportedFrameBody to hold it, then this library constructs a
     * v24 tag, it will contain a frame with id TDRC but it will not have the expected frame body it is not really a
     * TDRC frame.
     *
     * @param identifier frameId to lookup
     * @return true if tag has frame with this identifier
     */
    public boolean hasFrameAndBody(String identifier)
    {
        if (hasFrame(identifier))
        {
            Object o = getFrame(identifier);
            if (o instanceof AbstractID3v2Frame)
            {
                return !(((AbstractID3v2Frame) o).getBody() instanceof FrameBodyUnsupported);
            }
            return true;
        }
        return false;
    }

    /**
     * Return whether tag has frame starting with this identifier
     * <p/>
     * Warning the match is only done against the identifier so if a tag contains a frame with an unsupported body
     * but happens to have an identifier that is valid for another version of the tag it will return true
     *
     * @param identifier start of frameId to lookup
     * @return tag has frame starting with this identifier
     */
    public boolean hasFrameOfType(String identifier)
    {
        Iterator<String> iterator = frameMap.keySet().iterator();
        String key;
        boolean found = false;
        while (iterator.hasNext() && !found)
        {
            key = iterator.next();
            if (key.startsWith(identifier))
            {
                found = true;
            }
        }
        return found;
    }


    /**
     * For single frames return the frame in this tag with given identifier if it exists, if multiple frames
     * exist with the same identifier it will return a list containing all the frames with this identifier
     * <p/>
     * Warning the match is only done against the identifier so if a tag contains a frame with an unsupported body
     * but happens to have an identifier that is valid for another version of the tag it will be returned.
     * <p/>
     *
     * @param identifier is an ID3Frame identifier
     * @return matching frame, or list of matching frames
     */
    //TODO:This method is problematic because sometimes it returns a list and sometimes a frame, we need to
    //replace with two separate methods as in the tag interface.
    public Object getFrame(String identifier)
    {
        return frameMap.get(identifier);
    }

    /**
     * Return any encrypted frames with this identifier
     * <p/>
     * <p>For single frames return the frame in this tag with given identifier if it exists, if multiple frames
     * exist with the same identifier it will return a list containing all the frames with this identifier
     *
     * @param identifier
     * @return
     */
    public Object getEncryptedFrame(String identifier)
    {
        return encryptedFrameMap.get(identifier);
    }

    /**
     * Retrieve the first value that exists for this identifier
     * <p/>
     * If the value is a String it returns that, otherwise returns a summary of the fields information
     * <p/>
     *
     * @param identifier
     * @return
     */
    public String getFirst(String identifier)
    {
        AbstractID3v2Frame frame = getFirstField(identifier);
        if (frame == null)
        {
            return "";
        }
        return getTextValueForFrame(frame);
    }

    /**
     * @param frame
     * @return
     */
    private String getTextValueForFrame(AbstractID3v2Frame frame)
    {
        return frame.getBody().getUserFriendlyValue();
    }

    public TagField getFirstField(FieldKey genericKey) throws KeyNotFoundException
    {
        List<TagField> fields = getFields(genericKey);
        if (fields.size() > 0)
        {
            return fields.get(0);
        }
        return null;
    }


    /**
     * Retrieve the first tag field that exists for this identifier
     *
     * @param identifier
     * @return tag field or null if doesn't exist
     */
    public AbstractID3v2Frame getFirstField(String identifier)
    {
        Object object = getFrame(identifier);
        if (object == null)
        {
            return null;
        }
        if (object instanceof List)
        {
            return ((List<AbstractID3v2Frame>) object).get(0);
        }
        else
        {
            return (AbstractID3v2Frame) object;
        }
    }

    /**
     * Add a frame to this tag
     *
     * @param frame the frame to add
     *              <p/>
     *              <p/>
     *              Warning if frame(s) already exists for this identifier that they are overwritten
     *              <p/>
     */
    //TODO needs to ensure do not addField an invalid frame for this tag
    //TODO what happens if already contains a list with this ID
    public void setFrame(AbstractID3v2Frame frame)
    {
        frameMap.put(frame.getIdentifier(), frame);
    }

    protected abstract ID3Frames getID3Frames();

    public void setField(FieldKey genericKey, String value) throws KeyNotFoundException, FieldDataInvalidException
    {
        TagField tagfield = createField(genericKey, value);
        setField(tagfield);
    }

    public void addField(FieldKey genericKey, String value) throws KeyNotFoundException, FieldDataInvalidException
    {
        TagField tagfield = createField(genericKey, value);
        addField(tagfield);
    }


    /**
     * Add frame taking into account existing frames of the same type
     *
     * @param newFrame
     * @param frames
     */
    public void mergeDuplicateFrames(AbstractID3v2Frame newFrame, List<AbstractID3v2Frame> frames)
    {
        for (ListIterator<AbstractID3v2Frame> li = frames.listIterator(); li.hasNext();)
        {
            AbstractID3v2Frame nextFrame = li.next();

            if (newFrame.getBody() instanceof FrameBodyTXXX)
            {
                //Value with matching key exists so replace
                if (((FrameBodyTXXX) newFrame.getBody()).getDescription().equals(((FrameBodyTXXX) nextFrame.getBody()).getDescription()))
                {
                    li.set(newFrame);
                    frameMap.put(newFrame.getId(), frames);
                    return;
                }
            }
            else if (newFrame.getBody() instanceof FrameBodyWXXX)
            {
                //Value with matching key exists so replace
                if (((FrameBodyWXXX) newFrame.getBody()).getDescription().equals(((FrameBodyWXXX) nextFrame.getBody()).getDescription()))
                {
                    li.set(newFrame);
                    frameMap.put(newFrame.getId(), frames);
                    return;
                }
            }
            else if (newFrame.getBody() instanceof FrameBodyCOMM)
            {
                if (((FrameBodyCOMM) newFrame.getBody()).getDescription().equals(((FrameBodyCOMM) nextFrame.getBody()).getDescription()))
                {
                    li.set(newFrame);
                    frameMap.put(newFrame.getId(), frames);
                    return;
                }
            }
            else if (newFrame.getBody() instanceof FrameBodyUFID)
            {
                if (((FrameBodyUFID) newFrame.getBody()).getOwner().equals(((FrameBodyUFID) nextFrame.getBody()).getOwner()))
                {
                    li.set(newFrame);
                    frameMap.put(newFrame.getId(), frames);
                    return;
                }
            }
            else if (newFrame.getBody() instanceof FrameBodyUSLT)
            {
                if (((FrameBodyUSLT) newFrame.getBody()).getDescription().equals(((FrameBodyUSLT) nextFrame.getBody()).getDescription()))
                {
                    li.set(newFrame);
                    frameMap.put(newFrame.getId(), frames);
                    return;
                }
            }
            else if (newFrame.getBody() instanceof FrameBodyPOPM)
            {
                if (((FrameBodyPOPM) newFrame.getBody()).getEmailToUser().equals(((FrameBodyPOPM) nextFrame.getBody()).getEmailToUser()))
                {
                    li.set(newFrame);
                    frameMap.put(newFrame.getId(), frames);
                    return;
                }
            }
            //Just grab any additional info from new TRCK Frame and add to the existing one
            else if (newFrame.getBody() instanceof FrameBodyTRCK)
            {
                FrameBodyTRCK newBody = (FrameBodyTRCK) newFrame.getBody();
                FrameBodyTRCK oldBody = (FrameBodyTRCK) nextFrame.getBody();

                if (newBody.getTrackNo() != null && newBody.getTrackNo() > 0)
                {
                    oldBody.setTrackNo(newBody.getTrackNo());
                }

                if (newBody.getTrackTotal() != null && newBody.getTrackTotal() > 0)
                {
                    oldBody.setTrackTotal(newBody.getTrackTotal());
                }
                return;
            }
            //Just grab any additional info from new TPOS Frame and add to the existing one
            else if (newFrame.getBody() instanceof FrameBodyTPOS)
            {
                FrameBodyTPOS newBody = (FrameBodyTPOS) newFrame.getBody();
                FrameBodyTPOS oldBody = (FrameBodyTPOS) nextFrame.getBody();

                Integer newDiscNo = newBody.getDiscNo();
                if ((newDiscNo != null) && (newDiscNo > 0))
                {
                    oldBody.setDiscNo(newDiscNo);
                }

                Integer newDiscTotal = newBody.getDiscTotal();
                if ((newDiscTotal != null) && (newDiscTotal > 0))
                {
                    oldBody.setDiscTotal(newDiscTotal);
                }
                return;
            }
            else if (newFrame.getBody() instanceof FrameBodyIPLS)
            {
                FrameBodyIPLS frameBody         = (FrameBodyIPLS) newFrame.getBody();
                FrameBodyIPLS existingFrameBody = (FrameBodyIPLS) nextFrame.getBody();
                existingFrameBody.addPair(frameBody.getText());
                return;
            }
            else if (newFrame.getBody() instanceof FrameBodyTIPL)
            {
                FrameBodyTIPL frameBody         = (FrameBodyTIPL) newFrame.getBody();
                FrameBodyTIPL existingFrameBody = (FrameBodyTIPL) nextFrame.getBody();
                existingFrameBody.addPair(frameBody.getText());
                return;
            }
        }

        if(!getID3Frames().isMultipleAllowed(newFrame.getId()))
        {
            frameMap.put(newFrame.getId(), newFrame);
        }
        else
        {
            //No match found so addField new one
            frames.add(newFrame);
            frameMap.put(newFrame.getId(), frames);
        }
    }

    /**
     * Handles adding of a new field that's shares a frame with other fields, so modifies the existing frame rather
     * than creating an ew frame for these special cases
     *
     * @param list
     * @param frameMap
     * @param existingFrame
     * @param frame
     */
    private void addNewFrameOrAddField(List<TagField> list, HashMap frameMap, AbstractID3v2Frame existingFrame, AbstractID3v2Frame frame)
    {
        /**
         * If the frame is a TextInformation (but not the TXXX) frame then we just add an extra string to the existing frame
         * otherwise we create a new frame
         */
        if (frame.getBody() instanceof AbstractFrameBodyTextInfo && !(frame.getBody() instanceof FrameBodyTXXX))
        {
            AbstractFrameBodyTextInfo frameBody = (AbstractFrameBodyTextInfo) frame.getBody();
            AbstractFrameBodyTextInfo existingFrameBody = (AbstractFrameBodyTextInfo) existingFrame.getBody();
            existingFrameBody.addTextValue(frameBody.getText());
        }
        else if (frame.getBody() instanceof FrameBodyIPLS)
        {
            FrameBodyIPLS frameBody = (FrameBodyIPLS) frame.getBody();
            FrameBodyIPLS existingFrameBody = (FrameBodyIPLS) existingFrame.getBody();
            existingFrameBody.addPair(frameBody.getText());
        }
        else if (frame.getBody() instanceof FrameBodyTIPL)
        {
            FrameBodyTIPL frameBody = (FrameBodyTIPL) frame.getBody();
            FrameBodyTIPL existingFrameBody = (FrameBodyTIPL) existingFrame.getBody();
            existingFrameBody.addPair(frameBody.getText());
        }
        else
        {
            if (list.size() == 0)
            {
                list.add(existingFrame);
                list.add(frame);
                frameMap.put(frame.getId(), list);
            }
            else
            {
                list.add(frame);
            }
        }
    }

    /**
     * @param field
     * @throws FieldDataInvalidException
     */
    public void setField(TagField field) throws FieldDataInvalidException
    {
        if (!(field instanceof AbstractID3v2Frame))
        {
            throw new FieldDataInvalidException("Field " + field + " is not of type AbstractID3v2Frame");
        }

        AbstractID3v2Frame newFrame = (AbstractID3v2Frame) field;

        Object obj = frameMap.get(field.getId());


        //If no frame of this type exist or if multiples are not allowed
        if (obj == null)
        {
            frameMap.put(field.getId(), field);
        }
        //frame of this type already exists
        else if (obj instanceof AbstractID3v2Frame)
        {
            List<AbstractID3v2Frame> frames = new ArrayList<AbstractID3v2Frame>();
            frames.add((AbstractID3v2Frame) obj);
            mergeDuplicateFrames(newFrame, frames);
        }
        //Multiple frames of this type already exist
        else if (obj instanceof List)
        {
            mergeDuplicateFrames(newFrame, (List<AbstractID3v2Frame>) obj);
        }
    }


    /**
     * Add new field
     * <p/>
     * There is a special handling if adding another text field of the same type, in this case the value will
     * be appended to the existing field, separated by the null character.
     *
     * @param field
     * @throws FieldDataInvalidException
     */
    public void addField(TagField field) throws FieldDataInvalidException
    {
        if (field == null)
        {
            return;
        }

        if (!(field instanceof AbstractID3v2Frame))
        {
            throw new FieldDataInvalidException("Field " + field + " is not of type AbstractID3v2Frame");
        }

        AbstractID3v2Frame frame = (AbstractID3v2Frame) field;

        Object o = frameMap.get(field.getId());

        //No frame of this type
        if (o == null)
        {
            frameMap.put(field.getId(), field);
        }
        //There are already frames of this type
        else if (o instanceof List)
        {
            List<TagField> list = (List<TagField>) o;
            addNewFrameOrAddField(list, frameMap, null, frame);
        }
        //One frame exists, we are adding another so may need to convert to list
        else
        {
            AbstractID3v2Frame existingFrame = (AbstractID3v2Frame) o;
            List<TagField> list = new ArrayList<TagField>();
            addNewFrameOrAddField(list, frameMap, existingFrame, frame);
        }
    }


    /**
     * Used for setting multiple frames for a single frame Identifier
     * <p/>
     * Warning if frame(s) already exists for this identifier thay are overwritten
     * <p/>
     * TODO needs to ensure do not add an invalid frame for this tag
     *
     * @param identifier
     * @param multiFrame
     */
    public void setFrame(String identifier, List<AbstractID3v2Frame> multiFrame)
    {
        logger.finest("Adding " + multiFrame.size() + " frames for " + identifier);
        frameMap.put(identifier, multiFrame);
    }

    /**
     * Return the number of frames in this tag of a particular type, multiple frames
     * of the same time will only be counted once
     *
     * @return a count of different frames
     */
    /*
    public int getFrameCount()
    {
        if (frameMap == null)
        {
            return 0;
        }
        else
        {
            return frameMap.size();
        }
    }
    */

    /**
     * Return all frames which start with the identifier, this
     * can be more than one which is useful if trying to retrieve
     * similar frames e.g TIT1,TIT2,TIT3 ... and don't know exactly
     * which ones there are.
     * <p/>
     * Warning the match is only done against the identifier so if a tag contains a frame with an unsupported body
     * but happens to have an identifier that is valid for another version of the tag it will be returned.
     *
     * @param identifier
     * @return an iterator of all the frames starting with a particular identifier
     */
    public Iterator getFrameOfType(String identifier)
    {
        Iterator<String> iterator = frameMap.keySet().iterator();
        HashSet result = new HashSet();
        String key;
        while (iterator.hasNext())
        {
            key = iterator.next();
            if (key.startsWith(identifier))
            {
                result.add(frameMap.get(key));
            }
        }
        return result.iterator();
    }


    /**
     * Delete Tag
     *
     * @param file to delete the tag from
     * @throws IOException if problem accessing the file
     *                     <p/>
     */
    //TODO should clear all data and preferably recover lost space and go upto end of mp3s 
    public void delete(RandomAccessFile file) throws IOException
    {
        // this works by just erasing the "ID3" tag at the beginning
        // of the file
        byte[] buffer = new byte[FIELD_TAGID_LENGTH];
        //Read into Byte Buffer
        final FileChannel fc = file.getChannel();
        fc.position();
        ByteBuffer byteBuffer = ByteBuffer.allocate(TAG_HEADER_LENGTH);
        fc.read(byteBuffer, 0);
        byteBuffer.flip();
        if (seek(byteBuffer))
        {
            file.seek(0L);
            file.write(buffer);
        }
    }

    /**
     * Is this tag equivalent to another
     *
     * @param obj to test for equivalence
     * @return true if they are equivalent
     */
    public boolean equals(Object obj)
    {
        if (!(obj instanceof AbstractID3v2Tag))
        {
            return false;
        }
        AbstractID3v2Tag object = (AbstractID3v2Tag) obj;
        return this.frameMap.equals(object.frameMap) && super.equals(obj);
    }


    /**
     * Return the frames in the order they were added
     *
     * @return and iterator of the frmaes/list of multi value frames
     */
    public Iterator iterator()
    {
        return frameMap.values().iterator();
    }

    /**
     * Remove frame(s) with this identifier from tag
     *
     * @param identifier frameId to look for
     */
    public void removeFrame(String identifier)
    {
        logger.finest("Removing frame with identifier:" + identifier);
        frameMap.remove(identifier);
    }

    /**
     * Remove all frame(s) which have an unsupported body, in other words
     * remove all frames that are not part of the standard frameSet for
     * this tag
     */
    public void removeUnsupportedFrames()
    {
        for (Iterator i = iterator(); i.hasNext();)
        {
            Object o = i.next();
            if (o instanceof AbstractID3v2Frame)
            {
                if (((AbstractID3v2Frame) o).getBody() instanceof FrameBodyUnsupported)
                {
                    logger.finest("Removing frame" + ((AbstractID3v2Frame) o).getIdentifier());
                    i.remove();
                }
            }
        }
    }

    /**
     * Remove any frames starting with this identifier from tag
     *
     * @param identifier start of frameId to look for
     */
    public void removeFrameOfType(String identifier)
    {
        //First fine matching keys
        HashSet<String> result = new HashSet<String>();
        for (Object match : frameMap.keySet())
        {
            String key = (String) match;
            if (key.startsWith(identifier))
            {
                result.add(key);
            }
        }
        //Then deleteField outside of loop to prevent concurrent modificatioon eception if there are two keys
        //with the same id
        for (String match : result)
        {
            logger.finest("Removing frame with identifier:" + match + "because starts with:" + identifier);
            frameMap.remove(match);
        }
    }


    /**
     * Write tag to file.
     *
     * @param file
     * @param audioStartByte
     * @throws IOException TODO should be abstract
     */
    public void write(File file, long audioStartByte) throws IOException
    {
    }

    /**
     * Get file lock for writing too file
     * <p/>
     * TODO:this appears to have little effect on Windows Vista
     *
     * @param fileChannel
     * @param filePath
     * @return lock or null if locking is not supported
     * @throws IOException if unable to get lock because already locked by another program
     * @throws java.nio.channels.OverlappingFileLockException
     *                     if already locked by another thread in the same VM, we dont catch this
     *                     because indicates a programming error
     */
    protected FileLock getFileLockForWriting(FileChannel fileChannel, String filePath) throws IOException
    {
        logger.finest("locking fileChannel for " + filePath);
        FileLock fileLock;
        try
        {
            fileLock = fileChannel.tryLock();
        }
        //Assumes locking is not supported on this platform so just returns null
        catch (IOException exception)
        {
            return null;
        }

        //Couldnt getFields lock because file is already locked by another application
        if (fileLock == null)
        {
            throw new IOException(ErrorMessage.GENERAL_WRITE_FAILED_FILE_LOCKED.getMsg(filePath));
        }
        return fileLock;
    }

    /**
     * Write tag to file.
     *
     * @param file
     * @throws IOException TODO should be abstract
     */
    public void write(RandomAccessFile file) throws IOException
    {
    }

    /**
     * Write tag to channel.
     *
     * @param channel
     * @throws IOException TODO should be abstract
     */
    public void write(WritableByteChannel channel) throws IOException
    {
    }

    /**
     * Write tag to output stream
     *
     * @param outputStream
     * @throws IOException
     */
    public void write(OutputStream outputStream) throws IOException
    {
        write(Channels.newChannel(outputStream));
    }
    /**
     * Checks to see if the file contains an ID3tag and if so return its size as reported in
     * the tag header  and return the size of the tag (including header), if no such tag exists return
     * zero.
     *
     * @param file
     * @return the end of the tag in the file or zero if no tag exists.
     * @throws java.io.IOException
     */
    public static long getV2TagSizeIfExists(File file) throws IOException
    {
        FileInputStream fis = null;
        FileChannel fc = null;
        ByteBuffer bb = null;
        try
        {
            //Files
            fis = new FileInputStream(file);
            fc = fis.getChannel();

            //Read possible Tag header  Byte Buffer
            bb = ByteBuffer.allocate(TAG_HEADER_LENGTH);
            fc.read(bb);
            bb.flip();
            if (bb.limit() < (TAG_HEADER_LENGTH))
            {
                return 0;
            }
        }
        finally
        {
            if (fc != null)
            {
                fc.close();
            }

            if (fis != null)
            {
                fis.close();
            }
        }

        //ID3 identifier
        byte[] tagIdentifier = new byte[FIELD_TAGID_LENGTH];
        bb.get(tagIdentifier, 0, FIELD_TAGID_LENGTH);
        if (!(Arrays.equals(tagIdentifier, TAG_ID)))
        {
            return 0;
        }

        //Is it valid Major Version
        byte majorVersion = bb.get();
        if ((majorVersion != ID3v22Tag.MAJOR_VERSION) && (majorVersion != ID3v23Tag.MAJOR_VERSION) && (majorVersion != ID3v24Tag.MAJOR_VERSION))
        {
            return 0;
        }

        //Skip Minor Version
        bb.get();

        //Skip Flags
        bb.get();

        //Get size as recorded in frame header
        int frameSize = ID3SyncSafeInteger.bufferToValue(bb);

        //addField header size to frame size
        frameSize += TAG_HEADER_LENGTH;
        return frameSize;
    }

    /**
     * Does a tag of the correct version exist in this file.
     *
     * @param byteBuffer to search through
     * @return true if tag exists.
     */
    public boolean seek(ByteBuffer byteBuffer)
    {
        byteBuffer.rewind();
        logger.config("ByteBuffer pos:" + byteBuffer.position() + ":limit" + byteBuffer.limit() + ":cap" + byteBuffer.capacity());


        byte[] tagIdentifier = new byte[FIELD_TAGID_LENGTH];
        byteBuffer.get(tagIdentifier, 0, FIELD_TAGID_LENGTH);
        if (!(Arrays.equals(tagIdentifier, TAG_ID)))
        {
            return false;
        }
        //Major Version
        if (byteBuffer.get() != getMajorVersion())
        {
            return false;
        }
        //Minor Version
        return byteBuffer.get() == getRevision();
    }

    /**
     * This method determines the total tag size taking into account
     * where the audio file starts, the size of the tagging data and
     * user options for defining how tags should shrink or grow.
     *
     * @param tagSize
     * @param audioStart
     * @return
     */
    protected int calculateTagSize(int tagSize, int audioStart)
    {
        /** We can fit in the tag so no adjustments required */
        if (tagSize <= audioStart)
        {
            return audioStart;
        }
        /** There is not enough room as we need to move the audio file we might
         *  as well increase it more than neccessary for future changes
         */
        return tagSize + TAG_SIZE_INCREMENT;
    }

    /**
     * Adjust the length of the  padding at the beginning of the MP3 file, this is only called when there is currently
     * not enough space before the start of the audio to write the tag.
     * <p/>
     * A new file will be created with enough size to fit the <code>ID3v2</code> tag.
     * The old file will be deleted, and the new file renamed.
     *
     * @param paddingSize This is total size required to store tag before audio
     * @param audioStart
     * @param file        The file to adjust the padding length of
     * @throws FileNotFoundException if the file exists but is a directory
     *                               rather than a regular file or cannot be opened for any other
     *                               reason
     * @throws IOException           on any I/O error
     */
    public void adjustPadding(File file, int paddingSize, long audioStart) throws FileNotFoundException, IOException
    {
        logger.finer("Need to move audio file to accommodate tag");
        FileChannel fcIn = null;
        FileChannel fcOut;

        //Create buffer holds the necessary padding
        ByteBuffer paddingBuffer = ByteBuffer.wrap(new byte[paddingSize]);

        //Create Temporary File and write channel, make sure it is locked        
        File paddedFile;

        try
        {
            paddedFile = File.createTempFile(Utils.getBaseFilenameForTempFile(file), ".new", file.getParentFile());
            logger.finest("Created temp file:" + paddedFile.getName() + " for " + file.getName());
        }
        //Vista:Can occur if have Write permission on folder this file would be created in Denied
        catch (IOException ioe)
        {
            logger.log(Level.SEVERE, ioe.getMessage(), ioe);
            if (ioe.getMessage().equals(FileSystemMessage.ACCESS_IS_DENIED.getMsg()))
            {
                logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER.getMsg(file.getName(), file.getParentFile().getPath()));
                throw new UnableToCreateFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER.getMsg(file.getName(), file.getParentFile().getPath()));
            }
            else
            {
                logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER.getMsg(file.getName(), file.getParentFile().getPath()));
                throw new UnableToCreateFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_CREATE_TEMPORARY_FILE_IN_FOLDER.getMsg(file.getName(), file.getParentFile().getPath()));
            }
        }

        try
        {
            fcOut = new FileOutputStream(paddedFile).getChannel();
        }
        //Vista:Can occur if have special permission Create Folder/Append Data denied
        catch (FileNotFoundException ioe)
        {
            logger.log(Level.SEVERE, ioe.getMessage(), ioe);
            logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_TO_MODIFY_TEMPORARY_FILE_IN_FOLDER.getMsg(file.getName(), file.getParentFile().getPath()));
            throw new UnableToModifyFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_MODIFY_TEMPORARY_FILE_IN_FOLDER.getMsg(file.getName(), file.getParentFile().getPath()));
        }

        try
        {
            //Create read channel from original file
            //TODO lock so cant be modified by anything else whilst reading from it ?
            fcIn = new FileInputStream(file).getChannel();

            //Write padding to new file (this is where the tag will be written to later)
            long written = fcOut.write(paddingBuffer);

            //Write rest of file starting from audio
            logger.finer("Copying:" + (file.length() - audioStart) + "bytes");

            //If the amount to be copied is very large we split into 10MB lumps to try and avoid
            //out of memory errors
            long audiolength = file.length() - audioStart;
            if (audiolength <= MAXIMUM_WRITABLE_CHUNK_SIZE)
            {
                fcIn.position(audioStart);
                long written2 = fcOut.transferFrom(fcIn, paddingSize, audiolength);
                logger.finer("Written padding:" + written + " Data:" + written2);
                if (written2 != audiolength)
                {
                    throw new RuntimeException(ErrorMessage.MP3_UNABLE_TO_ADJUST_PADDING.getMsg(audiolength, written2));
                }
            }
            else
            {
                long noOfChunks = audiolength / MAXIMUM_WRITABLE_CHUNK_SIZE;
                long lastChunkSize = audiolength % MAXIMUM_WRITABLE_CHUNK_SIZE;
                long written2 = 0;
                for (int i = 0; i < noOfChunks; i++)
                {
                    written2 += fcIn.transferTo(audioStart + (i * MAXIMUM_WRITABLE_CHUNK_SIZE), MAXIMUM_WRITABLE_CHUNK_SIZE, fcOut);
                }
                written2 += fcIn.transferTo(audioStart + (noOfChunks * MAXIMUM_WRITABLE_CHUNK_SIZE), lastChunkSize, fcOut);
                logger.finer("Written padding:" + written + " Data:" + written2);
                if (written2 != audiolength)
                {
                    throw new RuntimeException(ErrorMessage.MP3_UNABLE_TO_ADJUST_PADDING.getMsg(audiolength, written2));
                }
            }

            //Store original modification time
            long lastModified = file.lastModified();

            //Close Channels and locks
            if (fcIn != null)
            {
                if (fcIn.isOpen())
                {
                    fcIn.close();
                }
            }

            if (fcOut != null)
            {
                if (fcOut.isOpen())
                {
                    fcOut.close();
                }
            }

            //Replace file with paddedFile
            replaceFile(file, paddedFile);

            //Update modification time
            //TODO is this the right file ?
            paddedFile.setLastModified(lastModified);
        }
        catch(UnableToRenameFileException ure)
        {
            paddedFile.delete();
            throw ure;
        }
        finally
        {
            try
            {
                //Whatever happens ensure all locks and channels are closed/released
                if (fcIn != null)
                {
                    if (fcIn.isOpen())
                    {
                        fcIn.close();
                    }
                }

                if (fcOut != null)
                {
                    if (fcOut.isOpen())
                    {
                        fcOut.close();
                    }
                }
            }
            catch (Exception e)
            {
                logger.log(Level.WARNING, "Problem closing channels and locks:" + e.getMessage(), e);
            }
        }
    }

    /**
     * Write the data from the buffer to the file
     *
     * @param file
     * @param headerBuffer
     * @param bodyByteBuffer
     * @param padding
     * @param sizeIncPadding
     * @param audioStartLocation
     * @throws IOException
     */
    protected void writeBufferToFile(File file, ByteBuffer headerBuffer, byte[] bodyByteBuffer, int padding, int sizeIncPadding, long audioStartLocation) throws IOException
    {
        FileChannel fc = null;
        FileLock fileLock = null;

        //We need to adjust location of audio file if true
        if (sizeIncPadding > audioStartLocation)
        {
            logger.finest("Adjusting Padding");
            adjustPadding(file, sizeIncPadding, audioStartLocation);
        }

        try
        {
            fc = new RandomAccessFile(file, "rws").getChannel();
            fileLock = getFileLockForWriting(fc, file.getPath());
            fc.write(headerBuffer);
            fc.write(ByteBuffer.wrap(bodyByteBuffer));
            fc.write(ByteBuffer.wrap(new byte[padding]));
        }
        catch (FileNotFoundException fe)
        {
            logger.log(Level.SEVERE, getLoggingFilename() + fe.getMessage(), fe);
            if (fe.getMessage().equals(FileSystemMessage.ACCESS_IS_DENIED.getMsg()))
            {
                logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getPath()));
                throw new UnableToModifyFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getPath()));
            }
            else
            {
                logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getPath()));
                throw new UnableToCreateFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getPath()));
            }
        }
        catch (IOException ioe)
        {
            logger.log(Level.SEVERE, getLoggingFilename() + ioe.getMessage(), ioe);
            if (ioe.getMessage().equals(FileSystemMessage.ACCESS_IS_DENIED.getMsg()))
            {
                logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getParentFile().getPath()));
                throw new UnableToModifyFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getParentFile().getPath()));
            }
            else
            {
                logger.severe(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getParentFile().getPath()));
                throw new UnableToCreateFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_OPEN_FILE_FOR_EDITING.getMsg(file.getParentFile().getPath()));
            }
        }
        finally
        {
            if (fc != null)
            {
                if (fileLock != null)
                {
                    fileLock.release();
                }
                fc.close();
            }
        }
    }

    /**
     * Replace originalFile with the contents of newFile
     * <p/>
     * Both files must exist in the same folder so that there are no problems with filesystem mount points
     *
     * @param newFile
     * @param originalFile
     * @throws IOException
     */
    private void replaceFile(File originalFile, File newFile) throws IOException
    {
        boolean renameOriginalResult;
        //Rename Original File to make a backup in case problem with new file
        File originalFileBackup = new File(originalFile.getAbsoluteFile().getParentFile().getPath(), AudioFile.getBaseFilename(originalFile) + ".old");
        //If already exists modify the suffix
        int count = 1;
        while (originalFileBackup.exists())
        {
            originalFileBackup = new File(originalFile.getAbsoluteFile().getParentFile().getPath(), AudioFile.getBaseFilename(originalFile) + ".old" + count);
            count++;
        }

        renameOriginalResult = originalFile.renameTo(originalFileBackup);
        if (!renameOriginalResult)
        {
            logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_FILE_TO_BACKUP.getMsg(originalFile.getAbsolutePath(), originalFileBackup.getName()));
            newFile.delete();
            throw new UnableToRenameFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_FILE_TO_BACKUP.getMsg(originalFile.getAbsolutePath(), originalFileBackup.getName()));
        }

        //Rename new Temporary file to the final file
        boolean renameResult = newFile.renameTo(originalFile);
        if (!renameResult)
        {
            //Renamed failed so lets do some checks rename the backup back to the original file
            //New File doesnt exist
            if (!newFile.exists())
            {
                logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_NEW_FILE_DOESNT_EXIST.getMsg(newFile.getAbsolutePath()));
            }

            //Rename the backup back to the original
            renameOriginalResult = originalFileBackup.renameTo(originalFile);
            if (!renameOriginalResult)
            {
                //TODO now if this happens we are left with testfile.old instead of testfile.mp3
                logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_ORIGINAL_BACKUP_TO_ORIGINAL.getMsg(originalFileBackup.getAbsolutePath(), originalFile.getName()));
            }


            logger.warning(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE.getMsg(originalFile.getAbsolutePath(), newFile.getName()));
            newFile.delete();
            throw new UnableToRenameFileException(ErrorMessage.GENERAL_WRITE_FAILED_TO_RENAME_TO_ORIGINAL_FILE.getMsg(originalFile.getAbsolutePath(), newFile.getName()));
        }
        else
        {
            //Rename was okay so we can now deleteField the backup of the original
            boolean deleteResult = originalFileBackup.delete();
            if (!deleteResult)
            {
                //Not a disaster but can't deleteField the backup so make a warning
                logger.warning(ErrorMessage.GENERAL_WRITE_WARNING_UNABLE_TO_DELETE_BACKUP_FILE.getMsg(originalFileBackup.getAbsolutePath()));
            }
        }
    }

    /*
    * Copy frame into map, whilst accounting for multiple frame of same type which can occur even if there were
    * not frames of the same type in the original tag
    */

    protected void copyFrameIntoMap(String id, AbstractID3v2Frame newFrame)
    {

        if (frameMap.containsKey(newFrame.getIdentifier()))
        {
            Object o = frameMap.get(newFrame.getIdentifier());
            if (o instanceof AbstractID3v2Frame)
            {
                List<AbstractID3v2Frame> list = new ArrayList<AbstractID3v2Frame>();
                list.add((AbstractID3v2Frame) o);
                list.add(newFrame);
                frameMap.put(newFrame.getIdentifier(), list);
            }
            else
            {
                List<AbstractID3v2Frame> list = (List) o;
                list.add(newFrame);
            }
        }
        else
        {
            frameMap.put(newFrame.getIdentifier(), newFrame);
        }
    }

    /**
     * Add frame to the frame map
     *
     * @param frameId
     * @param next
     */
    protected void loadFrameIntoMap(String frameId, AbstractID3v2Frame next)
    {
        if (next.getBody() instanceof FrameBodyEncrypted)
        {
            loadFrameIntoSpecifiedMap(encryptedFrameMap, frameId, next);
        }
        else
        {
            loadFrameIntoSpecifiedMap(frameMap, frameId, next);
        }
    }


    /**
     * Decides what to with the frame that has just been read from file.
     * If the frame is an allowable duplicate frame and is a duplicate we add all
     * frames into an ArrayList and add the ArrayList to the HashMap. if not allowed
     * to be duplicate we store the number of bytes in the duplicateBytes variable and discard
     * the frame itself.
     *
     * @param frameId
     * @param next
     */
    protected void loadFrameIntoSpecifiedMap(HashMap map, String frameId, AbstractID3v2Frame next)
    {
        if ((ID3v24Frames.getInstanceOf().isMultipleAllowed(frameId)) || (ID3v23Frames.getInstanceOf().isMultipleAllowed(frameId)) || (ID3v22Frames.getInstanceOf().isMultipleAllowed(frameId)))
        {
            //If a frame already exists of this type
            if (map.containsKey(frameId))
            {
                Object o = map.get(frameId);
                if (o instanceof ArrayList)
                {
                    ArrayList<AbstractID3v2Frame> multiValues = (ArrayList<AbstractID3v2Frame>) o;
                    multiValues.add(next);
                    logger.finer("Adding Multi Frame(1)" + frameId);
                }
                else
                {
                    ArrayList<AbstractID3v2Frame> multiValues = new ArrayList<AbstractID3v2Frame>();
                    multiValues.add((AbstractID3v2Frame) o);
                    multiValues.add(next);
                    map.put(frameId, multiValues);
                    logger.finer("Adding Multi Frame(2)" + frameId);
                }
            }
            else
            {
                logger.finer("Adding Multi FrameList(3)" + frameId);
                map.put(frameId, next);
            }
        }
        //If duplicate frame just stores the name of the frame and the number of bytes the frame contains
        else if (map.containsKey(frameId))
        {
            logger.warning("Ignoring Duplicate Frame" + frameId);
            //If we have multiple duplicate frames in a tag separate them with semicolons
            if (this.duplicateFrameId.length() > 0)
            {
                this.duplicateFrameId += ";";
            }
            this.duplicateFrameId += frameId;
            this.duplicateBytes += ((AbstractID3v2Frame) frameMap.get(frameId)).getSize();
        }
        else
        {
            logger.finer("Adding Frame" + frameId);
            map.put(frameId, next);
        }
    }

    /**
     * Return tag size based upon the sizes of the tags rather than the physical
     * no of bytes between start of ID3Tag and start of Audio Data.Should be extended
     * by subclasses to include header.
     *
     * @return size of the tag
     */
    public int getSize()
    {
        int size = 0;
        Iterator iterator = frameMap.values().iterator();
        AbstractID3v2Frame frame;
        while (iterator.hasNext())
        {
            Object o = iterator.next();
            if (o instanceof AbstractID3v2Frame)
            {
                frame = (AbstractID3v2Frame) o;
                size += frame.getSize();
            }
            else
            {
                ArrayList<AbstractID3v2Frame> multiFrames = (ArrayList<AbstractID3v2Frame>) o;
                for (ListIterator<AbstractID3v2Frame> li = multiFrames.listIterator(); li.hasNext();)
                {
                    frame = li.next();
                    size += frame.getSize();
                }
            }
        }
        return size;
    }

    /**
     * Write all the frames to the byteArrayOutputStream
     * <p/>
     * <p>Currently Write all frames, defaults to the order in which they were loaded, newly
     * created frames will be at end of tag.
     *
     * @return ByteBuffer Contains all the frames written within the tag ready for writing to file
     * @throws IOException
     */
    protected ByteArrayOutputStream writeFramesToBuffer() throws IOException
    {
        ByteArrayOutputStream bodyBuffer = new ByteArrayOutputStream();
        writeFramesToBufferStream(frameMap, bodyBuffer);
        writeFramesToBufferStream(encryptedFrameMap, bodyBuffer);
        return bodyBuffer;
    }

    /**
     * Write frames in map to bodyBuffer
     *
     * @param map
     * @param bodyBuffer
     * @throws IOException
     */
    private void writeFramesToBufferStream(Map map, ByteArrayOutputStream bodyBuffer) throws IOException
    {
        //Sort keys into Preferred Order
        TreeSet<String> sortedWriteOrder = new TreeSet<String>(getPreferredFrameOrderComparator());
        sortedWriteOrder.addAll(map.keySet());

        AbstractID3v2Frame frame;
        for (String id : sortedWriteOrder)
        {
            Object o = map.get(id);
            if (o instanceof AbstractID3v2Frame)
            {
                frame = (AbstractID3v2Frame) o;
                frame.setLoggingFilename(getLoggingFilename());
                frame.write(bodyBuffer);
            }
            else
            {
                List<AbstractID3v2Frame> multiFrames = (List<AbstractID3v2Frame>) o;
                for (AbstractID3v2Frame nextFrame : multiFrames)
                {
                    nextFrame.setLoggingFilename(getLoggingFilename());
                    nextFrame.write(bodyBuffer);
                }
            }
        }
    }

    /**
     * @return comparator used to order frames in preferred order for writing to file
     *         so that most important frames are written first.
     */
    public abstract Comparator getPreferredFrameOrderComparator();

    public void createStructure()
    {
        createStructureHeader();
        createStructureBody();
    }

    public void createStructureHeader()
    {
        MP3File.getStructureFormatter().addElement(TYPE_DUPLICATEBYTES, this.duplicateBytes);
        MP3File.getStructureFormatter().addElement(TYPE_DUPLICATEFRAMEID, this.duplicateFrameId);
        MP3File.getStructureFormatter().addElement(TYPE_EMPTYFRAMEBYTES, this.emptyFrameBytes);
        MP3File.getStructureFormatter().addElement(TYPE_FILEREADSIZE, this.fileReadSize);
        MP3File.getStructureFormatter().addElement(TYPE_INVALIDFRAMES, this.invalidFrames);
    }

    public void createStructureBody()
    {
        MP3File.getStructureFormatter().openHeadingElement(TYPE_BODY, "");

        AbstractID3v2Frame frame;
        for (Object o : frameMap.values())
        {
            if (o instanceof AbstractID3v2Frame)
            {
                frame = (AbstractID3v2Frame) o;
                frame.createStructure();
            }
            else
            {
                ArrayList<AbstractID3v2Frame> multiFrames = (ArrayList<AbstractID3v2Frame>) o;
                for (ListIterator<AbstractID3v2Frame> li = multiFrames.listIterator(); li.hasNext();)
                {
                    frame = li.next();
                    frame.createStructure();
                }
            }
        }
        MP3File.getStructureFormatter().closeHeadingElement(TYPE_BODY);
    }

    /**
     * Retrieve the values that exists for this id3 frame id
     */
    public List<TagField> getFields(String id) throws KeyNotFoundException
    {
        Object o = getFrame(id);
        if (o == null)
        {
            return new ArrayList<TagField>();
        }
        else if (o instanceof List)
        {
            //TODO should return copy
            return (List<TagField>) o;
        }
        else if (o instanceof AbstractID3v2Frame)
        {
            List<TagField> list = new ArrayList<TagField>();
            list.add((TagField) o);
            return list;
        }
        else
        {
            throw new RuntimeException("Found entry in frameMap that was not a frame or a list:" + o);
        }
    }


    /**
     * Create Frame of correct ID3 version with the specified id
     *
     * @param id
     * @return
     */
    public abstract AbstractID3v2Frame createFrame(String id);

    //TODO

    public boolean hasCommonFields()
    {
        return true;
    }

    /**
     * Does this tag contain a field with the specified key
     *
     * @param key The field id to look for.
     * @return
     */
    public boolean   hasField(FieldKey key)
    {
        return getFirstField(key)!=null;
    }

    /**
     * Does this tag contain a field with the specified id
     *
     * @see org.jaudiotagger.tag.Tag#hasField(java.lang.String)
     */
    public boolean   hasField(String id)
    {
        return hasFrame(id);
    }

    /**
     * Is this tag empty
     *
     * @see org.jaudiotagger.tag.Tag#isEmpty()
     */
    public boolean isEmpty()
    {
        return frameMap.size() == 0;
    }

    /**
     * @return iterator of all fields, multiple values for the same Id (e.g multiple TXXX frames) count as separate
     *         fields
     */
    public Iterator<TagField> getFields()
    {
        //Iterator of each different frameId in this tag
        final Iterator<Map.Entry<String, Object>> it = this.frameMap.entrySet().iterator();

        //Iterator used by hasNext() so doesn't effect next()
        final Iterator<Map.Entry<String, Object>> itHasNext = this.frameMap.entrySet().iterator();


        return new Iterator<TagField>()
        {
            Map.Entry<String, Object> latestEntry = null;

            //this iterates through frames through for a particular frameId
            private Iterator<TagField> fieldsIt;

            private void changeIt()
            {
                if (!it.hasNext())
                {
                    return;
                }

                while (it.hasNext())
                {
                    Map.Entry<String, Object> e = it.next();
                    latestEntry = itHasNext.next();
                    if (e.getValue() instanceof List)
                    {
                        List<TagField> l = (List<TagField>) e.getValue();
                        //If list is empty (which it shouldn't be) we skip over this entry
                        if (l.size() == 0)
                        {
                            continue;
                        }
                        else
                        {
                            fieldsIt = l.iterator();
                            break;
                        }
                    }
                    else
                    {
                        //TODO must be a better way
                        List<TagField> l = new ArrayList<TagField>();
                        l.add((TagField) e.getValue());
                        fieldsIt = l.iterator();
                        break;
                    }
                }
            }

            //TODO assumes if have entry its valid, but what if empty list but very different to check this
            //without causing a side effect on next() so leaving for now
            public boolean hasNext()
            {
                //Check Current frameId, does it contain more values
                if (fieldsIt != null)
                {
                    if (fieldsIt.hasNext())
                    {
                        return true;
                    }
                }

                //No remaining entries return false
                if (!itHasNext.hasNext())
                {
                    return false;
                }

                //Issue #236
                //TODO assumes if have entry its valid, but what if empty list but very different to check this
                //without causing a side effect on next() so leaving for now
                return itHasNext.hasNext();
            }

            public TagField next()
            {
                //Hasn't been initialized yet
                if (fieldsIt == null)
                {
                    changeIt();
                }

                if (fieldsIt != null)
                {
                    //Go to the end of the run
                    if (!fieldsIt.hasNext())
                    {
                        changeIt();
                    }
                }

                if (fieldsIt == null)
                {
                    throw new NoSuchElementException();
                }
                return fieldsIt.next();
            }

            public void remove()
            {
                fieldsIt.remove();
            }
        };
    }

    /**
     * Count number of frames/fields in this tag
     *
     * @return
     */
    public int getFieldCount()
    {
        Iterator<TagField> it = getFields();
        int count = 0;

        //Done this way because it.hasNext() incorrectly counts empty list
        //whereas it.next() works correctly
        try
        {
            while (true)
            {
                TagField next = it.next();
                count++;
            }
        }
        catch (NoSuchElementException nse)
        {
            //this is thrown when no more elements
        }
        return count;
    }

    /**
     * Return count of fields, this considers a text frame with two null separated values as two fields, if you want
     * a count of frames @see getFrameCount
     *
     * @return count of fields
     */
    public int getFieldCountIncludingSubValues()
    {
        Iterator<TagField> it = getFields();
        int count = 0;

        //Done this way because it.hasNext() incorrectly counts empty list
        //whereas it.next() works correctly
        try
        {
            while (true)
            {
                TagField next = it.next();
                if (next instanceof AbstractID3v2Frame)
                {
                    AbstractID3v2Frame frame = (AbstractID3v2Frame) next;
                    if ((frame.getBody() instanceof AbstractFrameBodyTextInfo) && !(frame.getBody() instanceof FrameBodyTXXX))
                    {
                        AbstractFrameBodyTextInfo frameBody = (AbstractFrameBodyTextInfo) frame.getBody();
                        count += frameBody.getNumberOfValues();
                        continue;
                    }
                }
                count++;
            }
        }
        catch (NoSuchElementException nse)
        {
            //this is thrown when no more elements
        }
        return count;
    }

    //TODO is this a special field?

    public boolean setEncoding(String enc) throws FieldDataInvalidException
    {
        throw new UnsupportedOperationException("Not Implemented Yet");
    }

    /**
     * Retrieve the first value that exists for this generic key
     *
     * @param genericKey
     * @return
     */
    public String getFirst(FieldKey genericKey) throws KeyNotFoundException
    {
        return getValue(genericKey, 0);
    }

    /**
     * Retrieve the mth value that exists in the nth frame for this generic key
     *
     * @param genericKey
     * @param n          the index of the frame
     * @param m
     * @return
     */
    public String getSubValue(FieldKey genericKey, int n, int m)
    {
        String wholeValue = getValue(genericKey, n);
        List<String> values = TextEncodedStringSizeTerminated.splitByNullSeperator(wholeValue);
        if (values.size() > m)
        {
            return values.get(m);
        }
        return "";
    }

    /**
     * Retrieve the value that exists for this generic key and this index
     * <p/>
     * Have to do some special mapping for certain generic keys because they share frame
     * with another generic key.
     *
     * @param genericKey
     * @return
     */
    public String getValue(FieldKey genericKey, int index) throws KeyNotFoundException
    {
        if (genericKey == null)
        {
            throw new KeyNotFoundException();
        }

        FrameAndSubId frameAndSubId = getFrameAndSubIdFromGenericKey(genericKey);

        List<TagField> fields = getFields(genericKey);
        if (fields != null && fields.size() > index)
        {
            AbstractID3v2Frame frame = (AbstractID3v2Frame) fields.get(index);
            if (frame != null)
            {
                if (genericKey == FieldKey.TRACK)
                {
                    return String.valueOf(((FrameBodyTRCK) frame.getBody()).getTrackNo());
                }
                else if (genericKey == FieldKey.TRACK_TOTAL)
                {
                    return String.valueOf(((FrameBodyTRCK) frame.getBody()).getTrackTotal());
                }
                else if (genericKey == FieldKey.DISC_NO)
                {
                    return String.valueOf(((FrameBodyTPOS) frame.getBody()).getDiscNo());
                }
                else if (genericKey == FieldKey.DISC_TOTAL)
                {
                    return String.valueOf(((FrameBodyTPOS) frame.getBody()).getDiscTotal());
                }
                else if (genericKey == FieldKey.RATING)
                {
                    return String.valueOf(((FrameBodyPOPM) frame.getBody()).getRating());
                }
                else
                {
                    return doGetValueAtIndex(frameAndSubId, index);
                }
            }
            else
            {
                return "";
            }
        }
        return "";
    }

    /**
     * Create a new TagField
     * <p/>
     * Only textual data supported at the moment. The genericKey will be mapped
     * to the correct implementation key and return a TagField.
     *
     * @param genericKey is the generic key
     * @param value      to store
     * @return
     */
    public TagField createField(FieldKey genericKey, String value) throws KeyNotFoundException, FieldDataInvalidException
    {
        if (genericKey == null)
        {
            throw new KeyNotFoundException();
        }

        FrameAndSubId formatKey = getFrameAndSubIdFromGenericKey(genericKey);
        if (genericKey == FieldKey.TRACK)
        {
            AbstractID3v2Frame frame = createFrame(formatKey.getFrameId());
            FrameBodyTRCK framebody = (FrameBodyTRCK) frame.getBody();
            framebody.setTrackNo(Integer.parseInt(value));
            return frame;
        }
        else if (genericKey == FieldKey.TRACK_TOTAL)
        {
            AbstractID3v2Frame frame = createFrame(formatKey.getFrameId());
            FrameBodyTRCK framebody = (FrameBodyTRCK) frame.getBody();
            framebody.setTrackTotal(Integer.parseInt(value));
            return frame;
        }
        else if (genericKey == FieldKey.DISC_NO)
        {
            AbstractID3v2Frame frame = createFrame(formatKey.getFrameId());
            FrameBodyTPOS framebody = (FrameBodyTPOS) frame.getBody();
            framebody.setDiscNo(Integer.parseInt(value));
            return frame;
        }
        else if (genericKey == FieldKey.DISC_TOTAL)
        {
            AbstractID3v2Frame frame = createFrame(formatKey.getFrameId());
            FrameBodyTPOS framebody = (FrameBodyTPOS) frame.getBody();
            framebody.setDiscTotal(Integer.parseInt(value));
            return frame;
        }
        else
        {
            return doCreateTagField(formatKey, value);
        }
    }

    /**
     * Create Frame for Id3 Key
     * <p/>
     * Only textual data supported at the moment, should only be used with frames that
     * support a simple string argument.
     *
     * @param formatKey
     * @param value
     * @return
     * @throws KeyNotFoundException
     * @throws FieldDataInvalidException
     */
    protected TagField doCreateTagField(FrameAndSubId formatKey, String value) throws KeyNotFoundException, FieldDataInvalidException
    {
        AbstractID3v2Frame frame = createFrame(formatKey.getFrameId());
        if (frame.getBody() instanceof FrameBodyUFID)
        {
            ((FrameBodyUFID) frame.getBody()).setOwner(formatKey.getSubId());
            try
            {
                ((FrameBodyUFID) frame.getBody()).setUniqueIdentifier(value.getBytes("ISO-8859-1"));
            }
            catch (UnsupportedEncodingException uee)
            {
                //This will never happen because we are using a charset supported on all platforms
                //but just in case
                throw new RuntimeException("When encoding UFID charset ISO-8859-1 was deemed unsupported");
            }
        }
        else if (frame.getBody() instanceof FrameBodyTXXX)
        {
            ((FrameBodyTXXX) frame.getBody()).setDescription(formatKey.getSubId());
            ((FrameBodyTXXX) frame.getBody()).setText(value);
        }
        else if (frame.getBody() instanceof FrameBodyWXXX)
        {
            ((FrameBodyWXXX) frame.getBody()).setDescription(formatKey.getSubId());
            ((FrameBodyWXXX) frame.getBody()).setUrlLink(value);
        }
        else if (frame.getBody() instanceof FrameBodyCOMM)
        {
            //Set description if set
            if (formatKey.getSubId() != null)
            {
                ((FrameBodyCOMM) frame.getBody()).setDescription(formatKey.getSubId());
                //Special Handling for Media Monkey Compatability
                if (((FrameBodyCOMM) frame.getBody()).isMediaMonkeyFrame())
                {
                    ((FrameBodyCOMM) frame.getBody()).setLanguage(Languages.MEDIA_MONKEY_ID);
                }
            }
            ((FrameBodyCOMM) frame.getBody()).setText(value);
        }
        else if (frame.getBody() instanceof FrameBodyUSLT)
        {
            ((FrameBodyUSLT) frame.getBody()).setDescription("");
            ((FrameBodyUSLT) frame.getBody()).setLyric(value);
        }
        else if (frame.getBody() instanceof FrameBodyWOAR)
        {
            ((FrameBodyWOAR) frame.getBody()).setUrlLink(value);
        }
        else if (frame.getBody() instanceof AbstractFrameBodyTextInfo)
        {
            ((AbstractFrameBodyTextInfo) frame.getBody()).setText(value);
        }
        else if (frame.getBody() instanceof FrameBodyPOPM)
        {
            ((FrameBodyPOPM) frame.getBody()).parseString(value);
        }
        else if (frame.getBody() instanceof FrameBodyIPLS)
        {
            PairedTextEncodedStringNullTerminated.ValuePairs pair = new PairedTextEncodedStringNullTerminated.ValuePairs();
            pair.add(formatKey.getSubId(), value);
            frame.getBody().setObjectValue(DataTypes.OBJ_TEXT, pair);
        }
        else if (frame.getBody() instanceof FrameBodyTIPL)
        {
            PairedTextEncodedStringNullTerminated.ValuePairs pair = new PairedTextEncodedStringNullTerminated.ValuePairs();
            pair.add(formatKey.getSubId(), value);
            frame.getBody().setObjectValue(DataTypes.OBJ_TEXT, pair);
        }
        else if ((frame.getBody() instanceof FrameBodyAPIC) || (frame.getBody() instanceof FrameBodyPIC))
        {
            throw new UnsupportedOperationException(ErrorMessage.ARTWORK_CANNOT_BE_CREATED_WITH_THIS_METHOD.getMsg());
        }
        else
        {
            throw new FieldDataInvalidException("Field with key of:" + formatKey.getFrameId() + ":does not accept cannot parse data:" + value);
        }
        return frame;
    }


    /**
     * @param formatKey
     * @param index
     * @return
     * @throws KeyNotFoundException
     */
    protected String doGetValueAtIndex(FrameAndSubId formatKey, int index) throws KeyNotFoundException
    {
        //Simple 1 to 1 mapping
        if (formatKey.getSubId() == null)
        {
            List<TagField> list = getFields(formatKey.getFrameId());
            if (list.size() > index)
            {
                return getTextValueForFrame((AbstractID3v2Frame) list.get(index));
            }
        }
        else
        {
            //Get list of frames that this uses
            List<TagField> list = getFields(formatKey.getFrameId());
            ListIterator<TagField> li = list.listIterator();
            List<String> listOfMatches = new ArrayList<String>();
            while (li.hasNext())
            {
                AbstractTagFrameBody next = ((AbstractID3v2Frame) li.next()).getBody();

                if (next instanceof FrameBodyTXXX)
                {
                    if (((FrameBodyTXXX) next).getDescription().equals(formatKey.getSubId()))
                    {
                        listOfMatches.add(((FrameBodyTXXX) next).getText());
                    }
                }
                else if (next instanceof FrameBodyWXXX)
                {
                    if (((FrameBodyWXXX) next).getDescription().equals(formatKey.getSubId()))
                    {
                        listOfMatches.add(((FrameBodyWXXX) next).getUrlLink());
                    }
                }
                else if (next instanceof FrameBodyCOMM)
                {
                    if (((FrameBodyCOMM) next).getDescription().equals(formatKey.getSubId()))
                    {
                        listOfMatches.add(((FrameBodyCOMM) next).getText());
                    }
                }
                else if (next instanceof FrameBodyUFID)
                {
                    if (Arrays.equals(((FrameBodyUFID) next).getUniqueIdentifier(), formatKey.getSubId().getBytes()))
                    {
                        listOfMatches.add(new String(((FrameBodyUFID) next).getUniqueIdentifier()));
                    }
                }
                else if (next instanceof FrameBodyIPLS)
                {
                    for (Pair entry : ((FrameBodyIPLS) next).getPairing().getMapping())
                    {
                        if (entry.getKey().equals(formatKey.getSubId()))
                        {
                            listOfMatches.add(entry.getValue());
                        }
                    }
                }
                else if (next instanceof FrameBodyTIPL)
                {
                    for (Pair entry : ((FrameBodyTIPL) next).getPairing().getMapping())
                    {
                        if (entry.getKey().equals(formatKey.getSubId()))
                        {
                            listOfMatches.add(entry.getValue());
                        }
                    }
                }
                else
                {
                    throw new RuntimeException("Need to implement getFields(FieldKey genericKey) for:" + next.getClass());
                }
            }
            if (listOfMatches.size() > index)
            {
                return listOfMatches.get(index);
            }
            else
            {
                return "";
            }
        }
        return "";
    }

    /**
     * Create a link to artwork, this is not recommended because the link may be broken if the mp3 or image
     * file is moved
     *
     * @param url specifies the link, it could be a local file or could be a full url
     * @return
     */
    public TagField createLinkedArtworkField(String url)
    {
        AbstractID3v2Frame frame = createFrame(getFrameAndSubIdFromGenericKey(FieldKey.COVER_ART).getFrameId());
        if (frame.getBody() instanceof FrameBodyAPIC)
        {
            FrameBodyAPIC body = (FrameBodyAPIC) frame.getBody();
            body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, Utils.getDefaultBytes(url, TextEncoding.CHARSET_ISO_8859_1));
            body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, PictureTypes.DEFAULT_ID);
            body.setObjectValue(DataTypes.OBJ_MIME_TYPE, FrameBodyAPIC.IMAGE_IS_URL);
            body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
        }
        else if (frame.getBody() instanceof FrameBodyPIC)
        {
            FrameBodyPIC body = (FrameBodyPIC) frame.getBody();
            body.setObjectValue(DataTypes.OBJ_PICTURE_DATA, Utils.getDefaultBytes(url, TextEncoding.CHARSET_ISO_8859_1));
            body.setObjectValue(DataTypes.OBJ_PICTURE_TYPE, PictureTypes.DEFAULT_ID);
            body.setObjectValue(DataTypes.OBJ_IMAGE_FORMAT, FrameBodyAPIC.IMAGE_IS_URL);
            body.setObjectValue(DataTypes.OBJ_DESCRIPTION, "");
        }
        return frame;
    }


    /**
     * Delete fields with this generic key
     *
     * @param genericKey
     */
    public void deleteField(FieldKey genericKey) throws KeyNotFoundException
    {
        if (genericKey == null)
        {
            throw new KeyNotFoundException();
        }
        FrameAndSubId formatKey = getFrameAndSubIdFromGenericKey(genericKey);
        doDeleteTagField(formatKey);
    }

    /**
     * Internal delete method
     *
     * @param formatKey
     * @throws KeyNotFoundException
     */
    protected void doDeleteTagField(FrameAndSubId formatKey) throws KeyNotFoundException
    {
        //Simple 1 to 1 mapping
        if (formatKey.getSubId() == null)
        {
            removeFrame(formatKey.getFrameId());
        }
        else
        {
            //Get list of frames that this uses
            List<TagField> list = getFields(formatKey.getFrameId());
            ListIterator<TagField> li = list.listIterator();
            while (li.hasNext())
            {
                AbstractTagFrameBody next = ((AbstractID3v2Frame) li.next()).getBody();
                if (next instanceof FrameBodyTXXX)
                {
                    if (((FrameBodyTXXX) next).getDescription().equals(formatKey.getSubId()))
                    {
                        if(list.size()==1)
                        {
                            removeFrame(formatKey.getFrameId());
                        }
                        else
                        {
                            li.remove();
                        }
                    }
                }
                else if (next instanceof FrameBodyWXXX)
                {
                    if (((FrameBodyWXXX) next).getDescription().equals(formatKey.getSubId()))
                    {
                        if(list.size()==1)
                        {
                            removeFrame(formatKey.getFrameId());
                        }
                        else
                        {
                            li.remove();
                        }
                    }
                }
                else if (next instanceof FrameBodyUFID)
                {
                    if (Arrays.equals(((FrameBodyUFID) next).getUniqueIdentifier(), formatKey.getSubId().getBytes()))
                    {
                        if(list.size()==1)
                        {
                            removeFrame(formatKey.getFrameId());
                        }
                        else
                        {
                            li.remove();
                        }
                    }
                }
                //A single TIPL frame is used for multiple fields, so we just delete the matching pair rather than
                //deleting the frame itself unless now empty
                else if (next instanceof FrameBodyTIPL)
                {
                    PairedTextEncodedStringNullTerminated.ValuePairs pairs = ((FrameBodyTIPL) next).getPairing();
                    ListIterator<Pair> pairIterator = pairs.getMapping().listIterator();
                    while(pairIterator.hasNext())
                    {
                        Pair nextPair =  pairIterator.next();
                        if(nextPair.getKey().equals(formatKey.getSubId()))
                        {
                            pairIterator.remove();
                        }
                    }
                    if(pairs.getMapping().size()==0)
                    {
                        removeFrame(formatKey.getFrameId());
                    }
                }
                //A single IPLS frame is used for multiple fields, so we just delete the matching pair rather than
                //deleting the frame itself unless now empty 
                else if (next instanceof FrameBodyIPLS)
                {
                    PairedTextEncodedStringNullTerminated.ValuePairs pairs = ((FrameBodyIPLS) next).getPairing();
                    ListIterator<Pair> pairIterator = pairs.getMapping().listIterator();
                    while(pairIterator.hasNext())
                    {
                        Pair nextPair =  pairIterator.next();
                        if(nextPair.getKey().equals(formatKey.getSubId()))
                        {
                            pairIterator.remove();
                        }
                    }

                    if(pairs.getMapping().size()==0)
                    {
                        removeFrame(formatKey.getFrameId());
                    }
                }
                else
                {
                    throw new RuntimeException("Need to implement getFields(FieldKey genericKey) for:" + next.getClass());
                }
            }
        }
    }

    protected abstract FrameAndSubId getFrameAndSubIdFromGenericKey(FieldKey genericKey);

    /**
     * Get field(s) for this key
     *
     * @param genericKey
     * @return
     * @throws KeyNotFoundException
     */
    public List<TagField> getFields(FieldKey genericKey) throws KeyNotFoundException
    {

        if (genericKey == null)
        {
            throw new KeyNotFoundException();
        }

        FrameAndSubId formatKey = getFrameAndSubIdFromGenericKey(genericKey);

        //Get list of frames that this uses, as we are going to remove entries we don't want take a copy
        List<TagField> list = getFields(formatKey.getFrameId());
        List<TagField> filteredList = new ArrayList<TagField>();
        String subFieldId = formatKey.getSubId();

        //... do we need to refine the list further i.e we only want TXXX frames that relate to the particular
        //key that was passed as a parameter
        if (subFieldId != null)
        {
            for (TagField tagfield : list)
            {
                AbstractTagFrameBody next = ((AbstractID3v2Frame) tagfield).getBody();
                if (next instanceof FrameBodyTXXX)
                {
                    if (((FrameBodyTXXX) next).getDescription().equals(formatKey.getSubId()))
                    {
                        filteredList.add(tagfield);
                    }
                }
                else if (next instanceof FrameBodyWXXX)
                {
                    if (((FrameBodyWXXX) next).getDescription().equals(formatKey.getSubId()))
                    {
                        filteredList.add(tagfield);
                    }
                }
                else if (next instanceof FrameBodyCOMM)
                {
                    if (((FrameBodyCOMM) next).getDescription().equals(formatKey.getSubId()))
                    {
                        filteredList.add(tagfield);
                    }
                }
                else if (next instanceof FrameBodyUFID)
                {
                    if (Arrays.equals(((FrameBodyUFID) next).getUniqueIdentifier(), formatKey.getSubId().getBytes()))
                    {
                        filteredList.add(tagfield);
                    }
                }
                else if (next instanceof FrameBodyIPLS)
                {
                    for (Pair entry : ((FrameBodyIPLS) next).getPairing().getMapping())
                    {
                        if (entry.getKey().equals(formatKey.getSubId()))
                        {
                            filteredList.add(tagfield);
                        }
                    }
                }
                else if (next instanceof FrameBodyTIPL)
                {
                    for (Pair entry : ((FrameBodyTIPL) next).getPairing().getMapping())
                    {

                        if (entry.getKey().equals(formatKey.getSubId()))
                        {
                            filteredList.add(tagfield);
                        }
                    }
                }
                else
                {
                    throw new RuntimeException("Need to implement getFields(FieldKey genericKey) for:" + next.getClass());
                }
            }
            return filteredList;
        }
        else
        {
            return list;
        }
    }

    /**
     * This class had to be created to minimize the duplicate code in concrete subclasses
     * of this class. It is required in some cases when using the fieldKey enums because enums
     * cannot be sub classed. We want to use enums instead of regular classes because they are
     * much easier for end users to  to use.
     */
    class FrameAndSubId
    {
        private String frameId;
        private String subId;

        public FrameAndSubId(String frameId, String subId)
        {
            this.frameId = frameId;
            this.subId = subId;
        }

        public String getFrameId()
        {
            return frameId;
        }

        public String getSubId()
        {
            return subId;
        }
    }

    public Artwork getFirstArtwork()
    {
        List<Artwork> artwork = getArtworkList();
        if (artwork.size() > 0)
        {
            return artwork.get(0);
        }
        return null;
    }

    /**
     * Create field and then set within tag itself
     *
     * @param artwork
     * @throws FieldDataInvalidException
     */
    public void setField(Artwork artwork) throws FieldDataInvalidException
    {
        this.setField(createField(artwork));
    }

    /**
     * Create field and then set within tag itself
     *
     * @param artwork
     * @throws FieldDataInvalidException
     */
    public void addField(Artwork artwork) throws FieldDataInvalidException
    {
        this.addField(createField(artwork));
    }

    /**
     * Delete all instance of artwork Field
     *
     * @throws KeyNotFoundException
     */
    public void deleteArtworkField() throws KeyNotFoundException
    {
        this.deleteField(FieldKey.COVER_ART);
    }

    @Override
    public String toString()
    {
        final StringBuilder out = new StringBuilder();
        out.append("Tag content:\n");
        final Iterator<TagField> it = getFields();
        while (it.hasNext())
        {
            final TagField field = it.next();
            out.append("\t");
            out.append(field.getId());
            out.append(":");
            out.append(field.toString());
            out.append("\n");
        }

        return out.toString();
    }
}