FileDocCategorySizeDatePackage
ID3v24Frame.javaAPI DocJaudiotagger 2.0.442917Wed Jun 08 12:05:40 BST 2011org.jaudiotagger.tag.id3

ID3v24Frame.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 get 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.FileConstants;
import org.jaudiotagger.audio.generic.Utils;
import org.jaudiotagger.audio.mp3.MP3File;
import org.jaudiotagger.logging.ErrorMessage;
import org.jaudiotagger.logging.Hex;
import org.jaudiotagger.tag.*;
import org.jaudiotagger.tag.datatype.Lyrics3Line;
import org.jaudiotagger.tag.id3.framebody.*;
import org.jaudiotagger.tag.id3.valuepair.TextEncoding;
import org.jaudiotagger.tag.lyrics3.*;
import org.jaudiotagger.utils.EqualsUtil;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Represents an ID3v2.4 frame.
 *
 * @author : Paul Taylor
 * @author : Eric Farng
 * @version $Id: ID3v24Frame.java 976 2011-06-08 10:05:34Z paultaylor $
 */
public class ID3v24Frame extends AbstractID3v2Frame
{
    private static Pattern validFrameIdentifier = Pattern.compile("[A-Z][0-9A-Z]{3}");

    protected static final int FRAME_DATA_LENGTH_SIZE = 4;

    protected static final int FRAME_ID_SIZE = 4;
    protected static final int FRAME_FLAGS_SIZE = 2;
    protected static final int FRAME_SIZE_SIZE = 4;
    protected static final int FRAME_ENCRYPTION_INDICATOR_SIZE = 1;
    protected static final int FRAME_GROUPING_INDICATOR_SIZE = 1;

    protected static final int FRAME_HEADER_SIZE = FRAME_ID_SIZE + FRAME_SIZE_SIZE + FRAME_FLAGS_SIZE;

    /**
     * If the frame is encrypted then the encryption method is stored in this byte
     */
    private int encryptionMethod;

    /**
     * If the frame belongs in a group with other frames then the group identifier byte is stored
     */
    private int groupIdentifier;

    protected int getFrameIdSize()
    {
        return FRAME_ID_SIZE;
    }

    protected int getFrameSizeSize()
    {
        return FRAME_SIZE_SIZE;
    }

    protected int getFrameFlagsSize()
    {
        return FRAME_FLAGS_SIZE;
    }

    protected int getFrameHeaderSize()
    {
        return FRAME_HEADER_SIZE;
    }


    public ID3v24Frame()
    {
    }

    /**
     * Creates a new ID3v2_4Frame of type identifier. An empty
     * body of the correct type will be automatically created.
     * This constructor should be used when wish to create a new
     * frame from scratch using user input
     *
     * @param identifier defines the type of body to be created
     */
    public ID3v24Frame(String identifier)
    {
        //Super Constructor creates a frame with empty body of type specified
        super(identifier);
        statusFlags = new StatusFlags();
        encodingFlags = new EncodingFlags();

    }

    /**
     * Copy Constructor:Creates a new ID3v2_4Frame datatype based on another frame.
     *
     * @param frame
     */
    public ID3v24Frame(ID3v24Frame frame)
    {
        super(frame);
        statusFlags = new StatusFlags(frame.getStatusFlags().getOriginalFlags());
        encodingFlags = new EncodingFlags(frame.getEncodingFlags().getFlags());
    }

