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 org.jaudiotagger.tag.options.PadNumberOption;
import org.jaudiotagger.utils.EqualsUtil;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents the form 01/10 whereby the second part is optional. This is used by frame such as TRCK and TPOS
*
* Some applications like to prepend the count with a zero to aid sorting, (i.e 02 comes before 10)
*
* If TagOptionSingleton.getInstance().isPadNumbers() is enabled then all fields will be written to file padded
* depending on the value of agOptionSingleton.getInstance().getPadNumberTotalLength(). Additionally fields returned
* from file will be returned as padded even if they are not currently stored as padded in the file.
*
* If TagOptionSingleton.getInstance().isPadNumbers() is disabled then count and track are written to file as they
* are provided, i.e if provided pre-padded they will be stored pre-padded, if not they will not. Values read from
* file will be returned as they are currently stored in file.
*
*
*/
@SuppressWarnings({"EmptyCatchBlock"})
public class PartOfSet extends AbstractString
{
/**
* Creates a new empty PartOfSet datatype.
*
* @param identifier identifies the frame type
* @param frameBody
*/
public PartOfSet(String identifier, AbstractTagFrameBody frameBody)
{
super(identifier, frameBody);
}
/**
* Copy constructor
*
* @param object
*/
public PartOfSet(PartOfSet object)
{
super(object);
}
public boolean equals(Object obj)
{
if (obj == this)
{
return true;
}
if (!(obj instanceof PartOfSet))
{
return false;
}
PartOfSet that = (PartOfSet) obj;
return EqualsUtil.areEqual(value, that.value);
}
/**
* Read a 'n' bytes from buffer into a String where n is the frameSize - offset
* so therefore cannot use this if there are other objects after it because it has no
* delimiter.
*
* Must take into account the text encoding defined in the Encoding Object
* ID3 Text Frames often allow multiple strings separated by the null char
* appropriate for the encoding.
*
* @param arr this is the buffer for the frame
* @param offset this is where to start reading in the buffer for this field
* @throws NullPointerException
* @throws IndexOutOfBoundsException
*/
public void readByteArray(byte[] arr, int offset) throws InvalidDataTypeException
{
logger.finest("Reading from array from offset:" + offset);
//Get the Specified Decoder
String charSetName = getTextEncodingCharSet();
CharsetDecoder decoder = Charset.forName(charSetName).newDecoder();
//Decode sliced inBuffer
ByteBuffer inBuffer = ByteBuffer.wrap(arr, offset, arr.length - offset).slice();
CharBuffer outBuffer = CharBuffer.allocate(arr.length - offset);
decoder.reset();
CoderResult coderResult = decoder.decode(inBuffer, outBuffer, true);
if (coderResult.isError())
{
logger.warning("Decoding error:" + coderResult.toString());
}
decoder.flush(outBuffer);
outBuffer.flip();
//Store value
String stringValue = outBuffer.toString();
value = new PartOfSetValue(stringValue);
//SetSize, important this is correct for finding the next datatype
setSize(arr.length - offset);
logger.config("Read SizeTerminatedString:" + value + " size:" + size);
}
/**
* Write String into byte array
*
* It will remove a trailing null terminator if exists if the option
* RemoveTrailingTerminatorOnWrite has been set.
*
* @return the data as a byte array in format to write to file
*/
public byte[] writeByteArray()
{
String value = getValue().toString();
byte[] data;
//Try and write to buffer using the CharSet defined by getTextEncodingCharSet()
try
{
if (TagOptionSingleton.getInstance().isRemoveTrailingTerminatorOnWrite())
{
if (value.length() > 0)
{
if (value.charAt(value.length() - 1) == '\0')
{
value = value.substring(0, value.length() - 1);
}
}
}
String charSetName = getTextEncodingCharSet();
if (charSetName.equals(TextEncoding.CHARSET_UTF_16))
{
charSetName = TextEncoding.CHARSET_UTF_16_LE_ENCODING_FORMAT;
CharsetEncoder encoder = Charset.forName(charSetName).newEncoder();
encoder.onMalformedInput(CodingErrorAction.IGNORE);
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
//Note remember LE BOM is ff fe but this is handled by encoder Unicode char is fe ff
ByteBuffer bb = encoder.encode(CharBuffer.wrap('\ufeff' + value));
data = new byte[bb.limit()];
bb.get(data, 0, bb.limit());
}
else
{
CharsetEncoder encoder = Charset.forName(charSetName).newEncoder();
ByteBuffer bb = encoder.encode(CharBuffer.wrap(value));
encoder.onMalformedInput(CodingErrorAction.IGNORE);
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
data = new byte[bb.limit()];
bb.get(data, 0, bb.limit());
}
}
//Should never happen so if does throw a RuntimeException
catch (CharacterCodingException ce)
{
logger.severe(ce.getMessage());
throw new RuntimeException(ce);
}
setSize(data.length);
return data;
}
/**
* Get the text encoding being used.
*
* The text encoding is defined by the frame body that the text field belongs to.
*
* @return the text encoding charset
*/
protected String getTextEncodingCharSet()
{
byte textEncoding = this.getBody().getTextEncoding();
String charSetName = TextEncoding.getInstanceOf().getValueForId(textEncoding);
logger.finest("text encoding:" + textEncoding + " charset:" + charSetName);
return charSetName;
}
/**
* Holds data
*/
public static class PartOfSetValue
{
private static final Pattern trackNoPatternWithTotalCount;
private static final Pattern trackNoPattern;
static
{
//Match track/total pattern allowing for extraneous nulls ecetera at the end
trackNoPatternWithTotalCount = Pattern.compile("([0-9]+)/([0-9]+)(.*)", Pattern.CASE_INSENSITIVE);
trackNoPattern = Pattern.compile("([0-9]+)(.*)", Pattern.CASE_INSENSITIVE);
}
private static final String SEPARATOR = "/";
private Integer count;
private Integer total;
private String extra; //Any extraneous info such as null chars
private String rawText; // raw text representation used to actually save the data IF !TagOptionSingleton.getInstance().isPadNumbers()
private String rawCount; //count value as provided
private String rawTotal; //total value as provided
public PartOfSetValue()
{
rawText = "";
}
/**
* When constructing from data
*
* @param value
*/
public PartOfSetValue(String value)
{
this.rawText = value;
initFromValue(value);
}
/**
* Newly created
*
* @param count
* @param total
*/
public PartOfSetValue(Integer count, Integer total)
{
this.count = count;
this.rawCount = count.toString();
this.total = total;
this.rawTotal = total.toString();
resetValueFromCounts();
}
/**
* Given a raw value that could contain both a count and total and extra stuff (but needdnt contain
* anything tries to parse it)
*
* @param value
*/
private void initFromValue(String value)
{
try
{
Matcher m = trackNoPatternWithTotalCount.matcher(value);
if (m.matches())
{
this.extra = m.group(3);
this.count = Integer.parseInt(m.group(1));
this.rawCount=m.group(1);
this.total = Integer.parseInt(m.group(2));
this.rawTotal=m.group(2);
return;
}
m = trackNoPattern.matcher(value);
if (m.matches())
{
this.extra = m.group(2);
this.count = Integer.parseInt(m.group(1));
this.rawCount = m.group(1);
}
}
catch (NumberFormatException nfe)
{
//#JAUDIOTAGGER-366 Could occur if actually value is a long not an int
this.count = 0;
}
}
private void resetValueFromCounts()
{
StringBuffer sb = new StringBuffer();
if(rawCount!=null)
{
sb.append(rawCount);
}
else
{
sb.append("0");
}
if(rawTotal!=null)
{
sb.append(SEPARATOR + rawTotal);
}
if(extra!=null)
{
sb.append(extra);
}
this.rawText = sb.toString();
}
public Integer getCount()
{
return count;
}
public Integer getTotal()
{
return total;
}
public void setCount(Integer count)
{
this.count = count;
this.rawCount = count.toString();
resetValueFromCounts();
}
public void setTotal(Integer total)
{
this.total = total;
this.rawTotal = total.toString();
resetValueFromCounts();
}
public void setCount(String count)
{
try
{
this.count = Integer.parseInt(count);
this.rawCount = count;
resetValueFromCounts();
}
catch (NumberFormatException nfe)
{
}
}
public void setTotal(String total)
{
try
{
this.total = Integer.parseInt(total);
this.rawTotal = total;
resetValueFromCounts();
}
catch (NumberFormatException nfe)
{
}
}
public String getRawValue()
{
return rawText;
}
public void setRawValue(String value)
{
this.rawText = value;
initFromValue(value);
}
/**
* Get Count including padded if padding is enabled
*
* @return
*/
public String getCountAsText()
{
//Don'timer Pad
StringBuffer sb = new StringBuffer();
if (!TagOptionSingleton.getInstance().isPadNumbers())
{
return rawCount;
}
else
{
padNumber(sb, count, TagOptionSingleton.getInstance().getPadNumberTotalLength());
}
return sb.toString();
}
/**
* Pad number so number is defined as long as length
*
* @param sb
* @param count
* @param padNumberLength
*/
private void padNumber(StringBuffer sb, Integer count,PadNumberOption padNumberLength)
{
if (count != null)
{
if(padNumberLength==PadNumberOption.PAD_ONE_ZERO)
{
if (count > 0 && count < 10)
{
sb.append("0").append(count);
}
else
{
sb.append(count.intValue());
}
}
else if(padNumberLength==PadNumberOption.PAD_TWO_ZERO)
{
if (count > 0 && count < 10)
{
sb.append("00").append(count);
}
else if (count > 9 && count < 100)
{
sb.append("0").append(count);
}
else
{
sb.append(count.intValue());
}
}
else if(padNumberLength==PadNumberOption.PAD_THREE_ZERO)
{
if (count > 0 && count < 10)
{
sb.append("000").append(count);
}
else if (count > 9 && count < 100)
{
sb.append("00").append(count);
}
else if (count > 99 && count < 1000)
{
sb.append("0").append(count);
}
else
{
sb.append(count.intValue());
}
}
}
}
/**
* Get Total padded
*
* @return
*/
public String getTotalAsText()
{
//Don'timer Pad
StringBuffer sb = new StringBuffer();
if (!TagOptionSingleton.getInstance().isPadNumbers())
{
return rawTotal;
}
else
{
padNumber(sb, total, TagOptionSingleton.getInstance().getPadNumberTotalLength());
}
return sb.toString();
}
public String toString()
{
//Don'timer Pad
StringBuffer sb = new StringBuffer();
if (!TagOptionSingleton.getInstance().isPadNumbers())
{
return rawText;
}
else
{
if (count != null)
{
padNumber(sb, count, TagOptionSingleton.getInstance().getPadNumberTotalLength());
}
else if (total != null)
{
padNumber(sb, 0, TagOptionSingleton.getInstance().getPadNumberTotalLength());
}
if (total != null)
{
sb.append(SEPARATOR);
padNumber(sb, total, TagOptionSingleton.getInstance().getPadNumberTotalLength());
}
if (extra != null)
{
sb.append(extra);
}
}
return sb.toString();
}
public boolean equals(Object obj)
{
if (obj == this)
{
return true;
}
if (!(obj instanceof PartOfSetValue))
{
return false;
}
PartOfSetValue that = (PartOfSetValue) obj;
return EqualsUtil.areEqual(getCount(), that.getCount())
&& EqualsUtil.areEqual(getTotal(), that.getTotal());
}
}
public PartOfSetValue getValue()
{
return (PartOfSetValue) value;
}
public String toString()
{
return value.toString();
}
}