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

ID3v22Frame.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.audio.generic.Utils;
import org.jaudiotagger.audio.mp3.MP3File;
import org.jaudiotagger.tag.*;
import org.jaudiotagger.tag.id3.framebody.AbstractID3v2FrameBody;
import org.jaudiotagger.tag.id3.framebody.FrameBodyDeprecated;
import org.jaudiotagger.tag.id3.framebody.FrameBodyUnsupported;
import org.jaudiotagger.tag.id3.valuepair.TextEncoding;
import org.jaudiotagger.utils.EqualsUtil;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

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

    public ID3v22Frame()
    {

    }

    protected int getFrameIdSize()
    {
        return FRAME_ID_SIZE;
    }

    protected int getFrameSizeSize()
    {
        return FRAME_SIZE_SIZE;
    }

    protected int getFrameHeaderSize()
    {
        return FRAME_HEADER_SIZE;
    }

    /**
     * Creates a new ID3v22 Frame with given body
     *
     * @param body New body and frame is based on this
     */
    public ID3v22Frame(AbstractID3v2FrameBody body)
    {
        super(body);
    }

     /**
     * Compare for equality
     * To be deemed equal obj must be a IDv23Frame with the same identifier
     * and the same flags.
     * containing the same body,datatype list ectera.
     * equals() method is made up from all the various components
     *
     * @param obj
     * @return if true if this object is equivalent to obj
     */
    public boolean equals(Object obj)
    {
        if ( this == obj ) return true;

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


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

    }

    /**
     * Creates a new ID3v22 Frame of type identifier.
     * <p/>
     * 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 values
     * @param identifier
     */
    @SuppressWarnings("unchecked")
    public ID3v22Frame(String identifier)
    {

        logger.config("Creating empty frame of type" + identifier);
        String bodyIdentifier = identifier;
        this.identifier = identifier;

        //If dealing with v22 identifier (Note this constructor is used by all three tag versions)
        if (ID3Tags.isID3v22FrameIdentifier(bodyIdentifier))
        {
            //Does it have its own framebody (PIC,CRM) or are we using v23/v24 body (the normal case)
            if (ID3Tags.forceFrameID22To23(bodyIdentifier) != null)
            {
                //Do not convert
            }
            else if(bodyIdentifier.equals("CRM"))
            {
                //Do not convert.
                //TODO we don't have a way of converting this to v23 which is why its not in the ForceMap
            }
            //TODO Improve messy fix for datetime
            //TODO need to check in case v22 body does exist before using V23 body(e.g PIC)
            else if ((bodyIdentifier.equals(ID3v22Frames.FRAME_ID_V2_TYER)) || (bodyIdentifier.equals(ID3v22Frames.FRAME_ID_V2_TIME)))
            {
                bodyIdentifier = ID3v24Frames.FRAME_ID_YEAR;
            }
            // Have to check for v22 because most don't have own body they use v23 or v24
            // body to hold the data, the frame is identified by its identifier, the body identifier
            // is just to create a body suitable for writing the data to
            else if (ID3Tags.isID3v22FrameIdentifier(bodyIdentifier))
            {
                bodyIdentifier = ID3Tags.convertFrameID22To23(bodyIdentifier);
            }
        }

        // Use reflection to map id to frame body, which makes things much easier
        // to keep things up to date.
        try
        {
            Class<AbstractID3v2FrameBody> c = (Class<AbstractID3v2FrameBody>) Class.forName("org.jaudiotagger.tag.id3.framebody.FrameBody" + bodyIdentifier);
            frameBody = c.newInstance();
        }
        catch (ClassNotFoundException cnfe)
        {
            logger.log(Level.SEVERE, cnfe.getMessage(), cnfe);
            frameBody = new FrameBodyUnsupported(identifier);
        }
        //Instantiate Interface/Abstract should not happen
        catch (InstantiationException ie)
        {
            logger.log(Level.SEVERE, ie.getMessage(), ie);
            throw new RuntimeException(ie);
        }
        //Private Constructor shouild not happen
        catch (IllegalAccessException iae)
        {
            logger.log(Level.SEVERE, iae.getMessage(), iae);
            throw new RuntimeException(iae);
        }
        frameBody.setHeader(this);
        logger.config("Created empty frame of type" + this.identifier + "with frame body of" + bodyIdentifier);

    }

    /**
     * Copy Constructor
     * <p/>
     * Creates a new v22 frame based on another v22 frame
     * @param frame
     */
    public ID3v22Frame(ID3v22Frame frame)
    {
        super(frame);
        logger.config("Creating frame from a frame of same version");
    }

    private void createV22FrameFromV23Frame(ID3v23Frame frame) throws InvalidFrameException
    {
        identifier = ID3Tags.convertFrameID23To22(frame.getIdentifier());
        if (identifier != null)
        {
            logger.config("V2:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
            this.frameBody = (AbstractID3v2FrameBody) ID3Tags.copyObject(frame.getBody());
        }
        // Is it a known v3 frame which needs forcing to v2 frame e.g. APIC - PIC
        else if (ID3Tags.isID3v23FrameIdentifier(frame.getIdentifier()))
        {
            identifier = ID3Tags.forceFrameID23To22(frame.getIdentifier());
            if (identifier != null)
            {
                logger.config("V2:Force:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
                this.frameBody = this.readBody(identifier, (AbstractID3v2FrameBody) frame.getBody());
            }
            // No mechanism exists to convert it to a v22 frame
            else
            {
                throw new InvalidFrameException("Unable to convert v23 frame:" + frame.getIdentifier() + " to a v22 frame");
            }
        }
        //Deprecated frame for v23
        else if (frame.getBody() instanceof FrameBodyDeprecated)
        {
            //Was it valid for this tag version, if so try and reconstruct
            if (ID3Tags.isID3v22FrameIdentifier(frame.getIdentifier()))
            {
                this.frameBody = frame.getBody();
                identifier = frame.getIdentifier();
                logger.config("DEPRECATED:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
            }
            //or was it still deprecated, if so leave as is
            else
            {
                this.frameBody = new FrameBodyDeprecated((FrameBodyDeprecated) frame.getBody());
                identifier = frame.getIdentifier();
                logger.config("DEPRECATED:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
            }
        }
        // Unknown Frame e.g NCON
        else
        {
            this.frameBody = new FrameBodyUnsupported((FrameBodyUnsupported) frame.getBody());
            identifier = frame.getIdentifier();
            logger.config("v2:UNKNOWN:Orig id is:" + frame.getIdentifier() + ":New id is:" + identifier);
        }
    }

    /**
     * Creates a new ID3v22 Frame from another frame of a different tag version
     *
     * @param frame to construct the new frame from
     * @throws org.jaudiotagger.tag.InvalidFrameException
     */
    public ID3v22Frame(AbstractID3v2Frame frame) throws InvalidFrameException
    {
        logger.config("Creating frame from a frame of a different version");
        if (frame instanceof ID3v22Frame)
        {
            throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument");
        }

        // If it is a v24 frame is it possible to convert it into a v23 frame, anmd then convert from that
        if (frame instanceof ID3v24Frame)
        {
            ID3v23Frame v23Frame = new ID3v23Frame(frame);
            createV22FrameFromV23Frame(v23Frame);
        }
        //If it is a v23 frame is it possible to convert it into a v22 frame
        else if (frame instanceof ID3v23Frame)
        {
            createV22FrameFromV23Frame((ID3v23Frame) frame);
        }
        this.frameBody.setHeader(this);
        logger.config("Created frame from a frame of a different version");
    }

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

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

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

    @Override 
    protected boolean isPadding(byte[] buffer)
    {
        if(
                (buffer[0]=='\0')&&
                (buffer[1]=='\0')&&
                (buffer[2]=='\0')
           )
        {
            return true;
        }
        return false;
    }

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

        byte[] buffer = new byte[getFrameSizeSize()];

        // Is this a valid identifier?
        if (!isValidID3v2FrameIdentifier(identifier))
        {
            logger.config("Invalid identifier:" + identifier);
            byteBuffer.position(byteBuffer.position() - (getFrameIdSize() - 1));
            throw new InvalidFrameIdentifierException(getLoggingFilename() + ":" + identifier + ":is not a valid ID3v2.20 frame");
        }
        //Read Frame Size (same size as Frame Id so reuse buffer)
        byteBuffer.get(buffer, 0, getFrameSizeSize());
        frameSize = decodeSize(buffer);
        if (frameSize < 0)
        {
            throw new InvalidFrameException(identifier + " has invalid size of:" + frameSize);
        }
        else if (frameSize == 0)
        {
            //We dont process this frame or add to framemap becuase contains no useful information
            logger.warning("Empty Frame:" + identifier);
            throw new EmptyFrameException(identifier + " is empty frame");
        }
        else if (frameSize > byteBuffer.remaining())
        {
            logger.warning("Invalid Frame size larger than size before mp3 audio:" + identifier);
            throw new InvalidFrameException(identifier + " is invalid frame");
        }
        else
        {
            logger.fine("Frame Size Is:" + frameSize);
            //Convert v2.2 to v2.4 id just for reading the data
            String id = ID3Tags.convertFrameID22To24(identifier);
            if (id == null)
            {
                //OK,it may be convertable to a v.3 id even though not valid v.4
                id = ID3Tags.convertFrameID22To23(identifier);
                if (id == null)
                {
                    // Is it a valid v22 identifier so should be able to find a
                    // frame body for it.
                    if (ID3Tags.isID3v22FrameIdentifier(identifier))
                    {
                        id = identifier;
                    }
                    // Unknown so will be created as FrameBodyUnsupported
                    else
                    {
                        id = UNSUPPORTED_ID;
                    }
                }
            }
            logger.fine("Identifier was:" + identifier + " reading using:" + id);

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

            try
            {
                frameBody = readBody(id, frameBodyBuffer, frameSize);
            }
            finally
            {
                //Update position of main buffer, so no attempt is made to reread these bytes
                byteBuffer.position(byteBuffer.position() + frameSize);
            }
        }
    }

    /**
     * Read Frame Size, which has to be decoded
     * @param buffer
     * @return
     */
    private int decodeSize(byte[] buffer)
    {
        BigInteger bi = new BigInteger(buffer);
        int tmpSize = bi.intValue();
        if (tmpSize < 0)
        {
            logger.warning("Invalid Frame Size of:" + tmpSize + "Decoded from bin:" + Integer.toBinaryString(tmpSize) + "Decoded from hex:" + Integer.toHexString(tmpSize));
        }
        return tmpSize;
    }


    /**
     * Write Frame raw data
     *
     * @throws IOException
     */
    public void write(ByteArrayOutputStream tagBuffer)
    {
        logger.config("Write Frame to Buffer" + getIdentifier());
        //This is where we will write header, move position to where we can
        //write body
        ByteBuffer headerBuffer = ByteBuffer.allocate(getFrameHeaderSize());

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

        //Write Frame Header
        //Write Frame ID must adjust can only be 3 bytes long
        headerBuffer.put(Utils.getDefaultBytes(getIdentifier(), "ISO-8859-1"), 0, getFrameIdSize());
        encodeSize(headerBuffer, frameBody.getSize());

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

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

    /**
     * Write Frame Size (can now be accurately calculated, have to convert 4 byte int
     * to 3 byte format.
     * @param headerBuffer
     * @param size
     */
    private void encodeSize(ByteBuffer headerBuffer, int size)
    {
        headerBuffer.put((byte) ((size & 0x00FF0000) >> 16));
        headerBuffer.put((byte) ((size & 0x0000FF00) >> 8));
        headerBuffer.put((byte) (size & 0x000000FF));
        logger.fine("Frame Size Is Actual:" + size + ":Encoded bin:" + Integer.toBinaryString(size) + ":Encoded Hex" + Integer.toHexString(size));
    }

    /**
     * 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
     * @return
     */
    public boolean isValidID3v2FrameIdentifier(String identifier)
    {
        Matcher m = ID3v22Frame.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);
        frameBody.createStructure();
        MP3File.getStructureFormatter().closeHeadingElement(TYPE_FRAME);
    }

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

    /**
     * @return true if considered a common frame
     */
    public boolean isBinary()
    {
        return ID3v22Frames.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 <2)
            {
                this.getBody().setTextEncoding(encodingId.byteValue());
            }
        }
    }
}