FileDocCategorySizeDatePackage
ID3V2Frame.javaAPI Docjid3 0.4625897Wed May 11 04:22:19 BST 2005org.blinkenlights.jid3.v2

ID3V2Frame.java

/*
 * Created on 26-Nov-2003
 *
 * Copyright (C)2003,2004 Paul Grebenc
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * $Id: ID3V2Frame.java,v 1.24 2005/05/11 03:22:19 paul Exp $
 */

package org.blinkenlights.jid3.v2;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.zip.*;

import org.blinkenlights.jid3.*;
import org.blinkenlights.jid3.crypt.*;
import org.blinkenlights.jid3.io.*;
import org.blinkenlights.jid3.util.*;

/**
 * @author paul
 *
 * The base class for all ID3 V2 frames.
 */
abstract public class ID3V2Frame implements ID3Subject, ID3Visitable
{
    // observers to changes in this object
    private Set m_oID3ObserverSet = new HashSet();
    
    // flags
    private boolean m_bTagAlterPreservationFlag = false;
    private boolean m_bFileAlterPreservationFlag = false;
    private boolean m_bReadOnlyFlag = false;
    private boolean m_bCompressionFlag = false;
    private boolean m_bEncryptionFlag = false;
    private boolean m_bGroupingIdentityFlag = false;
    
    // encryption support
    private byte m_byEncryptionMethod;
    private ICryptoAgent m_oCryptoAgent = null;
    private byte[] m_abyEncryptionData = null;
    
    public ID3V2Frame()
    {
    }
    
    public void addID3Observer(ID3Observer oID3Observer)
    {
        m_oID3ObserverSet.add(oID3Observer);
    }
    
    public void removeID3Observer(ID3Observer oID3Observer)
    {
        m_oID3ObserverSet.remove(oID3Observer);
    }
    
    public void notifyID3Observers()
        throws ID3Exception
    {
        Iterator oIter = m_oID3ObserverSet.iterator();
        
        while (oIter.hasNext())
        {
            ID3Observer oID3Observer = (ID3Observer)oIter.next();
            
            oID3Observer.update(this);
        }
    }
    
    /** Specify what should happen to this frame, if the tag it is in is modified by another program
     *  which does not recognize it.  If set to true, then this frame should be discarded by a program
     *  which does not recognize it.  If set to false, it should be left as is.
     *
     * @param bTagAlterPreservationFlagValue the state this flag should be set to
     */
    public void setTagAlterPreservationFlag(boolean bTagAlterPreservationFlagValue)
    {
        m_bTagAlterPreservationFlag = bTagAlterPreservationFlagValue;
    }

    /** Specify what should happen to this frame, if this file (other than the tag) is modified by a
     *  program which does not recognize it.  If set to true, then this frame should be discarded.  If
     *  set to false, then the frame should be preserved as is.
     *
     * @param bFileAlterPreservationFlagValue the state this flag should be set to
     */
    public void setFileAlterPreservationFlag(boolean bFileAlterPreservationFlagValue)
    {
        m_bFileAlterPreservationFlag = bFileAlterPreservationFlagValue;
    }
    
    /** Specify whether this frame should be considered read only or not.
     *
     * @param bReadOnlyFlagValue the state this flag should be set to
     */
    public void setReadOnlyFlag(boolean bReadOnlyFlagValue)
    {
        m_bReadOnlyFlag = bReadOnlyFlagValue;
    }

    /** Specify whether this frame is compressed or not.  The compression method used is zlib.
     *
     * @param bCompressionFlagValue the state this flag should be set to
     */
    public void setCompressionFlag(boolean bCompressionFlagValue)
    {
        m_bCompressionFlag = bCompressionFlagValue;
    }

    /** Set encryption method for this frame.  When an encryption method is specified, the
     *  frame will be encrypted before being written to a file.
     *
     * @param byEncryptionMethod the encryption method value to use (this value must match a
     *        method specified in an ENCR frame in this tag)
     */
    public void setEncryption(byte byEncryptionMethod)
        throws ID3Exception
    {
        m_bEncryptionFlag = true;
        m_byEncryptionMethod = byEncryptionMethod;
        
        notifyID3Observers();
    }
    
    /** Check whether this frame is encrypted.
     *
     * @return true if the frame is encrypted, false otherwise
     */
    public boolean isEncrypted()
    {
        return m_bEncryptionFlag;
    }
    
    /** Get the encryption method symbol used to encrypt this frame.
     *
     * @return the encryption method symbol used
     * @throws ID3Exception if this frame is not encrypted
     */
    public byte getEncryptionMethod()
        throws ID3Exception
    {
        if (! isEncrypted())
        {
            throw new ID3Exception("This frame is not encrypted.");
        }
        
        return m_byEncryptionMethod;
    }
    
