package org.jaudiotagger.tag.datatype;
import org.jaudiotagger.tag.InvalidDataTypeException;
import org.jaudiotagger.tag.TagOptionSingleton;
import org.jaudiotagger.tag.id3.AbstractTagFrameBody;
import org.jaudiotagger.tag.id3.valuepair.TextEncoding;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Overrides in order to properly support the ID3v23 implemenation of TCON
*/
public class TCONString extends TextEncodedStringSizeTerminated
{
private boolean isNullSeperateMultipleValues = true;
/**
* Creates a new empty TextEncodedStringSizeTerminated datatype.
*
* @param identifier identifies the frame type
* @param frameBody
*/
public TCONString(String identifier, AbstractTagFrameBody frameBody)
{
super(identifier, frameBody);
}
/**
* Copy constructor
*
* @param object
*/
public TCONString(TCONString object)
{
super(object);
}
public boolean equals(Object obj)
{
if(this==obj)
{
return true;
}
return obj instanceof TCONString && super.equals(obj);
}
/**
* if this field is used with ID3v24 then it is usual to null separate values. Within ID3v23 not many
* frames officially support mutiple values, so in absense of better solution we use the v24 method, however
* some frames such as TCON have there own method and should not null separate values. This can be controlled
* by this field.
*/
public boolean isNullSeperateMultipleValues()
{
return isNullSeperateMultipleValues;
}
public void setNullSeperateMultipleValues(boolean nullSeperateMultipleValues)
{
isNullSeperateMultipleValues = nullSeperateMultipleValues;
}
/**
* Write String using specified encoding
*
* When this is called multiple times, all but the last value has a trailing null
*
* @param encoder
* @param next
* @param i
* @param noOfValues
* @return
* @throws java.nio.charset.CharacterCodingException
*/
private ByteBuffer writeString( CharsetEncoder encoder, String next, int i, int noOfValues)
throws CharacterCodingException
{
ByteBuffer bb;
if(( i + 1) == noOfValues )
{
bb = encoder.encode(CharBuffer.wrap(next));
}
else
{
if(isNullSeperateMultipleValues())
{
bb = encoder.encode(CharBuffer.wrap(next + '\0'));
}
else
{
bb = encoder.encode(CharBuffer.wrap(next));
}
}
bb.rewind();
return bb;
}
/**
* Write String in UTF-LEBOM format
*
* When this is called multiple times, all but the last value has a trailing null
*
* Remember we are using this charset because the charset that writes BOM does it the wrong way for us
* so we use this none and then manually add the BOM ourselves.
*
* @param next
* @param i
* @param noOfValues
* @return
* @throws CharacterCodingException
*/
private ByteBuffer writeStringUTF16LEBOM( String next, int i, int noOfValues)
throws CharacterCodingException
{
CharsetEncoder encoder = Charset.forName(TextEncoding.CHARSET_UTF_16_LE_ENCODING_FORMAT).newEncoder();
encoder.onMalformedInput(CodingErrorAction.IGNORE);
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
ByteBuffer bb = null;
//Note remember LE BOM is ff fe but this is handled by encoder Unicode char is fe ff
if(( i + 1)==noOfValues)
{
bb = encoder.encode(CharBuffer.wrap('\ufeff' + next ));
}
else
{
if(isNullSeperateMultipleValues())
{
bb = encoder.encode(CharBuffer.wrap('\ufeff' + next + '\0'));
}
else
{
bb = encoder.encode(CharBuffer.wrap('\ufeff' + next));
}
}
bb.rewind();
return bb;
}
/**
* Write String in UTF-BEBOM format
*
* When this is called multiple times, all but the last value has a trailing null
*
* @param next
* @param i
* @param noOfValues
* @return
* @throws CharacterCodingException
*/
private ByteBuffer writeStringUTF16BEBOM( String next, int i, int noOfValues)
throws CharacterCodingException
{
CharsetEncoder encoder = Charset.forName(TextEncoding.CHARSET_UTF_16_BE_ENCODING_FORMAT).newEncoder();
encoder.onMalformedInput(CodingErrorAction.IGNORE);
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
ByteBuffer bb = null;
//Add BOM
if(( i + 1)==noOfValues)
{
bb = encoder.encode(CharBuffer.wrap('\ufeff' + next ));
}
else
{
if(isNullSeperateMultipleValues())
{
bb = encoder.encode(CharBuffer.wrap('\ufeff' + next + '\0'));
}
else
{
bb = encoder.encode(CharBuffer.wrap('\ufeff' + next));
}
}
bb.rewind();
return bb;
}
/**
* Add an additional String to the current String value
*
* @param value
*/
public void addValue(String value)
{
if(isNullSeperateMultipleValues())
{
setValue(this.value + "\u0000" + value);
}
else
{
setValue(this.value + value);
}
}
/**
* How many values are held, each value is separated by a null terminator
*
* @return number of values held, usually this will be one.
*/
public int getNumberOfValues()
{
return getValues().size();
}
/**
* Get the nth value
*
* @param index
* @return the nth value
* @throws IndexOutOfBoundsException if value does not exist
*/
public String getValueAtIndex(int index)
{
//Split String into separate components
List values = getValues();
return (String) values.get(index);
}
public static List<String> splitV23(String value)
{
String[] valuesarray = value.replaceAll("(\\(\\d+\\)|\\(RX\\)|\\(CR\\)\\w*)", "$1\u0000").split("\u0000");
List<String> values = Arrays.asList(valuesarray);
//Read only list so if empty have to create new list
if (values.size() == 0)
{
values = new ArrayList<String>(1);
values.add("");
}
return values;
}
/**
*
* @return list of all values
*/
public List<String> getValues()
{
if(isNullSeperateMultipleValues())
{
return splitByNullSeperator((String) value);
}
else
{
return splitV23((String)value);
}
}
/**
* Get value(s) whilst removing any trailing nulls
*
* @return
*/
public String getValueWithoutTrailingNull()
{
List<String> values = getValues();
StringBuffer sb = new StringBuffer();
for(int i=0;i<values.size();i++)
{
if(i!=0)
{
sb.append("\u0000");
}
sb.append(values.get(i));
}
return sb.toString();
}
}