/*
* 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.mp3.MP3File;
import org.jaudiotagger.tag.*;
import org.jaudiotagger.tag.id3.framebody.AbstractID3v2FrameBody;
import org.jaudiotagger.tag.id3.framebody.FrameBodyEncrypted;
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.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.util.logging.Level;
/**
* This abstract class is each frame header inside a ID3v2 tag.
*
* @author : Paul Taylor
* @author : Eric Farng
* @version $Id: AbstractID3v2Frame.java 917 2010-09-27 18:34:30Z paultaylor $
*/
public abstract class AbstractID3v2Frame extends AbstractTagFrame implements TagTextField {
protected static final String TYPE_FRAME = "frame";
protected static final String TYPE_FRAME_SIZE = "frameSize";
protected static final String UNSUPPORTED_ID = "Unsupported";
//Frame identifier
protected String identifier = "";
//Frame Size
protected int frameSize;
//The purpose of this is to provide the filename that should be used when writing debug messages
//when problems occur reading or writing to file, otherwise it is difficult to track down the error
//when processing many files
private String loggingFilename = "";
/**
* @return size in bytes of the frameid field
*/
protected abstract int getFrameIdSize();
/**
* @return the size in bytes of the frame size field
*/
protected abstract int getFrameSizeSize();
/**
* @return the size in bytes of the frame header
*/
protected abstract int getFrameHeaderSize();
/**
* Create an empty frame
*/
protected AbstractID3v2Frame() {
}
/**
* This holds the Status flags (not supported in v2.20
*/
StatusFlags statusFlags = null;
/**
* This holds the Encoding flags (not supported in v2.20)
*/
EncodingFlags encodingFlags = null;
/**
* Create a frame based on another frame
*
* @param frame
*/
public AbstractID3v2Frame(AbstractID3v2Frame frame) {
super(frame);
}
/**
* Create a frame based on a body
*
* @param body
*/
public AbstractID3v2Frame(AbstractID3v2FrameBody body) {
this.frameBody = body;
this.frameBody.setHeader(this);
}
/**
* Create a new frame with empty body based on identifier
*
* @param identifier
*/
//TODO the identifier checks should be done in the relevent subclasses
public AbstractID3v2Frame(String identifier) {
//logger.info("Creating empty frame of type" + identifier);
this.identifier = identifier;
// 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" + identifier);
frameBody = c.newInstance();
} catch (ClassNotFoundException cnfe) {
logger.severe(cnfe.getMessage());
frameBody = new FrameBodyUnsupported(identifier);
}
//Instantiate Interface/Abstract should not happen
catch (InstantiationException ie) {
logger.log(Level.SEVERE, "InstantiationException:" + identifier, ie);
throw new RuntimeException(ie);
}
//Private Constructor shouild not happen
catch (IllegalAccessException iae) {
logger.log(Level.SEVERE, "IllegalAccessException:" + identifier, iae);
throw new RuntimeException(iae);
}
frameBody.setHeader(this);
if (this instanceof ID3v24Frame) {
frameBody.setTextEncoding(TagOptionSingleton.getInstance().getId3v24DefaultTextEncoding());
} else if (this instanceof ID3v23Frame) {
frameBody.setTextEncoding(TagOptionSingleton.getInstance().getId3v23DefaultTextEncoding());
}
//logger.info("Created empty frame of type" + identifier);
}
/**
* Retrieve the logging filename to be used in debugging
*
* @return logging filename to be used in debugging
*/
protected String getLoggingFilename() {
return loggingFilename;
}
/**
* Set logging filename when construct tag for read from file
*
* @param loggingFilename
*/
protected void setLoggingFilename(String loggingFilename) {
this.loggingFilename = loggingFilename;
}
/**
* Return the frame identifier, this only identifies the frame it does not provide a unique
* key, when using frames such as TXXX which are used by many fields *
*
* @return the frame identifier (Tag Field Interface)
*/
//TODO, this is confusing only returns the frameId, which does not neccessarily uniquely
//identify the frame
public String getId() {
return getIdentifier();
}
/**
* Return the frame identifier
*
* @return the frame identifier
*/
public String getIdentifier() {
return identifier;
}
//TODO:needs implementing but not sure if this method is required at all
public void copyContent(TagField field) {
}
/**
* Read the frameBody when frame marked as encrypted
*
* @param identifier
* @param byteBuffer
* @param frameSize
* @return
* @throws InvalidFrameException
* @throws InvalidDataTypeException
* @throws InvalidTagException
*/
protected AbstractID3v2FrameBody readEncryptedBody(String identifier, ByteBuffer byteBuffer, int frameSize)
throws InvalidFrameException, InvalidDataTypeException {
try {
AbstractID3v2FrameBody frameBody = new FrameBodyEncrypted(identifier, byteBuffer, frameSize);
frameBody.setHeader(this);
return frameBody;
} catch (InvalidTagException ite) {
throw new InvalidDataTypeException(ite);
}
}
protected boolean isPadding(byte[] buffer) {
if (
(buffer[0] == '\0') &&
(buffer[1] == '\0') &&
(buffer[2] == '\0') &&
(buffer[3] == '\0')
) {
return true;
}
return false;
}
/**
* Read the frame body from the specified file via the buffer
*
* @param identifier the frame identifier
* @param byteBuffer to read the frame body from
* @param frameSize
* @return a newly created FrameBody
* @throws InvalidFrameException unable to construct a framebody from the data
*/
@SuppressWarnings("unchecked")
//TODO using reflection is rather slow perhaps we should change this
protected AbstractID3v2FrameBody readBody(String identifier, ByteBuffer byteBuffer, int frameSize)
throws InvalidFrameException, InvalidDataTypeException {
//Use reflection to map id to frame body, which makes things much easier
//to keep things up to date,although slight performance hit.
//logger.finest("Creating framebody:start");
AbstractID3v2FrameBody frameBody;
try {
Class<AbstractID3v2FrameBody> c = (Class<AbstractID3v2FrameBody>) Class.forName("org.jaudiotagger.tag.id3.framebody.FrameBody" + identifier);
Class<?>[] constructorParameterTypes = {Class.forName("java.nio.ByteBuffer"), Integer.TYPE};
Object[] constructorParameterValues = {byteBuffer, frameSize};
Constructor<AbstractID3v2FrameBody> construct = c.getConstructor(constructorParameterTypes);
frameBody = (construct.newInstance(constructorParameterValues));
}
//No class defined for this frame type,use FrameUnsupported
catch (ClassNotFoundException cex) {
//logger.info(getLoggingFilename() + ":" + "Identifier not recognised:" + identifier + " using FrameBodyUnsupported");
try {
frameBody = new FrameBodyUnsupported(byteBuffer, frameSize);
}
//Should only throw InvalidFrameException but unfortunately legacy hierachy forces
//read method to declare it can throw InvalidtagException
catch (InvalidFrameException ife) {
throw ife;
} catch (InvalidTagException te) {
throw new InvalidFrameException(te.getMessage());
}
}
//An error has occurred during frame instantiation, if underlying cause is an unchecked exception or error
//propagate it up otherwise mark this frame as invalid
catch (InvocationTargetException ite) {
logger.severe(getLoggingFilename() + ":" + "An error occurred within abstractID3v2FrameBody for identifier:" + identifier + ":" + ite.getCause().getMessage());
if (ite.getCause() instanceof Error) {
throw (Error) ite.getCause();
} else if (ite.getCause() instanceof RuntimeException) {
throw (RuntimeException) ite.getCause();
} else if (ite.getCause() instanceof InvalidFrameException) {
throw (InvalidFrameException) ite.getCause();
} else if (ite.getCause() instanceof InvalidDataTypeException) {
throw (InvalidDataTypeException) ite.getCause();
} else {
throw new InvalidFrameException(ite.getCause().getMessage());
}
}
//No Such Method should not happen
catch (NoSuchMethodException sme) {
logger.log(Level.SEVERE, getLoggingFilename() + ":" + "No such method:" + sme.getMessage(), sme);
throw new RuntimeException(sme.getMessage());
}
//Instantiate Interface/Abstract should not happen
catch (InstantiationException ie) {
logger.log(Level.SEVERE, getLoggingFilename() + ":" + "Instantiation exception:" + ie.getMessage(), ie);
throw new RuntimeException(ie.getMessage());
}
//Private Constructor shouild not happen
catch (IllegalAccessException iae) {
logger.log(Level.SEVERE, getLoggingFilename() + ":" + "Illegal access exception :" + iae.getMessage(), iae);
throw new RuntimeException(iae.getMessage());
}
//logger.finest(getLoggingFilename() + ":" + "Created framebody:end" + frameBody.getIdentifier());
frameBody.setHeader(this);
return frameBody;
}
/**
* Get the next frame id, throwing an exception if unable to do this and check against just having padded data
*
* @param byteBuffer
* @return
* @throws PaddingException
* @throws InvalidFrameException
*/
protected String readIdentifier(ByteBuffer byteBuffer) throws PaddingException, InvalidFrameException {
byte[] buffer = new byte[getFrameIdSize()];
if (byteBuffer.position() + getFrameHeaderSize() >= byteBuffer.limit()) {
//logger.warning(getLoggingFilename() + ":" + "No space to find another frame:");
throw new InvalidFrameException(getLoggingFilename() + ":" + "No space to find another frame");
}
//Read the Frame Identifier
byteBuffer.get(buffer, 0, getFrameIdSize());
if (isPadding(buffer)) {
throw new PaddingException(getLoggingFilename() + ":only padding found");
}
identifier = new String(buffer);
//logger.fine(getLoggingFilename() + ":" + "Identifier is" + identifier);
return identifier;
}
/**
* This creates a new body based of type identifier but populated by the data
* in the body. This is a different type to the body being created which is why
* TagUtility.copyObject() can't be used. This is used when converting between
* different versions of a tag for frames that have a non-trivial mapping such
* as TYER in v3 to TDRC in v4. This will only work where appropriate constructors
* exist in the frame body to be created, for example a FrameBodyTYER requires a constructor
* consisting of a FrameBodyTDRC.
* <p/>
* If this method is called and a suitable constructor does not exist then an InvalidFrameException
* will be thrown
*
* @param identifier to determine type of the frame
* @param body
* @return newly created framebody for this type
* @throws InvalidFrameException if unable to construct a framebody for the identifier and body provided.
*/
@SuppressWarnings("unchecked")
//TODO using reflection is rather slow perhaps we should change this
protected AbstractID3v2FrameBody readBody(String identifier, AbstractID3v2FrameBody body) throws InvalidFrameException {
/* Use reflection to map id to frame body, which makes things much easier
* to keep things up to date, although slight performance hit.
*/
AbstractID3v2FrameBody frameBody;
try {
Class<AbstractID3v2FrameBody> c = (Class<AbstractID3v2FrameBody>) Class.forName("org.jaudiotagger.tag.id3.framebody.FrameBody" + identifier);
Class<?>[] constructorParameterTypes = {body.getClass()};
Object[] constructorParameterValues = {body};
Constructor<AbstractID3v2FrameBody> construct = c.getConstructor(constructorParameterTypes);
frameBody = (construct.newInstance(constructorParameterValues));
} catch (ClassNotFoundException cex) {
//logger.info("Identifier not recognised:" + identifier + " unable to create framebody");
throw new InvalidFrameException("FrameBody" + identifier + " does not exist");
}
//If suitable constructor does not exist
catch (NoSuchMethodException sme) {
logger.log(Level.SEVERE, "No such method:" + sme.getMessage(), sme);
throw new InvalidFrameException("FrameBody" + identifier + " does not have a constructor that takes:" + body.getClass().getName());
} catch (InvocationTargetException ite) {
logger.severe("An error occurred within abstractID3v2FrameBody");
logger.log(Level.SEVERE, "Invocation target exception:" + ite.getCause().getMessage(), ite.getCause());
if (ite.getCause() instanceof Error) {
throw (Error) ite.getCause();
} else if (ite.getCause() instanceof RuntimeException) {
throw (RuntimeException) ite.getCause();
} else {
throw new InvalidFrameException(ite.getCause().getMessage());
}
}
//Instantiate Interface/Abstract should not happen
catch (InstantiationException ie) {
logger.log(Level.SEVERE, "Instantiation exception:" + ie.getMessage(), ie);
throw new RuntimeException(ie.getMessage());
}
//Private Constructor shouild not happen
catch (IllegalAccessException iae) {
logger.log(Level.SEVERE, "Illegal access exception :" + iae.getMessage(), iae);
throw new RuntimeException(iae.getMessage());
}
//logger.finer("frame Body created" + frameBody.getIdentifier());
frameBody.setHeader(this);
return frameBody;
}
public byte[] getRawContent() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
write(baos);
return baos.toByteArray();
}
public abstract void write(ByteArrayOutputStream tagBuffer);
/**
* @param b
*/
public void isBinary(boolean b) {
//do nothing because whether or not a field is binary is defined by its id and is immutable
}
public boolean isEmpty() {
AbstractTagFrameBody body = this.getBody();
if (body == null) {
return true;
}
//TODO depends on the body
return false;
}
public StatusFlags getStatusFlags() {
return statusFlags;
}
public EncodingFlags getEncodingFlags() {
return encodingFlags;
}
public class StatusFlags {
protected static final String TYPE_FLAGS = "statusFlags";
protected byte originalFlags;
protected byte writeFlags;
protected StatusFlags() {
}
/**
* This returns the flags as they were originally read or created
*
* @return
*/
public byte getOriginalFlags() {
return originalFlags;
}
/**
* This returns the flags amended to meet specification
*
* @return
*/
public byte getWriteFlags() {
return writeFlags;
}
public void createStructure() {
}
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof StatusFlags)) {
return false;
}
StatusFlags that = (StatusFlags) obj;
return
EqualsUtil.areEqual(this.getOriginalFlags(), that.getOriginalFlags()) &&
EqualsUtil.areEqual(this.getWriteFlags(), that.getWriteFlags());
}
}
class EncodingFlags {
protected static final String TYPE_FLAGS = "encodingFlags";
protected byte flags;
protected EncodingFlags() {
resetFlags();
}
protected EncodingFlags(byte flags) {
setFlags(flags);
}
public byte getFlags() {
return flags;
}
public void setFlags(byte flags) {
this.flags = flags;
}
public void resetFlags() {
setFlags((byte) 0);
}
public void createStructure() {
}
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof EncodingFlags)) {
return false;
}
EncodingFlags that = (EncodingFlags) obj;
return EqualsUtil.areEqual(this.getFlags(), that.getFlags());
}
}
/**
* Return String Representation of frame
*/
public void createStructure() {
MP3File.getStructureFormatter().openHeadingElement(TYPE_FRAME, getIdentifier());
MP3File.getStructureFormatter().closeHeadingElement(TYPE_FRAME);
}
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof AbstractID3v2Frame)) {
return false;
}
AbstractID3v2Frame that = (AbstractID3v2Frame) obj;
return super.equals(that);
}
/**
* Returns the content of the field.
* <p/>
* For frames consisting of different fields, this will return the value deemed to be most
* likely to be required
*
* @return Content
*/
public String getContent() {
return getBody().getUserFriendlyValue();
}
/**
* Returns the current used charset encoding.
*
* @return Charset encoding.
*/
public String getEncoding() {
return TextEncoding.getInstanceOf().getValueForId(this.getBody().getTextEncoding());
}
/**
* Sets the content of the field.
*
* @param content fields content.
*/
public void setContent(String content) {
throw new UnsupportedOperationException("Not implemeneted please use the generic tag methods for setting content");
}
}