    private void createV24FrameFromV23Frame(ID3v23Frame frame) throws InvalidFrameException
    {
        // Is it a straight conversion e.g TALB - TALB
        identifier = ID3Tags.convertFrameID23To24(frame.getIdentifier());
        logger.finer("Creating V24frame from v23:" + frame.getIdentifier() + ":" + identifier);


        //We cant convert unsupported bodies properly
        if (frame.getBody() instanceof FrameBodyUnsupported)
        {
            this.frameBody = new FrameBodyUnsupported((FrameBodyUnsupported) frame.getBody());
            this.frameBody.setHeader(this);
            identifier = frame.getIdentifier();
            logger.finer("V3:UnsupportedBody:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
        }//Simple Copy
        else if (identifier != null)
        {
            //Special Case
            if ((frame.getIdentifier().equals(ID3v23Frames.FRAME_ID_V3_USER_DEFINED_INFO)) && (((FrameBodyTXXX) frame.getBody()).getDescription().equals(FrameBodyTXXX.MOOD)))
            {
                this.frameBody = new FrameBodyTMOO((FrameBodyTXXX) frame.getBody());
                this.frameBody.setHeader(this);
                identifier = frameBody.getIdentifier();
            }
            else
            {
                logger.finer("V3:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
                this.frameBody = (AbstractTagFrameBody) ID3Tags.copyObject(frame.getBody());
                this.frameBody.setHeader(this);
            }
        }
        // Is it a known v3 frame which needs forcing to v4 frame e.g. TYER - TDRC
        else if (ID3Tags.isID3v23FrameIdentifier(frame.getIdentifier()))
        {
            identifier = ID3Tags.forceFrameID23To24(frame.getIdentifier());
            if (identifier != null)
            {
                logger.config("V3:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
                this.frameBody = this.readBody(identifier, (AbstractID3v2FrameBody) frame.getBody());
                this.frameBody.setHeader(this);
            }
            // No mechanism exists to convert it to a v24 frame, e.g deprecated frame e.g TSIZ, so hold
            // as a deprecated frame consisting of an array of bytes*/
            else
            {
                this.frameBody = new FrameBodyDeprecated((AbstractID3v2FrameBody) frame.getBody());
                this.frameBody.setHeader(this);
                identifier = frame.getIdentifier();
                logger.finer("V3:Deprecated:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
            }
        }
        // Unknown Frame e.g NCON or TDRL (because TDRL unknown to V23)
        else
        {
            this.frameBody = new FrameBodyUnsupported((FrameBodyUnsupported) frame.getBody());
            this.frameBody.setHeader(this);
            identifier = frame.getIdentifier();
            logger.finer("V3:Unknown:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
        }
    }

    /**
     * Creates a new ID3v2_4Frame datatype based on another frame of different version
     * Converts the framebody to the equivalent v24 framebody or to UnsupportedFrameBody if identifier
     * is unknown.
     *
     * @param frame to construct a new frame from
     * @throws org.jaudiotagger.tag.InvalidFrameException
     *
     */
    public ID3v24Frame(AbstractID3v2Frame frame) throws InvalidFrameException
    {
        //Should not be called
        if ((frame instanceof ID3v24Frame))
        {
            throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument");
        }
        //Flags
        if (frame instanceof ID3v23Frame)
        {
            statusFlags = new StatusFlags((ID3v23Frame.StatusFlags) frame.getStatusFlags());
            encodingFlags = new EncodingFlags(frame.getEncodingFlags().getFlags());
        }
        else
        {
            statusFlags = new StatusFlags();
            encodingFlags = new EncodingFlags();
        }

        // Convert Identifier. If the id was a known id for the original
        // version we should be able to convert it to an v24 frame, although it may mean minor
        // modification to the data. If it was not recognised originally it should remain
        // unknown.
        if (frame instanceof ID3v23Frame)
        {
            createV24FrameFromV23Frame((ID3v23Frame) frame);
        }
        else if (frame instanceof ID3v22Frame)
        {
            ID3v23Frame v23Frame = new ID3v23Frame(frame);
            createV24FrameFromV23Frame(v23Frame);
        }
        this.frameBody.setHeader(this);
    }

    /**
     * Creates a new ID3v2_4Frame datatype based on Lyrics3.
     *
     * @param field
     * @throws InvalidTagException
     */
    public ID3v24Frame(Lyrics3v2Field field) throws InvalidTagException
    {
        String id = field.getIdentifier();
        String value;
        if (id.equals("IND"))
        {
            throw new InvalidTagException("Cannot create ID3v2.40 frame from Lyrics3 indications field.");
        }
        else if (id.equals("LYR"))
        {
            FieldFrameBodyLYR lyric = (FieldFrameBodyLYR) field.getBody();
            Lyrics3Line line;
            Iterator<Lyrics3Line> iterator = lyric.iterator();
            FrameBodySYLT sync;
            FrameBodyUSLT unsync;
            boolean hasTimeStamp = lyric.hasTimeStamp();
            // we'll create only one frame here.
            // if there is any timestamp at all, we will create a sync'ed frame.
            sync = new FrameBodySYLT((byte) 0, "ENG", (byte) 2, (byte) 1, "", new byte[0]);
            unsync = new FrameBodyUSLT((byte) 0, "ENG", "", "");
            while (iterator.hasNext())
            {
                line = iterator.next();
                if (hasTimeStamp)
                {
                    // sync.addLyric(line);
                }
                else
                {
                    unsync.addLyric(line);
                }
            }
            if (hasTimeStamp)
            {
                this.frameBody = sync;
                this.frameBody.setHeader(this);
            }
            else
            {
                this.frameBody = unsync;
                this.frameBody.setHeader(this);
            }
        }
        else if (id.equals("INF"))
        {
            value = ((FieldFrameBodyINF) field.getBody()).getAdditionalInformation();
            this.frameBody = new FrameBodyCOMM((byte) 0, "ENG", "", value);
            this.frameBody.setHeader(this);
        }
        else if (id.equals("AUT"))
        {
            value = ((FieldFrameBodyAUT) field.getBody()).getAuthor();
            this.frameBody = new FrameBodyTCOM((byte) 0, value);
            this.frameBody.setHeader(this);
        }
        else if (id.equals("EAL"))
        {
            value = ((FieldFrameBodyEAL) field.getBody()).getAlbum();
            this.frameBody = new FrameBodyTALB((byte) 0, value);
            this.frameBody.setHeader(this);
        }
        else if (id.equals("EAR"))
        {
            value = ((FieldFrameBodyEAR) field.getBody()).getArtist();
            this.frameBody = new FrameBodyTPE1((byte) 0, value);
            this.frameBody.setHeader(this);
        }
        else if (id.equals("ETT"))
        {
            value = ((FieldFrameBodyETT) field.getBody()).getTitle();
            this.frameBody = new FrameBodyTIT2((byte) 0, value);
            this.frameBody.setHeader(this);
        }
        else if (id.equals("IMG"))
        {
            throw new InvalidTagException("Cannot create ID3v2.40 frame from Lyrics3 image field.");
        }
        else
        {
            throw new InvalidTagException("Cannot caret ID3v2.40 frame from " + id + " Lyrics3 field");
        }
    }

    /**
     * Creates a new ID3v24Frame datatype by reading from byteBuffer.
     *
     * @param byteBuffer      to read from
     * @param loggingFilename
     * @throws org.jaudiotagger.tag.InvalidFrameException
     *
     */
    public ID3v24Frame(ByteBuffer byteBuffer, String loggingFilename) throws InvalidFrameException, InvalidDataTypeException
    {
        setLoggingFilename(loggingFilename);
        read(byteBuffer);
    }

    /**
     * Creates a new ID3v24Frame datatype by reading from byteBuffer.
     *
     * @param byteBuffer to read from
     * @throws org.jaudiotagger.tag.InvalidFrameException
     *
     * @deprecated use {@link #ID3v24Frame(ByteBuffer,String)} instead
     */
    public ID3v24Frame(ByteBuffer byteBuffer) throws InvalidFrameException, InvalidDataTypeException
    {
        this(byteBuffer, "");
    }

    /**
     * @param obj
     * @return if obj is equivalent to this frame
     */
    public boolean equals(Object obj)
    {

        if (this == obj) return true;

        if (!(obj instanceof ID3v24Frame))
        {
            return false;
        }
        ID3v24Frame that = (ID3v24Frame) obj;


        return EqualsUtil.areEqual(this.statusFlags, that.statusFlags) && EqualsUtil.areEqual(this.encodingFlags, that.encodingFlags) && super.equals(that);
    }


    /**
     * Return size of frame
     *
     * @return int frame size
     */
    public int getSize()
    {
        return frameBody.getSize() + ID3v24Frame.FRAME_HEADER_SIZE;
    }

    /**
     * If frame is greater than certain size it will be decoded differently if unsynchronized to if synchronized
     * Frames with certain byte sequences should be unsynchronized but sometimes editors do not
     * unsynchronize them so this method checks both cases and goes with the option that fits best with the data
     *
     * @param byteBuffer
     * @throws InvalidFrameException
     */
    private void checkIfFrameSizeThatIsNotSyncSafe(ByteBuffer byteBuffer)
            throws InvalidFrameException
    {
        if (frameSize > ID3SyncSafeInteger.MAX_SAFE_SIZE)
        {
            //Set Just after size field this is where we want to be when we leave this if statement
            int currentPosition = byteBuffer.position();

            //Read as nonsync safe integer
            byteBuffer.position(currentPosition - getFrameIdSize());
            int nonSyncSafeFrameSize = byteBuffer.getInt();

            //Is the frame size syncsafe, should always be BUT some encoders such as Itunes do not do it properly
            //so do an easy check now.
            byteBuffer.position(currentPosition - getFrameIdSize());
            boolean isNotSyncSafe = ID3SyncSafeInteger.isBufferNotSyncSafe(byteBuffer);

            //not relative so need to move position
            byteBuffer.position(currentPosition);

            if (isNotSyncSafe)
            {
                logger.warning(getLoggingFilename() + ":" + "Frame size is NOT stored as a sync safe integer:" + identifier);

                //This will return a larger frame size so need to check against buffer size if too large then we are
                //buggered , give up
                if (nonSyncSafeFrameSize > (byteBuffer.remaining() - -getFrameFlagsSize()))
                {
                    logger.warning(getLoggingFilename() + ":" + "Invalid Frame size larger than size before mp3 audio:" + identifier);
                    throw new InvalidFrameException(identifier + " is invalid frame");
                }
                else
                {
                    frameSize = nonSyncSafeFrameSize;
                }
            }
            else
            {
                //appears to be sync safe but lets look at the bytes just after the reported end of this
                //frame to see if find a valid frame header

                //Read the Frame Identifier
                byte[] readAheadbuffer = new byte[getFrameIdSize()];
                byteBuffer.position(currentPosition + frameSize + getFrameFlagsSize());

                if (byteBuffer.remaining() < getFrameIdSize())
                {
                    //There is no padding or framedata we are at end so assume syncsafe
                    //reset position to just after framesize
                    byteBuffer.position(currentPosition);
                }
                else
                {
                    byteBuffer.get(readAheadbuffer, 0, getFrameIdSize());

                    //reset position to just after framesize
                    byteBuffer.position(currentPosition);

                    String readAheadIdentifier = new String(readAheadbuffer);
                    if (isValidID3v2FrameIdentifier(readAheadIdentifier))
                    {
                        //Everything ok, so continue
                    }
                    else if (ID3SyncSafeInteger.isBufferEmpty(readAheadbuffer))
                    {
                        //no data found so assume entered padding in which case assume it is last
                        //frame and we are ok
                    }
                    //haven't found identifier so maybe not syncsafe or maybe there are no more frames, just padding
                    else
                    {
                        //Ok lets try using a non-syncsafe integer

                        //size returned will be larger so is it valid
                        if (nonSyncSafeFrameSize > byteBuffer.remaining() - getFrameFlagsSize())
                        {
                            //invalid so assume syncsafe
                            byteBuffer.position(currentPosition);
                        }
                        else
                        {
                            readAheadbuffer = new byte[getFrameIdSize()];
                            byteBuffer.position(currentPosition + nonSyncSafeFrameSize + getFrameFlagsSize());

                            if (byteBuffer.remaining() >= getFrameIdSize())
                            {
                                byteBuffer.get(readAheadbuffer, 0, getFrameIdSize());
                                readAheadIdentifier = new String(readAheadbuffer);

                                //reset position to just after framesize
                                byteBuffer.position(currentPosition);

                                //ok found a valid identifier using non-syncsafe so assume non-syncsafe size
                                //and continue
                                if (isValidID3v2FrameIdentifier(readAheadIdentifier))
                                {
                                    frameSize = nonSyncSafeFrameSize;
                                    logger.warning(getLoggingFilename() + ":" + "Assuming frame size is NOT stored as a sync safe integer:" + identifier);
                                }
                                //no data found so assume entered padding in which case assume it is last
                                //frame and we are ok whereas we didn't hit padding when using syncsafe integer
                                //or we wouldn't have got to this point. So assume syncsafe integer ended within
                                //the frame data whereas this has reached end of frames.
                                else if (ID3SyncSafeInteger.isBufferEmpty(readAheadbuffer))
                                {
                                    frameSize = nonSyncSafeFrameSize;
                                    logger.warning(getLoggingFilename() + ":" + "Assuming frame size is NOT stored as a sync safe integer:" + identifier);
                                }
                                //invalid so assume syncsafe as that is is the standard
                                else
                                {

                                }
                            }
                            else
                            {
                                //reset position to just after framesize
                                byteBuffer.position(currentPosition);

                                //If the unsync framesize matches exactly the remaining bytes then assume it has the
                                //correct size for the last frame
                                if (byteBuffer.remaining() == 0)
                                {
                                    frameSize = nonSyncSafeFrameSize;
                                }
                                //Inconclusive stick with syncsafe
                                else
                                {
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Read the frame size form the header, check okay , if not try to fix
     * or just throw exception
     *
     * @param byteBuffer
     * @throws InvalidFrameException
     */
    private void getFrameSize(ByteBuffer byteBuffer)
            throws InvalidFrameException
    {
         //Read frame size as syncsafe integer
        frameSize = ID3SyncSafeInteger.bufferToValue(byteBuffer);

        if (frameSize < 0)
        {
            logger.warning(getLoggingFilename() + ":" + "Invalid Frame size:" + identifier);
            throw new InvalidFrameException(identifier + " is invalid frame");
        }
        else if (frameSize == 0)
        {
            logger.warning(getLoggingFilename() + ":" + "Empty Frame:" + identifier);
            //We dont process this frame or add to framemap becuase contains no useful information
            //Skip the two flag bytes so in correct position for subsequent frames
            byteBuffer.get();
            byteBuffer.get();
            throw new EmptyFrameException(identifier + " is empty frame");
        }
        else if (frameSize > (byteBuffer.remaining() - FRAME_FLAGS_SIZE))
        {
            logger.warning(getLoggingFilename() + ":" + "Invalid Frame size larger than size before mp3 audio:" + identifier);
            throw new InvalidFrameException(identifier + " is invalid frame");
        }

        checkIfFrameSizeThatIsNotSyncSafe(byteBuffer);
    }

    /**
     * Read the frame from the specified file.
     * Read the frame header then delegate reading of data to frame body.
     *
     * @param byteBuffer to read the frame from
     */
    public void read(ByteBuffer byteBuffer) throws InvalidFrameException, InvalidDataTypeException
    {
        String identifier = readIdentifier(byteBuffer);

        //Is this a valid identifier?
        if (!isValidID3v2FrameIdentifier(identifier))
        {
            //If not valid move file pointer back to one byte after
            //the original check so can try again.
            logger.config(getLoggingFilename() + ":" + "Invalid identifier:" + identifier);
            byteBuffer.position(byteBuffer.position() - (getFrameIdSize() - 1));
            throw new InvalidFrameIdentifierException(getLoggingFilename() + ":" + identifier + ":is not a valid ID3v2.30 frame");
        }

        //Get the frame size, adjusted as necessary
        getFrameSize(byteBuffer);

        //Read the flag bytes
        statusFlags = new StatusFlags(byteBuffer.get());
        encodingFlags = new EncodingFlags(byteBuffer.get());

        //Read extra bits appended to frame header for various encodings
        //These are not included in header size but are included in frame size but wont be read when we actually
        //try to read the frame body data
        int extraHeaderBytesCount = 0;
        int dataLengthSize = -1;
        if (((EncodingFlags) encodingFlags).isGrouping())
        {
            extraHeaderBytesCount = ID3v24Frame.FRAME_GROUPING_INDICATOR_SIZE;
            groupIdentifier=byteBuffer.get();
        }

        if (((EncodingFlags) encodingFlags).isEncryption())
        {
            //Read the Encryption byte, but do nothing with it
            extraHeaderBytesCount += ID3v24Frame.FRAME_ENCRYPTION_INDICATOR_SIZE;
            encryptionMethod=byteBuffer.get();
        }

        if (((EncodingFlags) encodingFlags).isDataLengthIndicator())
        {
            //Read the sync safe size field
            dataLengthSize = ID3SyncSafeInteger.bufferToValue(byteBuffer);
            extraHeaderBytesCount += FRAME_DATA_LENGTH_SIZE;
            logger.config(getLoggingFilename() + ":" + "Frame Size Is:" + frameSize + " Data Length Size:" + dataLengthSize);
        }

        //Work out the real size of the frameBody data
        int realFrameSize = frameSize - extraHeaderBytesCount;

        //Create Buffer that only contains the body of this frame rather than the remainder of tag
        ByteBuffer frameBodyBuffer = byteBuffer.slice();
        frameBodyBuffer.limit(realFrameSize);

        //Do we need to synchronize the frame body
        int syncSize = realFrameSize;
        if (((EncodingFlags) encodingFlags).isUnsynchronised())
        {
            //We only want to synchronize the buffer up to the end of this frame (remember this
            //buffer contains the remainder of this tag not just this frame), and we cannot just
            //create a new buffer because when this method returns the position of the buffer is used
            //to look for the next frame, so we need to modify the buffer. The action of synchronizing causes
            //bytes to be dropped so the existing buffer is large enough to hold the modifications
            frameBodyBuffer = ID3Unsynchronization.synchronize(frameBodyBuffer);
            syncSize = frameBodyBuffer.limit();
            logger.config(getLoggingFilename() + ":" + "Frame Size After Syncing is:" + syncSize);
        }

        //Read the body data
        try
        {
            if (((EncodingFlags) encodingFlags).isCompression())
            {
                frameBodyBuffer = ID3Compression.uncompress(identifier, getLoggingFilename(), byteBuffer, dataLengthSize, realFrameSize);
                frameBody = readBody(identifier, frameBodyBuffer, dataLengthSize);
            }
            else if (((EncodingFlags) encodingFlags).isEncryption())
            {
                frameBodyBuffer = byteBuffer.slice();
                frameBodyBuffer.limit(realFrameSize);
                frameBody = readEncryptedBody(identifier, byteBuffer,frameSize);
            }
            else
            {
                frameBody = readBody(identifier, frameBodyBuffer, syncSize);
            }
            if (!(frameBody instanceof ID3v24FrameBody))
            {
                logger.config(getLoggingFilename() + ":" + "Converted frame body with:" + identifier + " to deprecated framebody");
                frameBody = new FrameBodyDeprecated((AbstractID3v2FrameBody) frameBody);
            }
        }
        finally
        {
            //Update position of main buffer, so no attempt is made to reread these bytes
            byteBuffer.position(byteBuffer.position() + realFrameSize);
        }
    }

    /**
     * Write the frame. Writes the frame header but writing the data is delegated to the
     * frame body.
     *
     * @throws IOException
     */
    public void write(ByteArrayOutputStream tagBuffer)
    {
        boolean unsynchronization;

        logger.config("Writing frame to file:" + getIdentifier());

        //This is where we will write header, move position to where we can
        //write bodybuffer
        ByteBuffer headerBuffer = ByteBuffer.allocate(FRAME_HEADER_SIZE);

        //Write Frame Body Data to a new stream
        ByteArrayOutputStream bodyOutputStream = new ByteArrayOutputStream();
        ((AbstractID3v2FrameBody) frameBody).write(bodyOutputStream);

        //Does it need unsynchronizing, and are we allowing unsychronizing
        byte[] bodyBuffer = bodyOutputStream.toByteArray();
        unsynchronization = TagOptionSingleton.getInstance().isUnsyncTags() && ID3Unsynchronization.requiresUnsynchronization(bodyBuffer);
        if (unsynchronization)
        {
            bodyBuffer = ID3Unsynchronization.unsynchronize(bodyBuffer);
            logger.config("bodybytebuffer:sizeafterunsynchronisation:" + bodyBuffer.length);
        }

        //Write Frame Header
        //Write Frame ID, the identifier must be 4 bytes bytes long it may not be
        //because converted an unknown v2.2 id (only 3 bytes long)
        if (getIdentifier().length() == 3)
        {
            identifier = identifier + ' ';
        }
        headerBuffer.put(Utils.getDefaultBytes(getIdentifier(), "ISO-8859-1"), 0, FRAME_ID_SIZE);

        //Write Frame Size based on size of body buffer (if it has been unsynced then it size
        //will have increased accordingly
        int size = bodyBuffer.length;
        logger.fine("Frame Size Is:" + size);
        headerBuffer.put(ID3SyncSafeInteger.valueToBuffer(size));

        //Write the Flags
        //Status Flags:leave as they were when we read
        headerBuffer.put(statusFlags.getWriteFlags());

        //Remove any non standard flags
        ((ID3v24Frame.EncodingFlags) encodingFlags).unsetNonStandardFlags();
                
        //Encoding we only support unsynchronization
        if (unsynchronization)
        {
            ((ID3v24Frame.EncodingFlags) encodingFlags).setUnsynchronised();
        }
        else
        {
            ((ID3v24Frame.EncodingFlags) encodingFlags).unsetUnsynchronised();
        }
        //These are not currently supported on write
        ((ID3v24Frame.EncodingFlags) encodingFlags).unsetCompression();
        ((ID3v24Frame.EncodingFlags) encodingFlags).unsetDataLengthIndicator();
        headerBuffer.put(encodingFlags.getFlags());

        try
        {
            //Add header to the Byte Array Output Stream
            tagBuffer.write(headerBuffer.array());

            if (((EncodingFlags) encodingFlags).isEncryption())
            {
               tagBuffer.write(encryptionMethod);
            }

            if (((EncodingFlags) encodingFlags).isGrouping())
            {
                tagBuffer.write(groupIdentifier);
            }

            //Add bodybuffer to the Byte Array Output Stream
            tagBuffer.write(bodyBuffer);
        }
        catch (IOException ioe)
        {
            //This could never happen coz not writing to file, so convert to RuntimeException
            throw new RuntimeException(ioe);
        }
    }

    /**
     * Get Status Flags Object
     */
    public AbstractID3v2Frame.StatusFlags getStatusFlags()
    {
        return statusFlags;
    }

    /**
     * Get Encoding Flags Object
     */
    public AbstractID3v2Frame.EncodingFlags getEncodingFlags()
    {
        return encodingFlags;
    }

    public int getEncryptionMethod()
    {
        return encryptionMethod;
    }

    public int getGroupIdentifier()
    {
        return groupIdentifier;
    }

    /**
     * Member Class This represents a frame headers Status Flags
     * Make adjustments if necessary based on frame type and specification.
     */
    class StatusFlags extends AbstractID3v2Frame.StatusFlags
    {
        public static final String TYPE_TAGALTERPRESERVATION = "typeTagAlterPreservation";
        public static final String TYPE_FILEALTERPRESERVATION = "typeFileAlterPreservation";
        public static final String TYPE_READONLY = "typeReadOnly";


        /**
         * Discard frame if tag altered
         */
        public static final int MASK_TAG_ALTER_PRESERVATION = FileConstants.BIT6;

        /**
         * Discard frame if audio part of file altered
         */
        public static final int MASK_FILE_ALTER_PRESERVATION = FileConstants.BIT5;

        /**
         * Frame tagged as read only
         */
        public static final int MASK_READ_ONLY = FileConstants.BIT4;

        /**
         * Use this when creating a frame from scratch
         */
        StatusFlags()
        {
            super();
        }

        /**
         * Use this constructor when reading from file or from another v4 frame
         *
         * @param flags
         */
        StatusFlags(byte flags)
        {
            originalFlags = flags;
            writeFlags = flags;
            modifyFlags();
        }

        /**
         * Use this constructor when convert a v23 frame
         *
         * @param statusFlags
         */
        StatusFlags(ID3v23Frame.StatusFlags statusFlags)
        {
            originalFlags = convertV3ToV4Flags(statusFlags.getOriginalFlags());
            writeFlags = originalFlags;
            modifyFlags();
        }

        /**
         * Convert V3 Flags to equivalent V4 Flags
         *
         * @param v3Flag
         * @return
         */
        private byte convertV3ToV4Flags(byte v3Flag)
        {
            byte v4Flag = (byte) 0;
            if ((v3Flag & ID3v23Frame.StatusFlags.MASK_FILE_ALTER_PRESERVATION) != 0)
            {
                v4Flag |= (byte) MASK_FILE_ALTER_PRESERVATION;
            }
            if ((v3Flag & ID3v23Frame.StatusFlags.MASK_TAG_ALTER_PRESERVATION) != 0)
            {
                v4Flag |= (byte) MASK_TAG_ALTER_PRESERVATION;
            }
            return v4Flag;
        }

        /**
         * Makes modifications to flags based on specification and frameid
         */
        protected void modifyFlags()
        {
            String str = getIdentifier();
            if (ID3v24Frames.getInstanceOf().isDiscardIfFileAltered(str))
            {
                writeFlags |= (byte) MASK_FILE_ALTER_PRESERVATION;
                writeFlags &= (byte) ~MASK_TAG_ALTER_PRESERVATION;
            }
            else
            {
                writeFlags &= (byte) ~MASK_FILE_ALTER_PRESERVATION;
                writeFlags &= (byte) ~MASK_TAG_ALTER_PRESERVATION;
            }
        }

        public void createStructure()
        {
            MP3File.getStructureFormatter().openHeadingElement(TYPE_FLAGS, "");
            MP3File.getStructureFormatter().addElement(TYPE_TAGALTERPRESERVATION, originalFlags & MASK_TAG_ALTER_PRESERVATION);
            MP3File.getStructureFormatter().addElement(TYPE_FILEALTERPRESERVATION, originalFlags & MASK_FILE_ALTER_PRESERVATION);
            MP3File.getStructureFormatter().addElement(TYPE_READONLY, originalFlags & MASK_READ_ONLY);
            MP3File.getStructureFormatter().closeHeadingElement(TYPE_FLAGS);
        }


    }

    /**
     * This represents a frame headers Encoding Flags
     */
    class EncodingFlags extends AbstractID3v2Frame.EncodingFlags
    {
        public static final String TYPE_COMPRESSION = "compression";
        public static final String TYPE_ENCRYPTION = "encryption";
        public static final String TYPE_GROUPIDENTITY = "groupidentity";
        public static final String TYPE_FRAMEUNSYNCHRONIZATION = "frameUnsynchronisation";
        public static final String TYPE_DATALENGTHINDICATOR = "dataLengthIndicator";

        /**
         * Frame is part of a group
         */
        public static final int MASK_GROUPING_IDENTITY = FileConstants.BIT6;

        /**
         * Frame is compressed
         */
        public static final int MASK_COMPRESSION = FileConstants.BIT3;

        /**
         * Frame is encrypted
         */
        public static final int MASK_ENCRYPTION = FileConstants.BIT2;

        /**
         * Unsynchronisation
         */
        public static final int MASK_FRAME_UNSYNCHRONIZATION = FileConstants.BIT1;

        /**
         * Length
         */
        public static final int MASK_DATA_LENGTH_INDICATOR = FileConstants.BIT0;

        /**
         * Use this when creating a frame from scratch
         */
        EncodingFlags()
        {
            super();
        }

        /**
         * Use this when creating a frame from existing flags in another v4 frame
         *
         * @param flags
         */
        EncodingFlags(byte flags)
        {
            super(flags);
            logEnabledFlags();
        }

        public void logEnabledFlags()
        {
            if(isNonStandardFlags())
            {
                logger.warning(getLoggingFilename() + ":" + identifier + ":Unknown Encoding Flags:"+ Hex.asHex(flags));
            }
            if (isCompression())
            {
                logger.warning(ErrorMessage.MP3_FRAME_IS_COMPRESSED.getMsg(getLoggingFilename(), identifier));
            }

            if (isEncryption())
            {
                logger.warning(ErrorMessage.MP3_FRAME_IS_ENCRYPTED.getMsg(getLoggingFilename(), identifier));
            }

            if (isGrouping())
            {
                logger.config(ErrorMessage.MP3_FRAME_IS_GROUPED.getMsg(getLoggingFilename(), identifier));
            }

            if (isUnsynchronised())
            {
                logger.config(ErrorMessage.MP3_FRAME_IS_UNSYNCHRONISED.getMsg(getLoggingFilename(), identifier));
            }

            if (isDataLengthIndicator())
            {
                logger.config(ErrorMessage.MP3_FRAME_IS_DATA_LENGTH_INDICATOR.getMsg(getLoggingFilename(), identifier));
            }
        }

        public byte getFlags()
        {
            return flags;
        }

        public boolean isCompression()
        {
            return (flags & MASK_COMPRESSION) > 0;
        }

        public boolean isEncryption()
        {
            return (flags & MASK_ENCRYPTION) > 0;
        }

        public boolean isGrouping()
        {
            return (flags & MASK_GROUPING_IDENTITY) > 0;
        }

        public boolean isUnsynchronised()
        {
            return (flags & MASK_FRAME_UNSYNCHRONIZATION) > 0;
        }

        public boolean isDataLengthIndicator()
        {
            return (flags & MASK_DATA_LENGTH_INDICATOR) > 0;
        }

        public void setCompression()
        {
            flags |= MASK_COMPRESSION;
        }

        public void setEncryption()
        {
            flags |= MASK_ENCRYPTION;
        }

         public void setGrouping()
        {
            flags |= MASK_GROUPING_IDENTITY;
        }

        public void setUnsynchronised()
        {
            flags |= MASK_FRAME_UNSYNCHRONIZATION;
        }

        public void setDataLengthIndicator()
        {
            flags |= MASK_DATA_LENGTH_INDICATOR;
        }

        public void unsetCompression()
        {
            flags &= (byte) ~MASK_COMPRESSION;
        }

        public void unsetEncryption()
        {
            flags &= (byte) ~MASK_ENCRYPTION;
        }

        public void unsetGrouping()
        {
            flags &= (byte) ~MASK_GROUPING_IDENTITY;
        }

        public void unsetUnsynchronised()
        {
            flags &= (byte) ~MASK_FRAME_UNSYNCHRONIZATION;
        }

        public void unsetDataLengthIndicator()
        {
            flags &= (byte) ~MASK_DATA_LENGTH_INDICATOR;
        }

        public boolean isNonStandardFlags()
        {
            return ((flags & FileConstants.BIT7) > 0) ||
                   ((flags & FileConstants.BIT5) > 0) ||
                   ((flags & FileConstants.BIT4) > 0);
        }

        public void unsetNonStandardFlags()
        {
            if(isNonStandardFlags())
            {
                logger.warning(getLoggingFilename() + ":" + getIdentifier() + ":Unsetting Unknown Encoding Flags:"+  Hex.asHex(flags));
                flags &= (byte) ~FileConstants.BIT7;
                flags &= (byte) ~FileConstants.BIT5;
                flags &= (byte) ~FileConstants.BIT4;
            }
        }

        public void createStructure()
        {
            MP3File.getStructureFormatter().openHeadingElement(TYPE_FLAGS, "");
            MP3File.getStructureFormatter().addElement(TYPE_COMPRESSION, flags & MASK_COMPRESSION);
            MP3File.getStructureFormatter().addElement(TYPE_ENCRYPTION, flags & MASK_ENCRYPTION);
            MP3File.getStructureFormatter().addElement(TYPE_GROUPIDENTITY, flags & MASK_GROUPING_IDENTITY);
            MP3File.getStructureFormatter().addElement(TYPE_FRAMEUNSYNCHRONIZATION, flags & MASK_FRAME_UNSYNCHRONIZATION);
            MP3File.getStructureFormatter().addElement(TYPE_DATALENGTHINDICATOR, flags & MASK_DATA_LENGTH_INDICATOR);
            MP3File.getStructureFormatter().closeHeadingElement(TYPE_FLAGS);
        }
    }

    /**
     * Does the frame identifier meet the syntax for a idv3v2 frame identifier.
     * must start with a capital letter and only contain capital letters and numbers
     *
     * @param identifier to be checked
     * @return whether the identifier is valid
     */
    public boolean isValidID3v2FrameIdentifier(String identifier)
    {
        Matcher m = ID3v24Frame.validFrameIdentifier.matcher(identifier);
        return m.matches();
    }

    /**
     * Return String Representation of body
     */
    public void createStructure()
    {
        MP3File.getStructureFormatter().openHeadingElement(TYPE_FRAME, getIdentifier());
        MP3File.getStructureFormatter().addElement(TYPE_FRAME_SIZE, frameSize);
        statusFlags.createStructure();
        encodingFlags.createStructure();
        frameBody.createStructure();
        MP3File.getStructureFormatter().closeHeadingElement(TYPE_FRAME);
    }

    /**
     * @return true if considered a common frame
     */
    public boolean isCommon()
    {
        return ID3v24Frames.getInstanceOf().isCommon(getId());
    }

    /**
     * @return true if considered a common frame
     */
    public boolean isBinary()
    {
        return ID3v24Frames.getInstanceOf().isBinary(getId());
    }

     /**
     * Sets the charset encoding used by the field.
     *
     * @param encoding charset.
     */
    public void setEncoding(String encoding)
    {
        Integer encodingId = TextEncoding.getInstanceOf().getIdForValue(encoding);
        if(encoding!=null)
        {
            if(encodingId <4)
            {
                this.getBody().setTextEncoding(encodingId.byteValue());
            }
        }
    }
}