    /** Set the crypto agent to be used in this frame.
     *
     * @param oCryptoAgent the crypto agent to be used for encrypting this frame
     * @param abyEncryptionData the encryption data to be used when encrypting/decrypting with this agent
     */
    void setCryptoAgent(ICryptoAgent oCryptoAgent, byte[] abyEncryptionData)
    {
        m_oCryptoAgent = oCryptoAgent;
        m_abyEncryptionData = abyEncryptionData;
    }

    /** Specify whether this frame belongs to a group of other frames or not.  If set, the group
     *  identifier must be specified.
     *
     * @param bGroupingIdentityFlagValue the state this flag should be set to
     */
    public void setGroupingIdentityFlag(boolean bGroupingIdentityFlagValue)
    {
        m_bGroupingIdentityFlag = bGroupingIdentityFlagValue;
    }
    
    /** Get the four bytes which uniquely specify of which type this frame is. */
    abstract protected byte[] getFrameId();

    /** Return number of bytes required to store the body of this frame.
     *
     * @return the number of bytes
     */
    private int getLength()
        throws IOException
    {
        ByteArrayOutputStream oBAOS = new ByteArrayOutputStream();
        ID3DataOutputStream oLengthIDOS = new ID3DataOutputStream(oBAOS);
        writeBody(oLengthIDOS);
        
        return oBAOS.size();
    }
    
    /** Represent the contents of this frame as a string.  For debugging purposes.
     *
     * @return a string representing this frame
     */
    public abstract String toString();
    
    /** Read an ID3 v2 frame from an ID3DataInputStream.
     * 
     * @param oID3DIS input stream from which a frame can directly be read
     * @return an ID3V2Frame which was read from the input stream
     * @throws ID3Exception if an error while reading occurs
     */
    static ID3V2Frame read(ID3DataInputStream oID3DIS)
        throws ID3Exception
    {
        return read(oID3DIS, new ENCRID3V2Frame[0]);
    }

