/*
* Entagged Audio Tag library
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
*
* 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jaudiotagger.audio.asf.data;
import org.jaudiotagger.audio.asf.util.Utils;
import org.jaudiotagger.logging.ErrorMessage;
import org.jaudiotagger.tag.TagOptionSingleton;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* This structure represents metadata objects in ASF {@link MetadataContainer}.<br>
* The values are
* {@linkplain ContainerType#assertConstraints(String, byte[], int, int, int)
* checked} against the capability introduced by the given
* {@link ContainerType} at construction.<br>
* <br>
* <b>Limitation</b>: Even though some container types do not restrict the data
* size to {@link Integer#MAX_VALUE}, this implementation does it (due to java
* nature).<br>
* 2 GiB of data should suffice, and even be to large for normal java heap.
*
* @author Christian Laireiter
*/
public class MetadataDescriptor implements Comparable<MetadataDescriptor>,
Cloneable {
/**
* Maximum value for WORD.
*/
public static final long DWORD_MAXVALUE = new BigInteger("FFFFFFFF", 16)
.longValue();
/**
* Logger instance.
*/
private static final Logger LOGGER = Logger
.getLogger("org.jaudiotagger.audio.asf.data");
/**
* The maximum language index allowed. (exclusive)
*/
public static final int MAX_LANG_INDEX = 127;
/**
* Maximum stream number. (inclusive)
*/
public static final int MAX_STREAM_NUMBER = 127;
/**
* Maximum value for a QWORD value (64 bit unsigned).<br>
*/
public static final BigInteger QWORD_MAXVALUE = new BigInteger(
"FFFFFFFFFFFFFFFF", 16);
/**
* Constant for the metadata descriptor-type for binary data.
*/
public final static int TYPE_BINARY = 1;
/**
* Constant for the metadata descriptor-type for booleans.
*/
public final static int TYPE_BOOLEAN = 2;
/**
* Constant for the metadata descriptor-type for DWORD (32-bit unsigned). <br>
*/
public final static int TYPE_DWORD = 3;
/**
* Constant for the metadata descriptor-type for GUIDs (128-bit).<br>
*/
public final static int TYPE_GUID = 6;
/**
* Constant for the metadata descriptor-type for QWORD (64-bit unsinged). <br>
*/
public final static int TYPE_QWORD = 4;
/**
* Constant for the metadata descriptor-type for Strings.
*/
public final static int TYPE_STRING = 0;
/**
* Constant for the metadata descriptor-type for WORD (16-bit unsigned). <br>
*/
public final static int TYPE_WORD = 5;
/**
* Maximum value for WORD.
*/
public static final int WORD_MAXVALUE = 65535;
/**
* Stores the containerType of the descriptor.
*/
private final ContainerType containerType;
/**
* The binary representation of the value.
*/
/*
* Note: The maximum data length could be up to a 64-Bit number (unsigned),
* but java for now handles just int sized byte[]. Since this class stores
* all data in primitive byte[] this size restriction is cascaded to all
* dependent implementations.
*/
private byte[] content = new byte[0];
/**
* This field shows the type of the metadata descriptor. <br>
*
* @see #TYPE_BINARY
* @see #TYPE_BOOLEAN
* @see #TYPE_DWORD
* @see #TYPE_GUID
* @see #TYPE_QWORD
* @see #TYPE_STRING
* @see #TYPE_WORD
*/
private int descriptorType;
/**
* the index of the language in the {@linkplain LanguageList language list}
* this descriptor applies to.<br>
*
*/
private int languageIndex = 0;
/**
* The name of the metadata descriptor.
*/
private final String name;
/**
* The number of the stream, this descriptor applies to.<br>
*/
private int streamNumber = 0;
/**
* Creates an Instance.<br>
*
* @param type
* the container type, this descriptor is resctricted to.
* @param propName
* Name of the MetadataDescriptor.
* @param propType
* Type of the metadata descriptor. See {@link #descriptorType}
*/
public MetadataDescriptor(final ContainerType type, final String propName,
final int propType) {
this(type, propName, propType, 0, 0);
}
/**
* Creates an Instance.
*
* @param type
* The container type the values (the whole descriptor) is
* restricted to.
* @param propName
* Name of the MetadataDescriptor.
* @param propType
* Type of the metadata descriptor. See {@link #descriptorType}
* @param stream
* the number of the stream the descriptor refers to.
* @param language
* the index of the language entry in a {@link LanguageList} this
* descriptor refers to.<br>
* <b>Consider</b>: No checks performed if language entry exists.
*
*/
public MetadataDescriptor(final ContainerType type, final String propName,
final int propType, final int stream, final int language) {
assert type != null;
type.assertConstraints(propName, new byte[0], propType, stream,
language);
this.containerType = type;
this.name = propName;
this.descriptorType = propType;
this.streamNumber = stream;
this.languageIndex = language;
}
/**
* Creates an instance.<br>
* Capabilities are set to {@link ContainerType#METADATA_LIBRARY_OBJECT}.<br>
*
* @param propName
* name of the metadata descriptor.
*/
public MetadataDescriptor(final String propName) {
this(propName, TYPE_STRING);
}
/**
* Creates an Instance.<br>
* Capabilities are set to {@link ContainerType#METADATA_LIBRARY_OBJECT}.<br>
*
* @param propName
* Name of the MetadataDescriptor.
* @param propType
* Type of the metadata descriptor. See {@link #descriptorType}
*/
public MetadataDescriptor(final String propName, final int propType) {
this(ContainerType.METADATA_LIBRARY_OBJECT, propName, propType, 0, 0);
}
/**
* Converts the descriptors value into a number if possible.<br>
* A boolean will be converted to "1" if <code>true</code>,
* otherwise "0".<br>
* String will be interpreted as number with radix "10".<br>
* Binary data will be interpreted as the default WORD,DWORD or QWORD binary
* representation, but only if the data does not exceed 8 bytes. This
* precaution is done to prevent creating a number of a multi kilobyte
* image.<br>
* A GUID cannot be converted in any case.
*
* @return number representation.
* @throws NumberFormatException
* If no conversion is supported.
*/
public BigInteger asNumber() {
BigInteger result = null;
switch (this.descriptorType) {
case TYPE_BOOLEAN:
case TYPE_WORD:
case TYPE_DWORD:
case TYPE_QWORD:
case TYPE_BINARY:
if (this.content.length > 8) {
throw new NumberFormatException(
"Binary data would exceed QWORD");
}
break;
case TYPE_GUID:
throw new NumberFormatException(
"GUID cannot be converted to a number.");
case TYPE_STRING:
result = new BigInteger(getString(), 10);
break;
default:
throw new IllegalStateException();
}
if (result == null) {
final byte[] copy = new byte[this.content.length];
for (int i = 0; i < copy.length; i++) {
copy[i] = this.content[this.content.length - (i + 1)];
}
result = new BigInteger(1, copy);
}
return result;
}
/**
* (overridden)
*
* @see java.lang.Object#clone()
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
/**
* {@inheritDoc}
*/
public int compareTo(final MetadataDescriptor other) {
return getName().compareTo(other.getName());
}
/**
* This method creates a copy of the current object. <br>
* All data will be copied, too. <br>
*
* @return A new metadata descriptor containing the same values as the
* current one.
*/
public MetadataDescriptor createCopy() {
final MetadataDescriptor result = new MetadataDescriptor(
this.containerType, this.name, this.descriptorType,
this.streamNumber, this.languageIndex);
result.content = getRawData();
return result;
}
/**
* (overridden)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(final Object obj) {
boolean result = false;
if (obj instanceof MetadataDescriptor) {
if (obj == this) {
result = true;
} else {
final MetadataDescriptor other = (MetadataDescriptor) obj;
result = other.getName().equals(getName())
&& other.descriptorType == this.descriptorType
&& other.languageIndex == this.languageIndex
&& other.streamNumber == this.streamNumber
&& Arrays.equals(this.content, other.content);
}
}
return result;
}
/**
* Returns the value of the MetadataDescriptor as a Boolean. <br>
* If no Conversion is Possible false is returned. <br>
* <code>true</code> if first byte of {@link #content}is not zero.
*
* @return boolean representation of the current value.
*/
public boolean getBoolean() {
return this.content.length > 0 && this.content[0] != 0;
}
/**
* This method will return a byte array, which can directly be written into
* an "Extended Content Description"-chunk. <br>
*
* @return byte[] with the data, that occurs in ASF files.
* @deprecated {@link #writeInto(OutputStream,ContainerType)} is used
*/
@Deprecated
public byte[] getBytes() {
final ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
writeInto(result, this.containerType);
} catch (final IOException e) {
LOGGER.warning(e.getMessage());
}
return result.toByteArray();
}
/**
* Returns the container type this descriptor ist restricted to.
*
* @return the container type
*/
public ContainerType getContainerType() {
return this.containerType;
}
/**
* Returns the size (in bytes) this descriptor will take when written to an
* ASF file.<br>
*
* @param type
* the container type for which the size is calculated.
*
* @return size of the descriptor in an ASF file.
*/
public int getCurrentAsfSize(final ContainerType type) {
/*
* 2 bytes name length, 2 bytes name zero term, 2 bytes type, 2 bytes
* content length
*/
int result = 8;
if (type != ContainerType.EXTENDED_CONTENT) {
// Stream number and language index (respectively reserved field).
// And +2 bytes, because data type is 32 bit, not 16
result += 6;
}
result += getName().length() * 2;
if (this.getType() == TYPE_BOOLEAN) {
result += 2;
if (type == ContainerType.EXTENDED_CONTENT) {
// Extended content description boolean values are stored with
// 32-bit
result += 2;
}
} else {
result += this.content.length;
if (TYPE_STRING == this.getType()) {
result += 2; // zero term of content string.
}
}
return result;
}
/**
* Returns the GUID value, if content could represent one.
*
* @return GUID value
*/
public GUID getGuid() {
GUID result = null;
if (getType() == TYPE_GUID && this.content.length == GUID.GUID_LENGTH) {
result = new GUID(this.content);
}
return result;
}
/**
* Returns the index of the language that is referred (see
* {@link LanguageList}):
*
* @return the language index
*/
public int getLanguageIndex() {
return this.languageIndex;
}
/**
* This method returns the name of the metadata descriptor.
*
* @return Name.
*/
public String getName() {
return this.name;
}
/**
* This method returns the value of the metadata descriptor as a long. <br>
* Converts the needed amount of byte out of {@link #content}to a number. <br>
* Only possible if {@link #getType()}equals on of the following: <br>
* <li>
*
* @return integer value.
* @see #TYPE_BOOLEAN </li> <li>
* @see #TYPE_DWORD </li> <li>
* @see #TYPE_QWORD </li> <li>
* @see #TYPE_WORD </li>
*/
public long getNumber() {
int bytesNeeded;
switch (getType()) {
case TYPE_BOOLEAN:
bytesNeeded = 1;
break;
case TYPE_DWORD:
bytesNeeded = 4;
break;
case TYPE_QWORD:
bytesNeeded = 8;
break;
case TYPE_WORD:
bytesNeeded = 2;
break;
default:
throw new UnsupportedOperationException(
"The current type doesn't allow an interpretation as a number. ("
+ getType() + ")");
}
if (bytesNeeded > this.content.length) {
throw new IllegalStateException(
"The stored data cannot represent the type of current object.");
}
long result = 0;
for (int i = 0; i < bytesNeeded; i++) {
result |= (((long) this.content[i] & 0xFF) << (i * 8));
}
return result;
}
/**
* This method returns a copy of the content of the descriptor. <br>
*
* @return The content in binary representation, as it would be written to
* asf file. <br>
*/
public byte[] getRawData() {
final byte[] copy = new byte[this.content.length];
System.arraycopy(this.content, 0, copy, 0, this.content.length);
return copy;
}
/**
* Returns the size (in bytes) the binary representation of the content
* uses. (length of {@link #getRawData()})<br>
*
* @return size of binary representation of the content.
*/
public int getRawDataSize() {
return this.content.length;
}
/**
* Returns the stream number this descriptor applies to.<br>
*
* @return the stream number.
*/
public int getStreamNumber() {
return this.streamNumber;
}
/**
* Returns the value of the MetadataDescriptor as a String. <br>
*
* @return String - Representation Value
*/
public String getString() {
String result = null;
switch (getType()) {
case TYPE_BINARY:
result = "binary data";
break;
case TYPE_BOOLEAN:
result = String.valueOf(getBoolean());
break;
case TYPE_GUID:
result = getGuid() == null ? "Invalid GUID" : getGuid().toString();
break;
case TYPE_QWORD:
case TYPE_DWORD:
case TYPE_WORD:
result = String.valueOf(getNumber());
break;
case TYPE_STRING:
try {
result = new String(this.content, "UTF-16LE");
} catch (final UnsupportedEncodingException e) {
LOGGER.warning(e.getMessage());
}
break;
default:
throw new IllegalStateException("Current type is not known.");
}
return result;
}
/**
* Returns the type of the metadata descriptor. <br>
*
* @return the value of {@link #descriptorType}
* @see #TYPE_BINARY
* @see #TYPE_BOOLEAN
* @see #TYPE_DWORD
* @see #TYPE_GUID
* @see #TYPE_QWORD
* @see #TYPE_STRING
* @see #TYPE_WORD
*/
public int getType() {
return this.descriptorType;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return this.name.hashCode();
}
/**
* This method checks if the binary data is empty. <br>
* Disregarding the type of the descriptor its content is stored as a byte
* array.
*
* @return <code>true</code> if no value is set.
*/
public boolean isEmpty() {
return this.content.length == 0;
}
/**
* Sets the Value of the current metadata descriptor. <br>
* Using this method will change {@link #descriptorType}to
* {@link #TYPE_BINARY}.<br>
*
* @param data
* Value to set.
* @throws IllegalArgumentException
* if data is invalid for {@linkplain #getContainerType()
* container}.
*/
public void setBinaryValue(final byte[] data)
throws IllegalArgumentException {
this.containerType.assertConstraints(this.name, data,
this.descriptorType, this.streamNumber, this.languageIndex);
this.content = data.clone();
this.descriptorType = TYPE_BINARY;
}
/**
* Sets the Value of the current metadata descriptor. <br>
* Using this method will change {@link #descriptorType}to
* {@link #TYPE_BOOLEAN}.<br>
*
* @param value
* Value to set.
*/
public void setBooleanValue(final boolean value) {
this.content = new byte[] { value ? (byte) 1 : 0 };
this.descriptorType = TYPE_BOOLEAN;
}
/**
* Sets the Value of the current metadata descriptor. <br>
* Using this method will change {@link #descriptorType}to
* {@link #TYPE_DWORD}.
*
* @param value
* Value to set.
*/
public void setDWordValue(final long value) {
if (value < 0 || value > DWORD_MAXVALUE) {
throw new IllegalArgumentException("value out of range (0-"
+ DWORD_MAXVALUE + ")");
}
this.content = Utils.getBytes(value, 4);
this.descriptorType = TYPE_DWORD;
}
/**
* Sets the value of the metadata descriptor.<br>
* Using this method will change {@link #descriptorType} to
* {@link #TYPE_GUID}
*
* @param value
* value to set.
*/
public void setGUIDValue(final GUID value) {
this.containerType.assertConstraints(this.name, value.getBytes(),
TYPE_GUID, this.streamNumber, this.languageIndex);
this.content = value.getBytes();
this.descriptorType = TYPE_GUID;
}
/**
* Sets the index of the referred language (see {@link LanguageList}).<br>
* <b>Consider</b>: The {@linkplain #containerType requirements} must be
* held.
*
* @param language
* the language index to set
*/
public void setLanguageIndex(final int language) {
this.containerType.assertConstraints(this.name, this.content,
this.descriptorType, this.streamNumber, language);
this.languageIndex = language;
}
/**
* Sets the Value of the current metadata descriptor. <br>
* Using this method will change {@link #descriptorType}to
* {@link #TYPE_QWORD}
*
* @param value
* Value to set.
* @throws NumberFormatException
* on <code>null</code> values.
* @throws IllegalArgumentException
* on illegal values or values exceeding range.
*/
public void setQWordValue(final BigInteger value)
throws IllegalArgumentException {
if (value == null) {
throw new NumberFormatException("null");
}
if (BigInteger.ZERO.compareTo(value) > 0) {
throw new IllegalArgumentException(
"Only unsigned values allowed (no negative)");
}
if (MetadataDescriptor.QWORD_MAXVALUE.compareTo(value) < 0) {
throw new IllegalArgumentException(
"Value exceeds QWORD (64 bit unsigned)");
}
this.content = new byte[8];
final byte[] valuesBytes = value.toByteArray();
if (valuesBytes.length <= 8) {
for (int i = valuesBytes.length - 1; i >= 0; i--) {
this.content[valuesBytes.length - (i + 1)] = valuesBytes[i];
}
} else {
/*
* In case of 64-Bit set
*/
Arrays.fill(this.content, (byte) 0xFF);
}
this.descriptorType = TYPE_QWORD;
}
/**
* Sets the Value of the current metadata descriptor. <br>
* Using this method will change {@link #descriptorType}to
* {@link #TYPE_QWORD}
*
* @param value
* Value to set.
*/
public void setQWordValue(final long value) {
if (value < 0) {
throw new IllegalArgumentException("value out of range (0-"
+ MetadataDescriptor.QWORD_MAXVALUE.toString() + ")");
}
this.content = Utils.getBytes(value, 8);
this.descriptorType = TYPE_QWORD;
}
/**
* Sets the stream number the descriptor applies to.<br>
* <b>Consider</b>: The {@linkplain #containerType requirements} must be
* held.
*
* @param stream
* the stream number to set
*/
public void setStreamNumber(final int stream) {
this.containerType.assertConstraints(this.name, this.content,
this.descriptorType, stream, this.languageIndex);
this.streamNumber = stream;
}
/**
* This method converts the given string value into the current
* {@linkplain #getType() data type}.
*
* @param value
* value to set.
* @throws IllegalArgumentException
* If conversion was impossible.
*/
public void setString(final String value)
throws IllegalArgumentException {
try {
switch (getType()) {
case TYPE_BINARY:
throw new IllegalArgumentException(
"Cannot interpret binary as string.");
case TYPE_BOOLEAN:
setBooleanValue(Boolean.parseBoolean(value));
break;
case TYPE_DWORD:
setDWordValue(Long.parseLong(value));
break;
case TYPE_QWORD:
setQWordValue(new BigInteger(value, 10));
break;
case TYPE_WORD:
setWordValue(Integer.parseInt(value));
break;
case TYPE_GUID:
setGUIDValue(GUID.parseGUID(value));
break;
case TYPE_STRING:
setStringValue(value);
break;
default:
// new Type added but not handled.
throw new IllegalStateException();
}
} catch (final NumberFormatException nfe) {
throw new IllegalArgumentException(
"Value cannot be parsed as Number or is out of range (\""
+ value + "\")", nfe);
}
}
/**
* Sets the Value of the current metadata descriptor. <br>
* Using this method will change {@link #descriptorType}to
* {@link #TYPE_STRING}.
*
* @param value
* Value to set.
* @throws IllegalArgumentException
* If byte representation would take more than 65535 Bytes.
*/
// TODO Test
public void setStringValue(final String value)
throws IllegalArgumentException {
if (value == null) {
this.content = new byte[0];
} else {
final byte[] tmp = Utils.getBytes(value, AsfHeader.ASF_CHARSET);
if (getContainerType().isWithinValueRange(tmp.length)) {
// Everything is fine here, data can be stored.
this.content = tmp;
} else {
// Normally a size violation, check if JAudiotagger my truncate
// the string
if (TagOptionSingleton.getInstance()
.isTruncateTextWithoutErrors()) {
// truncate the string
final int copyBytes = (int) getContainerType()
.getMaximumDataLength().longValue();
this.content = new byte[copyBytes % 2 == 0 ? copyBytes
: copyBytes - 1];
System.arraycopy(tmp, 0, this.content, 0,
this.content.length);
} else {
// We may not truncate, so its an error
throw new IllegalArgumentException(
ErrorMessage.WMA_LENGTH_OF_DATA_IS_TOO_LARGE
.getMsg(tmp.length, getContainerType()
.getMaximumDataLength(),
getContainerType()
.getContainerGUID()
.getDescription()));
}
}
}
this.descriptorType = TYPE_STRING;
}
/**
* Sets the Value of the current metadata descriptor. <br>
* Using this method will change {@link #descriptorType}to
* {@link #TYPE_WORD}
*
* @param value
* Value to set.
* @throws IllegalArgumentException
* on negative values. ASF just supports unsigned values.
*/
public void setWordValue(final int value)
throws IllegalArgumentException {
if (value < 0 || value > WORD_MAXVALUE) {
throw new IllegalArgumentException("value out of range (0-"
+ WORD_MAXVALUE + ")");
}
this.content = Utils.getBytes(value, 2);
this.descriptorType = TYPE_WORD;
}
/**
* (overridden)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return getName()
+ " : "
+ new String[] { "String: ", "Binary: ", "Boolean: ",
"DWORD: ", "QWORD:", "WORD:", "GUID:" }[this.descriptorType]
+ getString() + " (language: " + this.languageIndex
+ " / stream: " + this.streamNumber + ")";
}
/**
* Writes this descriptor into the specified output stream.<br>
*
* @param out
* stream to write into.
* @param contType
* the container type this descriptor is written to.
* @return amount of bytes written.
* @throws IOException
* on I/O Errors
*/
public int writeInto(final OutputStream out,
final ContainerType contType) throws IOException {
final int size = getCurrentAsfSize(contType);
/*
* Booleans are stored as one byte, if a boolean is written, the data
* must be converted according to the container type.
*/
byte[] binaryData;
if (this.descriptorType == TYPE_BOOLEAN) {
binaryData = new byte[contType == ContainerType.EXTENDED_CONTENT ? 4
: 2];
binaryData[0] = (byte) (getBoolean() ? 1 : 0);
} else {
binaryData = this.content;
}
// for Metadata objects the stream number and language index
if (contType != ContainerType.EXTENDED_CONTENT) {
Utils.writeUINT16(getLanguageIndex(), out);
Utils.writeUINT16(getStreamNumber(), out);
}
Utils.writeUINT16(getName().length() * 2 + 2, out);
// The name for the metadata objects come later
if (contType == ContainerType.EXTENDED_CONTENT) {
out.write(Utils.getBytes(getName(), AsfHeader.ASF_CHARSET));
out.write(AsfHeader.ZERO_TERM);
}
// type and content len follow up are identical
final int type = getType();
Utils.writeUINT16(type, out);
int contentLen = binaryData.length;
if (TYPE_STRING == type) {
contentLen += 2; // Zero Term
}
if (contType == ContainerType.EXTENDED_CONTENT) {
Utils.writeUINT16(contentLen, out);
} else {
Utils.writeUINT32(contentLen, out);
}
// Metadata objects now write their descriptor name
if (contType != ContainerType.EXTENDED_CONTENT) {
out.write(Utils.getBytes(getName(), AsfHeader.ASF_CHARSET));
out.write(AsfHeader.ZERO_TERM);
}
// The content.
out.write(binaryData);
if (TYPE_STRING == type) {
out.write(AsfHeader.ZERO_TERM);
}
return size;
}
} |