/*
You may freely copy, distribute, modify and use this class as long
as the original author attribution remains intact. See message
below.
Copyright (C) 2001-2003 Christian Pesch. All Rights Reserved.
*/
package slash.metamusic.mp3;
import slash.metamusic.mp3.sections.*;
import slash.metamusic.mp3.util.BitConversion;
import javax.activation.MimeType;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* My instances represent a ID3v2 frame of the ID3v2 header as
* described in http://www.id3.org/id3v2.3.0.html#sec3.3.
*
* @author Christian Pesch
* @version $Id: ID3v2Frame.java 958 2007-02-28 14:44:37Z cpesch $
*/
public class ID3v2Frame {
/**
* Logging output
*/
protected static final Logger log = Logger.getLogger(ID3v2Frame.class.getName());
public static final int OBSOLETE_SIZE_SIZE = 3;
public static final int SIZE_SIZE = 4;
public static final int FLAG_SIZE = 2;
public static final int EXTRA_SIZE = 5;
protected ID3v2Frame(ID3v2Tag tag, ID3v2Version version, byte[] content) {
this.tag = tag;
this.version = version;
if (tag != null) {
this.fileAlterDiscard = tag.isDefaultFileAlterPreservation();
if (content != null) {
try {
if (!parseContent(content)) {
Thread.dumpStack();
log.severe("Cannot parse " + new String(content));
}
} catch (IOException e) {
log.severe("Cannot parse " + new String(content));
}
} else
this.sections = tag.getSections();
if (version == null)
this.version = tag.getVersion();
} else {
if (version == null)
this.version = new ID3v2Version();
}
}
/**
* Created while reading ID3v2Header
* @param version the version to use
*/
ID3v2Frame(ID3v2Version version) {
this(null, version, null);
}
/**
* Created while adding new frames
* @param tagName the name of the {@link ID3v2Tag}
* @param version the version to use
*/
ID3v2Frame(String tagName, ID3v2Version version) {
this(new ID3v2Tag(tagName), version, null);
}
public ID3v2Frame(String tagName) {
this(tagName, null);
}
public ID3v2Version getVersion() {
return version;
}
void migrateToVersion(ID3v2Version current) {
if (!getVersion().equals(current)) {
// crude migration
if (tag.getName().equals("PIC")) {
sections.add(getSectionIndex(DescriptionSection.class), new PictureTypeSection());
}
tag.migrateToVersion(current);
version = current;
}
}
// --- read object -----------------------------------------
/**
* Pulls out information from the frame.
*
* @param data the frame data
* @param offset the offset from which to read
* @return the number of bytes read
* @throws IOException if parsing fails due to IO problems
*/
public int parse(byte[] data, int offset) throws IOException {
valid = false;
tag = new ID3v2Tag(getVersion());
int tagSize = getVersion().getTagSize();
// frame name would be behind end of data
if (offset + tagSize > data.length) {
return 0;
}
// tag is always in ISO-8859-1 encoded
String tagName = new String(data, offset, tagSize, ID3v2Header.ISO_8859_1_ENCODING);
tag.setName(tagName);
this.sections = tag.getSections();
if (!tag.isValid())
return 0;
// ID3v2 3.0 has 4 bytes size, 2.0 just 3
int sizeOffset = offset + tagSize;
int sizeSize = getVersion().isObsolete() ? OBSOLETE_SIZE_SIZE : SIZE_SIZE;
// frame size be behind length of data
if (sizeOffset + sizeSize > data.length) {
return 0;
}
// ID3v2 3.0 has 4 bytes size, 2.0 just 3
int frameSize = getVersion().isObsolete() ?
BitConversion.extract3BigEndian(data, sizeOffset) :
BitConversion.extract4BigEndian(data, sizeOffset);
// if frame size is zero or less, this frame is invalid
if (frameSize <= 0)
return 0;
// if frame frame is longer than data, this frame is invalid
if (frameSize > data.length)
return 0;
// ID3v2 2.0 has no flags and encoding
int flagOffset = sizeOffset + sizeSize;
int flagSize = getVersion().isObsolete() ? 0 : FLAG_SIZE;
if (!getVersion().isObsolete()) {
parseFlags(data, flagOffset);
// TODO flags are not really used by now - no encryption, compression etc.
// TODO parsing extra flags does not work
parseExtraFlags(data, flagOffset + flagSize);
}
int contentOffset = flagOffset + flagSize;
//noinspection UnnecessaryLocalVariable
int contentSize = frameSize;
// if frame frame is longer than data, this frame is invalid
if (contentOffset + contentSize > data.length)
return 0;
byte[] content = new byte[contentSize];
System.arraycopy(data, contentOffset, content, 0, contentSize);
// if the content cannot be parsed, this frame is invalid
valid = parseContent(content);
if (!valid)
return 0;
return tagSize + sizeSize + flagSize + frameSize;
}
/**
* Read the information from the flags array.
*
* @param data the flags found in the frame header
* @param offset the offset from which to read
*/
protected void parseFlags(byte[] data, int offset) {
if (data.length < offset + FLAG_SIZE) {
log.severe("Error parsing flags of frame: " + tag + ". " +
"Expected flags not in data. ");
} else {
byte first = data[offset];
tagAlterDiscard = BitConversion.getBit(first, 6) == 1;
fileAlterDiscard = BitConversion.getBit(first, 5) == 1;
readOnly = BitConversion.getBit(first, 4) == 1;
byte second = data[offset + 1];
grouped = BitConversion.getBit(second, 6) == 1;
compressed = BitConversion.getBit(second, 3) == 1;
encrypted = BitConversion.getBit(second, 2) == 1;
unsynchronized = BitConversion.getBit(second, 1) == 1;
lengthIndicator = BitConversion.getBit(second, 0) == 1;
if (compressed && !lengthIndicator)
log.severe("Error parsing flags of frame: " + tag + ". " +
"Compressed bit set without data length bit set.");
}
}
/**
* Pulls out extra information inserted in the frame data depending
* on what flags are set.
*
* @param data the frame data
* @param offset the offset from which to read
* @return the amount of bytes read
*/
protected int parseExtraFlags(byte[] data, int offset) {
int bytesRead = 0;
if (grouped) {
group = data[offset + bytesRead];
bytesRead += 1;
// System.out.println("group: " + group);
}
if (encrypted) {
encryption = data[offset + bytesRead];
bytesRead += 1;
// System.out.println("encryption: " + encryption);
}
if (lengthIndicator) {
byte[] size = new byte[SIZE_SIZE];
System.arraycopy(data, offset + bytesRead, size, 0, size.length);
dataLength = BitConversion.extract4BigEndian(size);
bytesRead += size.length;
// System.out.println("dataLength: " + dataLength);
}
return bytesRead;
}
/**
* Parse the content of the frame.
*
* @param content the frame content
* @return true, if the content was parse successfully
* @throws IOException if parsing failed
*/
protected boolean parseContent(byte[] content) throws IOException {
if (content == null)
return false;
int offset = 0;
for (AbstractSection section : sections) {
// not enough content to parse
if (offset > content.length)
return false;
int bytesRead = section.parse(content, offset, this);
if (bytesRead > 0) {
offset += bytesRead;
}
}
return true;
}
// --- write object ----------------------------------------
/**
* Return the byte representation of this frame.
*
* @return an array with the byte representation of this frame
* @throws UnsupportedEncodingException if some encoding fails
*/
public byte[] getBytes() throws UnsupportedEncodingException {
byte[] data = new byte[(int) getFrameSize()];
int offset = 0;
byte[] tagData = tag.getBytes();
System.arraycopy(tagData, 0, data, 0, tagData.length);
offset += tagData.length;
byte[] tagSize = getVersion().isObsolete() ?
BitConversion.create3BigEndian((int) getContentSize()) :
BitConversion.create4BigEndian((int) getContentSize());
System.arraycopy(tagSize, 0, data, offset, tagSize.length);
offset += tagSize.length;
// ID3v2 2.0 has no flags and encoding
if (!getVersion().isObsolete()) {
byte[] tagFlags = getFlagBytes();
System.arraycopy(tagFlags, 0, data, offset, tagFlags.length);
offset += tagFlags.length;
byte[] tagExtra = getExtraDataBytes();
System.arraycopy(tagExtra, 0, data, offset, tagExtra.length);
offset += tagExtra.length;
}
byte[] tagContent = getContentBytes();
System.arraycopy(tagContent, 0, data, offset, tagContent.length);
return data;
}
/**
* A helper function for the getFrameBytes method that processes the
* info in the frame and returns the FLAG_SIZE byte array of flags
* to be added to the header.
*
* @return a value of type 'byte[]'
*/
protected byte[] getFlagBytes() {
byte flags[] = {0x00, 0x00};
if (tagAlterDiscard) {
flags[0] = BitConversion.setBit(flags[0], 6);
}
if (fileAlterDiscard) {
flags[0] = BitConversion.setBit(flags[0], 5);
}
if (readOnly) {
flags[0] = BitConversion.setBit(flags[0], 4);
}
if (grouped) {
flags[1] = BitConversion.setBit(flags[1], 6);
}
if (compressed) {
flags[1] = BitConversion.setBit(flags[1], 3);
}
if (encrypted) {
flags[1] = BitConversion.setBit(flags[1], 2);
}
if (unsynchronized) {
flags[1] = BitConversion.setBit(flags[1], 1);
}
if (lengthIndicator) {
flags[1] = BitConversion.setBit(flags[1], 0);
}
return flags;
}
/**
* A helper function for the getFrameBytes function that returns an array
* of all the data contained in any extra fields that may be present in
* this frame. This includes the group, the encryption type, and the
* length indicator. The length of the array returned is variable length.
*
* @return an array of bytes containing the extra data fields in the frame
*/
protected byte[] getExtraDataBytes() {
byte[] buf = new byte[EXTRA_SIZE];
int bytesCopied = 0;
if (grouped) {
buf[bytesCopied] = group;
bytesCopied += 1;
}
if (encrypted) {
buf[bytesCopied] = encryption;
bytesCopied += 1;
}
if (lengthIndicator) {
byte[] size = BitConversion.create4BigEndian(dataLength);
System.arraycopy(size, 0, buf, bytesCopied, size.length);
bytesCopied += size.length;
}
byte[] result = new byte[bytesCopied];
System.arraycopy(buf, 0, result, 0, bytesCopied);
return result;
}
protected byte[] getContentBytes() {
byte[] result = new byte[0];
for (AbstractSection section : sections) {
try {
byte[] bytes = section.getBytes(this);
byte[] newResult = new byte[result.length + bytes.length];
System.arraycopy(result, 0, newResult, 0, result.length);
System.arraycopy(bytes, 0, newResult, result.length, bytes.length);
result = newResult;
} catch (UnsupportedEncodingException e) {
log.severe("Cannot encode frame: " + e.getMessage());
return new byte[0];
}
}
return result;
}
// --- get object ------------------------------------------
public boolean isValid() {
return valid;
}
public long getFrameSize() {
return tag.getName().length() +
(getVersion().isObsolete() ? OBSOLETE_SIZE_SIZE : (SIZE_SIZE + FLAG_SIZE)) +
getContentSize();
/* TODO does not work
if(grouped) {
size += 1;
}
if(encrypted) {
size += 1;
}
if(lengthIndicator) {
size += 4;
}
return size;
*/
}
public long getContentSize() {
return getContentBytes().length;
}
public boolean isTagWithName(String tagName) {
// queries always go for the ID3v2 3.0 names but tags with 2.0 names are found, too
return tagName.equals(tag.getName()) || tagName.equals(tag.getSuccessorName());
}
public String getTagName() {
return tag.getName();
}
public String getTagDescription() {
return tag.getDescription();
}
public String getTextEncoding() {
TextEncodingSection textEncodingSection = findSection(TextEncodingSection.class);
return textEncodingSection != null ? textEncodingSection.getEncoding() : ID3v2Header.ISO_8859_1_ENCODING;
}
public String getTextContent() {
TextSection textSection = findSection(TextSection.class);
return textSection != null ? textSection.getText() : null;
}
public byte[] getByteContent() {
BytesSection bytesSection = findSection(BytesSection.class);
return bytesSection != null ? bytesSection.getBytes() : new byte[0];
}
public String getStringContent() {
StringBuffer buffer = new StringBuffer();
for (AbstractSection section : sections) {
String stringContent = section.getStringContent();
if (stringContent != null && stringContent.length() > 0) {
if (buffer.length() > 0)
buffer.append(",");
buffer.append(stringContent);
}
}
return buffer.toString();
}
public String getDescription() {
DescriptionSection descriptionSection = findSection(DescriptionSection.class);
return descriptionSection != null ? descriptionSection.getDescription() : null;
}
public String getLanguage() {
LanguageSection languageSection = findSection(LanguageSection.class);
return languageSection != null ? languageSection.getLanguage() : null;
}
public MimeType getMimeType() {
MimeTypeSection mimeTypeSection = findSection(MimeTypeSection.class);
return mimeTypeSection != null ? mimeTypeSection.getMimeType() : null;
}
@SuppressWarnings({"unchecked"})
public <T extends AbstractSection> T findSection(Class<T> sectionClass) {
List<T> abstractSections = (List<T>) sections;
for (T section : abstractSections) {
if (section.getClass().equals(sectionClass))
return section;
}
return null;
}
private int getSectionIndex(Class sectionClass) {
for (int i = 0; i < sections.size(); i++) {
AbstractSection section = sections.get(i);
if (section.getClass().equals(sectionClass))
return i;
}
return -1;
}
public void setTextEncoding(String encoding) {
TextEncodingSection encodingSection = findSection(TextEncodingSection.class);
if (encodingSection == null)
throw new IllegalArgumentException("No text encoding section in " + tag.getName() + " frame");
encodingSection.setEncoding(encoding);
}
public void setText(String text) {
TextSection textSection = findSection(TextSection.class);
if (textSection == null)
throw new IllegalArgumentException("No text section in " + tag.getName() + " frame");
textSection.setText(text);
}
public void setDescription(String description) {
DescriptionSection descriptionSection = findSection(DescriptionSection.class);
if (descriptionSection == null)
throw new IllegalArgumentException("No description section in " + tag.getName() + " frame");
descriptionSection.setDescription(description);
}
public void setLanguage(String language) {
LanguageSection languageSection = findSection(LanguageSection.class);
if (languageSection == null)
throw new IllegalArgumentException("No language section in " + tag.getName() + " frame");
languageSection.setLanguage(language);
}
public void setPictureType(PictureType picturetype) {
// no PictureTypeSection in ID3v2 2.0 PIC frame
if (getVersion().isObsolete())
return;
PictureTypeSection typeSection = findSection(PictureTypeSection.class);
if (typeSection == null)
throw new IllegalArgumentException("No picture type section in " + tag.getName() + " frame");
typeSection.setPictureType(picturetype);
}
public void setMimeType(MimeType mimetype) {
MimeTypeSection typeSection = findSection(MimeTypeSection.class);
if (typeSection == null)
throw new IllegalArgumentException("No mime type section in " + tag.getName() + " frame");
typeSection.setMimeType(mimetype);
}
public void setBytes(byte[] bytes) {
BytesSection bytesSection = findSection(BytesSection.class);
if (bytesSection == null)
throw new IllegalArgumentException("No bytes section in " + tag.getName() + " frame");
bytesSection.setBytes(bytes);
}
public String toString() {
return "ID3v2Frame[" +
"valid=" + isValid() + ", " +
"tag=" + tag + ", " +
"frameSize=" + getFrameSize() + ", " +
"contentSize=" + getContentSize() + ", " +
"tagAlterDiscard=" + tagAlterDiscard + ", " +
"fileAlterDiscard=" + fileAlterDiscard + ", " +
"readOnly=" + readOnly + ", " +
"grouped=" + grouped + ", " +
"compressed=" + compressed + ", " +
"encrypted=" + encrypted + ", " +
"unsynchronized=" + unsynchronized + ", " +
"lengthIndicator=" + lengthIndicator + ", " +
"sections=" + sections +
"]";
}
// --- member variables ------------------------------------
/**
* ID3v2Frame data
*/
protected boolean valid;
private ID3v2Version version;
protected ID3v2Tag tag;
/**
* flags
*/
protected boolean tagAlterDiscard = false;
protected boolean fileAlterDiscard = false;
protected boolean readOnly = false;
protected boolean grouped = false;
protected boolean compressed = false;
protected boolean encrypted = false;
protected boolean unsynchronized = false;
protected boolean lengthIndicator = false;
protected byte group = '0';
protected byte encryption = '0';
protected int dataLength = -1;
/**
* sections
*/
protected List<AbstractSection> sections = new ArrayList<AbstractSection>(1);
}