    /** Read an ID3 v2 frame from an ID3DataInputStream, providing the possibility for decryption.
     * 
     * @param oID3DIS input stream from which a frame can directly be read
     * @param aoENCRID3V2Frame the array of ENCR frames which were read in, which describe encryption details
     * @return an ID3V2Frame which was read from the input stream
     * @throws ID3Exception if an error while reading occurs
     */
    static ID3V2Frame read(ID3DataInputStream oID3DIS, ENCRID3V2Frame[] aoENCRID3V2Frame)
        throws ID3Exception
    {
        String sFrameId = null;
        try
        {
            // read frame id
            byte[] abyFrameId = new byte[4];
            oID3DIS.readFully(abyFrameId);
            if (abyFrameId[0] == 0) // we're reading into the padding past the frames
            {
                return null;
            }
            sFrameId = new String(abyFrameId);
            
            //HACK: This is a work-around for a bug in the MP3ext Windows explorer extension.  It repeatedly
            //      writes the string "MP3ext V3.3.18(unicode)" into the padding area after the frames in a v2 tag.
            //      This is invalid, and the resulting frames are corrupt, according to the ID3 specification, which
            //      requires that padding contain only nulls.
            if (sFrameId.equals("MP3e"))
            {
                return null;
            }
            
            if (( ! sFrameId.matches("[A-Z0-9]+")) && ID3V2Tag.usingStrict())
            {
                throw new InvalidFrameID3Exception("Invalid frame id [" + sFrameId + "].");
            }
            
            // read size
            int iFrameSize = oID3DIS.readBE32();
            
            // read first flags byte
            int iFirstFlags = oID3DIS.readUnsignedByte();
            boolean bTagAlterPreservationFlag = ((iFirstFlags & 0x80) != 0);
            boolean bFileAlterPreservationFlag = ((iFirstFlags & 0x40) != 0);
            boolean bReadOnlyFlag = ((iFirstFlags & 0x20) != 0);
            boolean bUnknownFirstByteFlags = ((iFirstFlags & 0x1f) != 0);
            
            // read second flags byte
            int iSecondFlags = oID3DIS.readUnsignedByte();
            boolean bCompressionFlag = ((iSecondFlags & 0x80) != 0);
            boolean bEncryptionFlag = ((iSecondFlags & 0x40) != 0);
            boolean bGroupingIdentityFlag = ((iSecondFlags & 0x20) != 0);
            boolean bUnknownSecondByteFlags = ((iSecondFlags & 0x1f) != 0);
            
            // get length of uncompressed frame if compression set
            int iUncompressedSize = iFrameSize;
            if (bCompressionFlag)
            {
                iUncompressedSize = oID3DIS.readBE32();
                iFrameSize -= 4;    // FIX: four bytes read for frame size
            }
            
            // read encryption method byte, if used
            int iEncryptionMethodSymbol = 0;
            ICryptoAgent oCryptoAgent = null;
            byte[] abyEncryptionData = null;
            if (bEncryptionFlag)
            {
                iEncryptionMethodSymbol = oID3DIS.readUnsignedByte();
                iFrameSize -= 1;    // FIX: one byte for encryption method
                
                // this frame is encrypted.. do we have a means of decrypting it?
                for (int i=0; i < aoENCRID3V2Frame.length; i++)
                {
                    if ((aoENCRID3V2Frame[i].getEncryptionMethodSymbol() & 0xff) == iEncryptionMethodSymbol)
                    {
                        // we can decrypt this frame now
                        oCryptoAgent = ID3Encryption.getInstance().lookupCryptoAgent(aoENCRID3V2Frame[i].getOwnerIdentifier());
                        abyEncryptionData = aoENCRID3V2Frame[i].getEncryptionData();
                        break;
                    }
                }
                
                if (oCryptoAgent == null)
                {
                    ByteArrayOutputStream oEncryptedBAOS = new ByteArrayOutputStream();
                    ID3DataOutputStream oEncryptedIDOS = new ID3DataOutputStream(oEncryptedBAOS);
                    oEncryptedIDOS.write(abyFrameId);
                    oEncryptedIDOS.writeBE32(iFrameSize + (bEncryptionFlag ? 1 : 0));
                    oEncryptedIDOS.writeUnsignedByte(iFirstFlags);
                    oEncryptedIDOS.writeUnsignedByte(iSecondFlags);
                    if (bCompressionFlag)
                    {
                        oEncryptedIDOS.writeID3Four(iUncompressedSize);
                    }
                    oEncryptedIDOS.writeUnsignedByte(iEncryptionMethodSymbol);
                    
                    // determine the length of the compressed/encrypted data to be read in (minus the after header bytes we've already read)
                    int iFrameDataLength = iFrameSize;  // initial length of frame data before data we have already read

                    // read compressed/encrypted data
                    byte[] abyEncryptedFrameData = new byte[iFrameDataLength];
                    oID3DIS.readFully(abyEncryptedFrameData);
                    oEncryptedIDOS.write(abyEncryptedFrameData);
                    
                    // we cannot decrypt this frame at this time, so return it, as we read it, as a special encrypted frame object
                    EncryptedID3V2Frame oEncryptedFrame = new EncryptedID3V2Frame(sFrameId, oEncryptedBAOS.toByteArray());
                    
                    return oEncryptedFrame;
                }
            }
            
            // read frame data
            byte[] abyFrameData = null;
            if (bCompressionFlag)
            {
                // read compressed data
                byte[] abyCompressedFrameData = new byte[iFrameSize];
                oID3DIS.readFully(abyCompressedFrameData);
                
                // decrypt compressed data first, if encrypted
                if (bEncryptionFlag)
                {
                    abyCompressedFrameData = oCryptoAgent.decrypt(abyCompressedFrameData, abyEncryptionData);
                }
                
                // deflate data
                ByteArrayInputStream oBAIS = new ByteArrayInputStream(abyCompressedFrameData);
                InflaterInputStream oInflaterIS = new InflaterInputStream(oBAIS);
                ID3DataInputStream oInflaterID3DIS = new ID3DataInputStream(oInflaterIS);
                abyFrameData = new byte[iUncompressedSize];
                oInflaterID3DIS.readFully(abyFrameData);
            }
            else
            {
                abyFrameData = new byte[iFrameSize];
                oID3DIS.readFully(abyFrameData);
                
                // decrypt data, if encrypted
                if (bEncryptionFlag)
                {
                    abyFrameData = oCryptoAgent.decrypt(abyFrameData, abyEncryptionData);
                }
            }
            
            // create a frame object here based on what we've read
            ID3V2Frame oID3V2Frame;
            if (sFrameId.startsWith("T"))
            {
                // text information frame
                String sClassName = "org.blinkenlights.jid3.v2." + sFrameId + "TextInformationID3V2Frame";
                
                // if this class exists, then create such an object
                try
                {
                    Class oID3V2FrameClass = Class.forName(sClassName);
                    Class[] aoArgClassTypes = { InputStream.class };
                    Constructor oConstructor = oID3V2FrameClass.getConstructor(aoArgClassTypes);
                    Object[] aoConstructorArgs = { new ByteArrayInputStream(abyFrameData) };
                    oID3V2Frame = (ID3V2Frame)oConstructor.newInstance(aoConstructorArgs);
                }
                catch (ClassNotFoundException e)
                {
                    // unknown frame type
                    oID3V2Frame = new UnknownTextInformationID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData));
                }
                catch (NoSuchMethodException e)
                {
                    // unknown frame type
                    oID3V2Frame = new UnknownTextInformationID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData));
                }
                catch (InvocationTargetException e)
                {
                    // constructor threw an exception
                    if (e.getCause() instanceof Exception)
                    {
                        throw (Exception)e.getCause();
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
            else if (sFrameId.startsWith("W"))
            {
                // URL link frame
                String sClassName = "org.blinkenlights.jid3.v2." + sFrameId + "UrlLinkID3V2Frame";
                
                // if this class exists, then create such an object
                try
                {
                    Class oID3V2FrameClass = Class.forName(sClassName);
                    Class[] aoArgClassTypes = { InputStream.class };
                    Constructor oConstructor = oID3V2FrameClass.getConstructor(aoArgClassTypes);
                    Object[] aoConstructorArgs = { new ByteArrayInputStream(abyFrameData) };
                    oID3V2Frame = (ID3V2Frame)oConstructor.newInstance(aoConstructorArgs);
                }
                catch (ClassNotFoundException e)
                {
                    // unknown frame type
                    oID3V2Frame = new UnknownUrlLinkID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData));
                }
                catch (NoSuchMethodException e)
                {
                    // unknown frame type
                    oID3V2Frame = new UnknownUrlLinkID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData));
                }
                catch (InvocationTargetException e)
                {
                    // constructor threw an exception
                    if (e.getCause() instanceof Exception)
                    {
                        throw (Exception)e.getCause();
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
            else
            {
                // unique frame
                String sClassName = "org.blinkenlights.jid3.v2." + sFrameId + "ID3V2Frame";
                
                // if this class exists, then create such an object
                try
                {
                    Class oID3V2FrameClass = Class.forName(sClassName);
                    Class[] aoArgClassTypes = { InputStream.class };
                    Constructor oConstructor = oID3V2FrameClass.getConstructor(aoArgClassTypes);
                    Object[] aoConstructorArgs = { new ByteArrayInputStream(abyFrameData) };
                    oID3V2Frame = (ID3V2Frame)oConstructor.newInstance(aoConstructorArgs);
                }
                catch (ClassNotFoundException e)
                {
                    // unknown frame
                    oID3V2Frame = new UnknownID3V2Frame(sFrameId, abyFrameData);
                }
                catch (NoSuchMethodException e)
                {
                    // unknown frame type
                    oID3V2Frame = new UnknownID3V2Frame(sFrameId, abyFrameData);
                }
            }

            // set flags applicable to all v2 frames
            oID3V2Frame.setTagAlterPreservationFlag(bTagAlterPreservationFlag);
            oID3V2Frame.setFileAlterPreservationFlag(bFileAlterPreservationFlag);
            oID3V2Frame.setReadOnlyFlag(bReadOnlyFlag);
            oID3V2Frame.setCompressionFlag(bCompressionFlag);
            if (bEncryptionFlag)
            {
                oID3V2Frame.setEncryption((byte)iEncryptionMethodSymbol);
            }
            oID3V2Frame.setGroupingIdentityFlag(bGroupingIdentityFlag);
            
            return oID3V2Frame;
        }
        catch (ID3Exception e)
        {
            throw e;
        }
        catch (Exception e)
        {
            if (sFrameId == null)
            {
                throw new ID3Exception("Error reading v2 frame.", e);
            }
            else
            {
                throw new ID3Exception("Error reading " + sFrameId + " v2 frame.", e);
            }
        }
    }

    /** Write the header of this frame to an output stream.
     *
     * @param oOS the output stream to write to
     * @throws ID3Exception if an error occurs while writing
     */
    protected void writeHeader(OutputStream oOS)
        throws ID3Exception
    {
        try
        {
            ID3DataOutputStream oIDOS = new ID3DataOutputStream(oOS);
            
            // frame id
            oIDOS.write(getFrameId());
            // size
            int iActualLength = getActualLength();
            oIDOS.writeBE32(iActualLength);
            //oIDOS.writeBE32(getLength());
            // first flags
            int iFirstFlags = 0;
            if (m_bTagAlterPreservationFlag)
            {
                iFirstFlags |= (1 << 7);
            }
            if (m_bFileAlterPreservationFlag)
            {
                iFirstFlags |= (1 << 6);
            }
            if (m_bReadOnlyFlag)
            {
                iFirstFlags |= (1 << 5);
            }
            oIDOS.writeUnsignedByte(iFirstFlags);
            // second flags
            int iSecondFlags = 0;
            if (m_bCompressionFlag)
            {
                iSecondFlags |= (1 << 7);
            }
            if (m_bEncryptionFlag)
            {
                iSecondFlags |= (1 << 6);
            }
            if (m_bGroupingIdentityFlag)
            {
                iSecondFlags |= (1 << 5);
            }
            oIDOS.writeUnsignedByte(iSecondFlags);
            
            // write uncompressed length of the body, if it is compressed
            if (m_bCompressionFlag)
            {
                oIDOS.writeBE32(getLength());
            }
            // write encrypted method, if used
            if (m_bEncryptionFlag)
            {
                oIDOS.writeUnsignedByte(m_byEncryptionMethod & 0xff);
            }
        }
        catch (Exception e)
        {
            throw new ID3Exception("Error writing frame: " + e.getMessage(), e);
        }
    }
    
    /** Returns the length in bytes that the body of the frame will require when actually written to
     *  a file.  This may be shorter than the default length, if the frame is compressed.
     *
     * @return the length of the frame when written
     * @throws IOException if an error occurs while determining the compressed length
     */
    private int getActualLength()
        throws Exception
    {
        ByteArrayOutputStream oBAOS = new ByteArrayOutputStream();
        ID3DataOutputStream oIDOS = new ID3DataOutputStream(oBAOS);
        writeBody(oIDOS);
        byte[] abyBody = oBAOS.toByteArray();

        if (m_bCompressionFlag)
        {
            ByteArrayOutputStream oCompressedBAOS = new ByteArrayOutputStream();
            DeflaterOutputStream oDeflaterOS = new DeflaterOutputStream(oCompressedBAOS);
            oDeflaterOS.write(abyBody);
            oDeflaterOS.finish();
            abyBody = oCompressedBAOS.toByteArray();
        }
        
        if (m_bEncryptionFlag)
        {
            if (m_oCryptoAgent == null)
            {
                throw new ID3Exception("Crypto agent for method " + m_byEncryptionMethod + " not registered.  Cannot write frame.");
            }
            
            abyBody = m_oCryptoAgent.encrypt(abyBody, m_abyEncryptionData);
        }
        
        return abyBody.length + (m_bCompressionFlag ? 4 : 0) + (m_bEncryptionFlag ? 1 : 0);
    }
    
    /** Write the body of the frame to an ID3 data output stream.
     *
     * @param oIDOS the output stream to write to
     * @throws ID3Exception if an error occurs while writing
     */
    protected abstract void writeBody(ID3DataOutputStream oIDOS) throws IOException;

    /** Write this frame to an output stream.
     *
     * @param oOS the output stream to write to
     * @throws ID3Exception if an error occurs while writing the frame
     * @throws IOException if an error occurs while writing the frame
     */
    public void write(OutputStream oOS)
        throws IOException, ID3Exception
    {
        ID3DataOutputStream oIDOS = new ID3DataOutputStream(oOS);
        
        // write header
        writeHeader(oIDOS);
        
        // write body
        byte[] abyBody = null;
        
        // put original body bytes in abyBody
        ByteArrayOutputStream oBodyBAOS = new ByteArrayOutputStream();
        ID3DataOutputStream oBodyIDOS = new ID3DataOutputStream(oBodyBAOS);
        writeBody(oBodyIDOS);
        abyBody = oBodyBAOS.toByteArray();
        
        // if compression used, compress body byte array
        if (m_bCompressionFlag)
        {
            ByteArrayOutputStream oCompressedBAOS = new ByteArrayOutputStream();
            DeflaterOutputStream oDeflaterOS = new DeflaterOutputStream(oCompressedBAOS);
            oDeflaterOS.write(abyBody);
            oDeflaterOS.finish();
            abyBody = oCompressedBAOS.toByteArray();
        }
        
        // if encryption used, encrypt body byte array
        if (m_bEncryptionFlag)
        {
            if (m_oCryptoAgent == null)
            {
                throw new ID3Exception("Crypto agent for method " + m_byEncryptionMethod + " not registered.  Cannot write frame.");
            }
            
            abyBody = m_oCryptoAgent.encrypt(abyBody, m_abyEncryptionData);
        }
        
        oIDOS.write(abyBody);
    }
}