/*
* OSCPacketCodec.java
* de.sciss.net (NetUtil)
*
* Copyright (c) 2004-2009 Hanns Holger Rutz. All rights reserved.
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*
*
* Changelog:
* 28-Apr-07 created
*/
package de.sciss.net;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
/**
* A packet codec defines how the translation between Java objects
* and OSC atoms is accomplished. For example, by default, when
* an OSC message is assembled for transmission, the encoder will
* translate a<code>java.lang.Integer</code> argument into
* a four byte integer with typetag <code>'i'</code>. Or when
* a received message is being decoded, finding an atom typetagged
* <code>'f'</code>, the decoder will create a <code>java.lang.Float</code>
* out of it.
* <p>
* This example sounds trivial, but the codec is also able to handle
* type conversions. For instance, in the strict OSC 1.0 specification,
* only 32bit numeric atoms are defined (<code>'i'</code> and <code>'f'</code>).
* A codec with mode <code>MODE_STRICT_V1</code> will reject a
* <code>java.lang.Double</code> in the encoding process and not be
* able to decode a typetag <code>'d'</code>. A codec with mode
* <code>MODE_MODEST</code> automatically breaks down everything the 32bit,
* so a <code>java.lang.Double</code> gets encoded as 32bit <code>'f'</code>
* and a received atom tagged <code>'d'</code> becomes a
* <code>java.lang.Float</code>. Other configurations exist.
* <p>
* Another important function of the codec is to specify the charset encoding
* of strings, something that was overseen in the OSC 1.0 spec. By default,
* <code>UTF-8</code> is used so all special characters can be safely encoded.
* <p>
* Last but not least, using the <code>putDecoder</code> and <code>putEncoder</code>
* methods, the codec can be extended to support additional Java classes or
* OSC typetags, without the need to subclass <code>OSCPacketCodec</code>.
*
* @author Hanns Holger Rutz
* @version 0.36, 18-Feb-09
*
* @since NetUtil 0.35
*/
public class OSCPacketCodec
{
private static final OSCPacketCodec defaultCodec = new OSCPacketCodec();
public static final int MODE_READ_DOUBLE = 0x0001;
public static final int MODE_READ_DOUBLE_AS_FLOAT = 0x0002;
private static final int MODE_READ_DOUBLE_MASK = 0x0003;
public static final int MODE_READ_LONG = 0x0004;
public static final int MODE_READ_LONG_AS_INTEGER = 0x0008;
private static final int MODE_READ_LONG_MASK = 0x000C;
public static final int MODE_WRITE_DOUBLE = 0x0010;
public static final int MODE_WRITE_DOUBLE_AS_FLOAT = 0x0020;
private static final int MODE_WRITE_DOUBLE_MASK = 0x0030;
public static final int MODE_WRITE_LONG = 0x0040;
public static final int MODE_WRITE_LONG_AS_INTEGER = 0x0080;
private static final int MODE_WRITE_LONG_MASK = 0x00C0;
public static final int MODE_READ_SYMBOL_AS_STRING = 0x0100;
public static final int MODE_WRITE_PACKET_AS_BLOB = 0x0200;
/**
* Support mode: coder only accepts <code>java.lang.Integer</code>,
* <code>java.lang.Float</code>, <code>java.lang.String</code>,
* and <code>byte[]</code>.
* Decoder only accepts <code>'i'</code>, <code>'f'</code>,
* <code>'s'</code>, and <code>'b'</code>. Note that <code>byte[]</code>
* is used to represents blobs (<code>'b'</code>).
*/
public static final int MODE_STRICT_V1 = 0x0000;
/**
* Support mode: like <code>MODE_STRICT_V1</code>, but coder additionally
* encodes <code>java.lang.Long</code> as a <code>'i'</code>,
* <code>java.lang.Double</code> as a <code>'f'</code>, and
* <code>de.sciss.net.OSCPacket</code> as a blob <code>'b'</code>.
* The decoder decodes <code>'h'</code> into <code>java.lang.Integer</code>,
* <code>'d'</code> into <code>java.lang.Float</code>, and
* <code>'S'</code> (Symbol) into <code>java.lang.String</code>.
*/
public static final int MODE_MODEST = MODE_READ_DOUBLE_AS_FLOAT | MODE_READ_LONG_AS_INTEGER | MODE_WRITE_DOUBLE_AS_FLOAT | MODE_WRITE_LONG_AS_INTEGER | MODE_READ_SYMBOL_AS_STRING | MODE_WRITE_PACKET_AS_BLOB;
/**
* Support mode: like <code>MODE_MODEST</code>, that is, it will
* downgrade to 32bit in the encoding process, but decoding leaves
* 64bit values intact, so <code>'h'</code> becomes <code>java.lang.Long</code>,
* and <code>'d'</code> into <code>java.lang.Double</code>.
*/
public static final int MODE_GRACEFUL = MODE_READ_DOUBLE | MODE_READ_LONG | MODE_WRITE_DOUBLE_AS_FLOAT | MODE_WRITE_LONG_AS_INTEGER | MODE_READ_SYMBOL_AS_STRING | MODE_WRITE_PACKET_AS_BLOB;
/**
* Support mode: like <code>MODE_STRICT_V1</code>, but with additional
* 64bit support, that is a mutual mapping between
* <code>'h'</code> <--> <code>java.lang.Long</code>, and
* <code>'d'</code> <--> <code>java.lang.Double</code>.
* Also, <code>'S'</code> (Symbol) is decoded into <code>java.lang.String</code>,
* and <code>de.sciss.net.OSCPacket</code> is encoded as a blob <code>'b'</code>.
*/
public static final int MODE_FAT_V1 = MODE_READ_DOUBLE | MODE_READ_LONG | MODE_WRITE_DOUBLE | MODE_WRITE_LONG | MODE_READ_SYMBOL_AS_STRING | MODE_WRITE_PACKET_AS_BLOB;
private final Atom[] atomDecoders = new Atom[ 128 ];
// private final Map atomEncoders = new HashMap();
private final Class[] atomEncoderC = new Class[ 128 ];
private final Atom[] atomEncoderA = new Atom[ 128 ];
protected String charsetName;
private static final byte[] bndlIdentifier = { 0x23, 0x62, 0x75, 0x6E, 0x64, 0x6C, 0x65, 0x00 }; // "#bundle" (4-aligned)
private static final byte[] pad = new byte[ 4 ];
/**
* Creates a new codec with <code>MODE_GRACEFUL</code> and
* <code>UTF-8</code> encoding. Note that since a codec
* and be shared between <code>OSCServer</code> or
* <code>OSCClient</code> instances, usually you will just
* want to call <code>getDefaultCodec</code>!
*
* @see #MODE_GRACEFUL
* @see #getDefaultCodec()
*/
public OSCPacketCodec()
{
this( MODE_GRACEFUL );
}
/**
* Creates a new codec with a given support mode and
* <code>UTF-8</code> encoding.
*
* @param mode the support mode flag field to use
*/
public OSCPacketCodec( int mode )
{
this( mode, "UTF-8" );
}
/**
* Creates a new codec with a given support mode and
* a given charset for string encoding.
*
* @param mode the support mode flag mask to use
* @param charset the name of the charset to use for
* string coding and decoding, like
* <code>"UTF-8"</code>,
* <code>"ISO-8859-1"</code> etc.
* @see java.nio.charset.Charset
*/
public OSCPacketCodec( int mode, String charset )
{
Atom a;
int encIdx = 0;
// OSC version 1.0 strict type tag support
a = new IntegerAtom();
atomDecoders[ a.getTypeTag( null )] = a;
// atomEncoders.put( Integer.class, a );
atomEncoderC[ encIdx ] = Integer.class;
atomEncoderA[ encIdx++ ] = a;
a = new FloatAtom();
atomDecoders[ a.getTypeTag( null )] = a;
// atomEncoders.put( Float.class, a );
atomEncoderC[ encIdx ] = Float.class;
atomEncoderA[ encIdx++ ] = a;
a = new StringAtom();
atomDecoders[ a.getTypeTag( null )] = a;
// atomEncoders.put( String.class, a );
atomEncoderC[ encIdx ] = String.class;
atomEncoderA[ encIdx++ ] = a;
a = new BlobAtom();
atomDecoders[ a.getTypeTag( null )] = a;
// atomEncoders.put( byte[].class, a );
atomEncoderC[ encIdx ] = byte[].class;
atomEncoderA[ encIdx++ ] = a;
setStringCharsetCodec( charset );
setSupportMode( mode );
}
/**
* Queries the standard codec which is used in all
* implicit client and server creations. This codec adheres
* to the <code>MODE_GRACEFUL</code> scheme and uses
* <code>UTF-8</code> string encoding.
* <p>
* Note that although it is not recommended, it is
* possible to modify the returned codec. That means that
* upon your application launch, you could query the default
* codec and switch its behaviour, e.g. change the string
* charset, so all successive operations with the default
* codec will be subject to those customizations.
*
* @return the default codec
* @see #MODE_GRACEFUL
*/
public static OSCPacketCodec getDefaultCodec()
{
return defaultCodec;
}
/**
* Specifies the charset to use in string coding and decoding.
*
* @param charsetName the name of the charset, e.g.
* <code>"UTF-8"</code>,
* <code>"ISO-8859-1"</code> etc.
* @see java.nio.charset.Charset
*/
public void setStringCharsetCodec( String charsetName )
{
this.charsetName = charsetName;
}
/**
* Registers an atomic decoder with the packet codec. This
* decoder is called whenever an OSC message with the
* given typetag is encountered.
*
* @param typeTag the typetag which is to be decoded with the
* new <code>Atom</code>. <code>typeTag</code>
* must be in the ASCII value range 0 to 127.
* @param a the decoder to use
*
* @see OSCPacketCodec.Atom
*/
public void putDecoder( byte typeTag, Atom a )
{
atomDecoders[ typeTag ] = a;
}
/**
* Registers an atomic encoder with the packet codec. This
* encoder is called whenever an OSC message to be assembled
* contains an argument of the given Java class.
*
* @param javaClass the class for which the encoder is responsible
* @param a the encoder to use
*
* @see OSCPacketCodec.Atom
*/
public void putEncoder( Class javaClass, Atom a )
{
int encIdx = 0;
// atomEncoders.put( javaClass, a );
while( (atomEncoderC[ encIdx ] != javaClass) && (atomEncoderC[ encIdx ] != null) ) encIdx++;
if( a != null ) {
atomEncoderC[ encIdx ] = javaClass;
atomEncoderA[ encIdx ] = a;
} else if( atomEncoderC[ encIdx ] != null ) {
int encIdx2;
for( encIdx2 = encIdx + 1; atomEncoderC[ encIdx2 ] != null; encIdx2 ++ ) ;
System.arraycopy( atomEncoderC, encIdx + 1, atomEncoderC, encIdx, encIdx2 - encIdx );
System.arraycopy( atomEncoderA, encIdx + 1, atomEncoderA, encIdx, encIdx2 - encIdx );
}
}
/**
* Adjusts the support mode for type tag handling. Usually
* you specify the mode directly in the instantiation of
* <code>OSCPacketCodec</code>, but you can change it later
* using this method.
*
* @param mode the new mode to use. A flag field combination
* of <code>MODE_READ_DOUBLE</code> or
* <code>MODE_READ_DOUBLE_AS_FLOAT</code> etc.,
* or a ready made combination such as
* <code>MODE_FAT_V1</code>.
*
* @see #OSCPacketCodec( int )
*/
public void setSupportMode( int mode )
{
Atom a;
switch( mode & MODE_READ_DOUBLE_MASK ) {
case MODE_STRICT_V1:
atomDecoders[ 0x64 ] = null; // 'd' double
break;
case MODE_READ_DOUBLE:
atomDecoders[ 0x64 ] = new DoubleAtom();
break;
case MODE_READ_DOUBLE_AS_FLOAT:
atomDecoders[ 0x64 ] = new DoubleAsFloatAtom();
break;
default:
throw new IllegalArgumentException( String.valueOf( mode ));
}
switch( mode & MODE_READ_LONG_MASK ) {
case MODE_STRICT_V1:
atomDecoders[ 0x68 ] = null; // 'h' long
break;
case MODE_READ_LONG:
atomDecoders[ 0x68 ] = new LongAtom();
break;
case MODE_READ_LONG_AS_INTEGER:
atomDecoders[ 0x68 ] = new LongAsIntegerAtom();
break;
default:
throw new IllegalArgumentException( String.valueOf( mode ));
}
switch( mode & MODE_WRITE_DOUBLE_MASK ) {
case MODE_STRICT_V1:
// atomEncoders.remove( Double.class );
putEncoder( Double.class, null );
break;
case MODE_WRITE_DOUBLE:
// atomEncoders.put( Double.class, new DoubleAtom() );
putEncoder( Double.class, new DoubleAtom() );
break;
case MODE_WRITE_DOUBLE_AS_FLOAT:
// atomEncoders.put( Double.class, new DoubleAsFloatAtom() );
putEncoder( Double.class, new DoubleAsFloatAtom() );
break;
default:
throw new IllegalArgumentException( String.valueOf( mode ));
}
switch( mode & MODE_WRITE_LONG_MASK ) {
case MODE_STRICT_V1:
// atomEncoders.remove( Long.class );
putEncoder( Long.class, null );
break;
case MODE_WRITE_LONG:
// atomEncoders.put( Long.class, new LongAtom() );
putEncoder( Long.class, new LongAtom() );
break;
case MODE_WRITE_LONG_AS_INTEGER:
// atomEncoders.put( Long.class, new LongAsIntegerAtom() );
putEncoder( Long.class, new LongAsIntegerAtom() );
break;
default:
throw new IllegalArgumentException( String.valueOf( mode ));
}
if( (mode & MODE_READ_SYMBOL_AS_STRING) != 0 ) {
atomDecoders[ 0x53 ] = new StringAtom(); // 'S' symbol
} else {
atomDecoders[ 0x53 ] = null;
}
if( (mode & MODE_WRITE_PACKET_AS_BLOB) != 0 ) {
a = new PacketAtom();
// atomEncoders.put( OSCBundle.class, a );
// atomEncoders.put( OSCMessage.class, a );
putEncoder( OSCBundle.class, a );
putEncoder( OSCMessage.class, a );
} else {
// atomEncoders.remove( OSCBundle.class );
// atomEncoders.remove( OSCMessage.class );
putEncoder( OSCBundle.class, null );
putEncoder( OSCMessage.class, null );
}
}
/**
* Creates a new packet decoded
* from the ByteBuffer. This method tries
* to read a null terminated string at the
* beginning of the provided buffer. If it
* equals the bundle identifier, the
* <code>decode</code> of <code>OSCBundle</code>
* is called (which may recursively decode
* nested bundles), otherwise the one from
* <code>OSCMessage</code>.
*
* @param b <code>ByteBuffer</code> pointing right at
* the beginning of the packet. the buffer's
* limited should be set appropriately to
* allow the complete packet to be read. when
* the method returns, the buffer's position
* is right after the end of the packet.
*
* @return new decoded OSC packet
*
* @throws IOException in case some of the
* reading or decoding procedures failed.
* @throws BufferUnderflowException in case of a parsing
* error that causes the
* method to read past the buffer limit
* @throws IllegalArgumentException occurs in some cases of buffer underflow
*/
public OSCPacket decode( ByteBuffer b )
throws IOException
{
final String command = readString( b );
skipToAlign( b );
if( command.equals( OSCBundle.TAG )) {
return decodeBundle( b );
} else {
return decodeMessage( command, b );
}
}
/**
* Encodes the contents of this packet
* into the provided <code>ByteBuffer</code>,
* beginning at the buffer's current position. To write the
* encoded packet, you will typically call <code>flip()</code>
* on the buffer, then <code>write()</code> on the channel.
*
* @param b <code>ByteBuffer</code> pointing right at
* the beginning of the osc packet.
* buffer position will be right after the end
* of the packet when the method returns.
*
* @throws IOException in case some of the
* writing procedures failed.
*/
public void encode( OSCPacket p, ByteBuffer b )
throws IOException
{
if( p instanceof OSCBundle ) {
encodeBundle( (OSCBundle) p, b );
} else {
encodeMessage( (OSCMessage) p, b );
}
}
/**
* Calculates and returns
* the packet's size in bytes
*
* @return the size of the packet in bytes, including the initial
* osc command and aligned to 4-byte boundary. this
* is the amount of bytes written by the <code>encode</code>
* method.
*
* @throws IOException if an error occurs during the calculation
*/
public int getSize( OSCPacket p )
throws IOException
{
if( p instanceof OSCBundle ) {
return getBundleSize( (OSCBundle) p );
} else {
return getMessageSize( (OSCMessage) p );
}
}
protected int getBundleSize( OSCBundle bndl )
throws IOException
{
synchronized( bndl.collPackets ) {
int result = bndlIdentifier.length + 8 + (bndl.collPackets.size() << 2); // name, timetag, size of each bundle element
for( int i = 0; i < bndl.collPackets.size(); i++ ) {
result += getSize( ((OSCPacket) bndl.collPackets.get( i )));
}
return result;
}
}
/**
* Calculates the byte size of the encoded message
*
* @return the size of the OSC message in bytes
*
* @throws IOException if the message contains invalid arguments
*/
protected int getMessageSize( OSCMessage msg )
throws IOException
{
final int numArgs = msg.getArgCount();
int result = ((msg.getName().length() + 4) & ~3) + ((1+numArgs + 4) & ~3);
Object o;
Class cl;
// Class oldCl = null;
// Atom a = null;
int j;
for( int i = 0; i < numArgs; i++ ) {
o = msg.getArg( i );
cl = o.getClass();
j = 0;
try {
while( atomEncoderC[ j ] != cl ) j++;
// a = (Atom) atomEncoders.get( cl );
// result += a.getAtomSize( o );
result += atomEncoderA[ j ].getAtomSize( o );
}
catch( NullPointerException e1 ) {
throw new OSCException( OSCException.JAVACLASS, cl.getName() );
}
}
return result;
}
protected OSCBundle decodeBundle( ByteBuffer b )
throws IOException
{
final OSCBundle bndl = new OSCBundle();
final int totalLimit = b.limit();
bndl.setTimeTagRaw( b.getLong() );
try {
while( b.hasRemaining() ) {
b.limit( b.getInt() + b.position() ); // msg size
// bndl.addPacket( OSCPacket.decode( b, m ));
bndl.addPacket( decode( b ));
b.limit( totalLimit );
}
return bndl;
}
catch( IllegalArgumentException e1 ) { // throws by b.limit if bundle size is corrupted
throw new OSCException( OSCException.FORMAT, e1.getLocalizedMessage() );
}
}
/**
* Creates a new message with arguments decoded
* from the ByteBuffer. Usually you call
* <code>decode</code> from the <code>OSCPacket</code>
* superclass which will invoke this method of
* it finds an OSC message.
*
* @param b ByteBuffer pointing right at
* the beginning of the type
* declaration section of the
* OSC message, i.e. the name
* was skipped before.
*
* @return new OSC message representing
* the received message described
* by the ByteBuffer.
*
* @throws IOException in case some of the
* reading or decoding procedures failed.
* @throws BufferUnderflowException in case of a parsing
* error that causes the
* method to read past the buffer limit
* @throws IllegalArgumentException occurs in some cases of buffer underflow
*/
protected OSCMessage decodeMessage( String command, ByteBuffer b )
throws IOException
{
final Object[] args;
final int numArgs;
final ByteBuffer b2;
final int pos1;
// int pos1, pos2;
byte typ = 0;
if( b.get() != 0x2C ) throw new OSCException( OSCException.FORMAT, null );
b2 = b.slice(); // faster to slice than to reposition all the time!
pos1 = b.position();
// b2.position( pos1 );
// pos1 = b.position();
// OSCPacket.skipToValues( b );
while( b.get() != 0x00 ) ;
numArgs = b.position() - pos1 - 1;
args = new Object[ numArgs ];
skipToAlign( b );
// pos2 = (b.position() + 3) & ~3;
try {
for( int argIdx = 0; argIdx < numArgs; argIdx++ ) {
typ = b2.get();
// if( typ == 0 ) break;
args[ argIdx ] = atomDecoders[ typ ].decodeAtom( typ, b );
}
} catch (NullPointerException e1 ) {
throw new OSCException( OSCException.TYPETAG, String.valueOf( (char) typ ));
}
//System.err.println( "done. numArgs = "+args.length );
return new OSCMessage( command, args );
}
protected void encodeBundle( OSCBundle bndl, ByteBuffer b )
throws IOException
{
int pos1, pos2;
b.put( bndlIdentifier ).putLong( bndl.getTimeTag() );
synchronized( bndl.collPackets ) {
for( int i = 0; i < bndl.collPackets.size(); i++ ) {
b.mark();
b.putInt( 0 ); // calculate size later
pos1 = b.position();
encode( (OSCPacket) bndl.collPackets.get( i ), b );
pos2 = b.position();
b.reset();
b.putInt( pos2 - pos1 ).position( pos2 );
}
}
}
/**
* Encodes the message onto the given <code>ByteBuffer</code>,
* beginning at the buffer's current position. To write the
* encoded message, you will typically call <code>flip()</code>
* on the buffer, then <code>write()</code> on the channel.
*
* @param b <code>ByteBuffer</code> pointing right at
* the beginning of the osc packet.
* buffer position will be right after the end
* of the message when the method returns.
*
* @throws IOException in case some of the
* writing procedures failed
* (buffer overflow, illegal arguments).
*/
protected void encodeMessage( OSCMessage msg, ByteBuffer b )
throws BufferOverflowException, IOException
{
final int numArgs = msg.getArgCount(); // args.length;
final ByteBuffer b2;
// int pos1, pos2;
int j;
Object o = null;
Class cl = null;
// Class oldCl = null;
Atom a = null;
b.put( msg.getName().getBytes() );
terminateAndPadToAlign( b );
// it's important to slice at a 4-byte boundary because
// the position will become 0 and terminateAndPadToAlign
// will be malfunctioning otherwise
b2 = b.slice();
b2.put( (byte) 0x2C ); // ',' to announce type string
b.position( b.position() + ((numArgs + 5) & ~3) ); // comma + numArgs + zero + align
try {
for( int i = 0; i < numArgs; i++ ) {
o = msg.getArg( i );
cl = o.getClass();
j = 0;
while( atomEncoderC[ j ] != cl ) j++;
a = atomEncoderA[ j ];
a.encodeAtom( o, b2, b );
}
}
catch( NullPointerException e1 ) {
throw new OSCException( OSCException.JAVACLASS, o == null ? "null" : cl.getName() );
}
terminateAndPadToAlign( b2 );
}
/**
* Reads a null terminated string from
* the current buffer position
*
* @param b buffer to read from. position and limit must be
* set appropriately. new position will be right after
* the terminating zero byte when the method returns
*
* @throws BufferUnderflowException in case the string exceeds
* the provided buffer limit
*/
public static String readString( ByteBuffer b )
{
final int pos = b.position();
final byte[] bytes;
// int len = 1;
while( b.get() != 0 ) ; // len++;
final int len = b.position() - pos;
bytes = new byte[ len ];
b.position( pos );
b.get( bytes );
return new String( bytes, 0, len - 1 );
}
/**
* Adds as many zero padding bytes as necessary to
* stop on a 4 byte alignment. if the buffer position
* is already on a 4 byte alignment when calling this
* function, another 4 zero padding bytes are added.
* buffer position will be on the new aligned boundary
* when return from this method
*
* @param b the buffer to pad
*
* @throws BufferOverflowException in case the padding exceeds
* the provided buffer limit
*/
public static void terminateAndPadToAlign( ByteBuffer b )
{
b.put( pad, 0, 4 - (b.position() & 0x03) );
}
/**
* Adds as many zero padding bytes as necessary to
* stop on a 4 byte alignment. if the buffer position
* is already on a 4 byte alignment when calling this
* function, this method does nothing.
*
* @param b the buffer to align
*
* @throws BufferOverflowException in case the padding exceeds
* the provided buffer limit
*/
public static void padToAlign( ByteBuffer b )
{
b.put( pad, 0, -b.position() & 0x03 ); // nearest 4-align
}
/**
* Advances in the buffer as long there
* are non-zero bytes, then advance to a
* four byte alignment.
*
* @param b the buffer to advance
*
* @throws BufferUnderflowException in case the reads exceed
* the provided buffer limit
* @throws IllegalArgumentException in case the skipping exceeds
* the provided buffer limit
*/
public static void skipToValues( ByteBuffer b )
throws BufferUnderflowException
{
while( b.get() != 0x00 ) ;
b.position( (b.position() + 3) & ~3 );
}
/**
* Advances the current buffer position
* to an integer of four bytes. The position
* is not altered if it is already
* aligned to a four byte boundary.
*
* @param b the buffer to advance
*
* @throws IllegalArgumentException in case the skipping exceeds
* the provided buffer limit
*/
public static void skipToAlign( ByteBuffer b )
{
b.position( (b.position() + 3) & ~3 );
}
// abstract class is a bit faster than interface!
// public static interface Atom
/**
* The <code>Atom</code> class represents a combination of
* an encoder and decoder of a Java respectively OSC atom.
* While typically an <code>Atom</code> does a one-to-one
* mapping between a single Java class and a single OSC
* type tag, other mappings are possible, such as different
* type tags for the same Java class, or decoding the
* same typetag into different Java classes.
* <p>
* An example of the <B>first case</B> would be a colour atom:
* The <code>decodeAtom</code> method would require a
* <code>'r'</code> typetag and return a <code>java.awt.Color</code>
* object, with a body similar to this:
* <PRE>
* return new java.awt.Color( b.getInt(), true );
* </PRE>
* The <code>encodeAtom</code> and <code>getTypeTag</code>
* methods would require its argument to be a <code>java.awt.Color</code>,
* <code>getTypeTag</code> would return <code>'r'</code>,
* <code>getAtomSize</code> would return <code>4</code>, and
* <code>encodeAtom</code> would do something like
* <pre>
* tb.put( (byte) 'r' );
* db.putInt( ((java.awt.Color) o).getRGB() );
* </pre>
* <p>
* And example of the <B>latter case</B> (one-to-many mapping) would be
* a codec for the <code>'T'</code> ("true") and <code>'F'</code>
* ("false") typetags. This codec would be registered once
* as an encoder, using <code>putEncoder( Boolean.class, myAtomCodec )</code>,
* and twice as a decoder, using <code>putDecoder( (byte) 'F', myAtomCodec )</code>
* and <code>putDecoder( (byte) 'T', myAtomCodec )</code>. The codec's
* <code>getAtomSize</code> method would return <code>0</code>, <code>getTypeTag</code>
* would return
* <pre>
* ((Boolean) o).booleanValue() ? (byte) 'T' : (byte) 'F'
* </pre>
* <code>decodeAtom</code> would return
* <pre>
* Boolean.valueOf( typeTag == (byte) 'T' )
* </pre>
* and finally <code>encodeAtom</code> would be:
* <pre>
* tb.put( this.getTypeTag( o ));
* </pre>
*
* @see OSCPacketCodec#putDecoder( byte, de.sciss.net.OSCPacketCodec.Atom )
* @see OSCPacketCodec#putEncoder( java.lang.Class, de.sciss.net.OSCPacketCodec.Atom )
*/
public static abstract class Atom
{
public abstract Object decodeAtom( byte typeTag, ByteBuffer b ) throws IOException;
public abstract void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db ) throws IOException;
public abstract byte getTypeTag( Object o );
public abstract int getAtomSize( Object o ) throws IOException;
}
private class IntegerAtom
// implements Atom
extends Atom
{
protected IntegerAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
// requires Java 1.5+
// return Integer.valueOf( b.getInt() );
return new Integer( b.getInt() );
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x69 ); // 'i'
db.putInt( ((Integer) o).intValue() );
}
public byte getTypeTag( Object o )
{
return 0x69; // 'i'
}
public int getAtomSize( Object o )
throws IOException
{
return 4;
}
}
private class FloatAtom
// implements Atom
extends Atom
{
protected FloatAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
// requires Java 1.5+
// return Float.valueOf( b.getFloat() );
return new Float( b.getFloat() );
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x66 ); // 'f'
db.putFloat( ((Float) o).floatValue() );
}
public byte getTypeTag( Object o )
{
return 0x66; // 'f'
}
public int getAtomSize( Object o )
throws IOException
{
return 4;
}
}
private class LongAtom
// implements Atom
extends Atom
{
protected LongAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
// requires Java 1.5+
// return Long.valueOf( b.getLong() );
return new Long( b.getLong() );
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x68 ); // 'h'
db.putLong( ((Long) o).longValue() );
}
public byte getTypeTag( Object o )
{
return 0x68; // 'h'
}
public int getAtomSize( Object o )
throws IOException
{
return 8;
}
}
private class DoubleAtom
// implements Atom
extends Atom
{
protected DoubleAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
// requires Java 1.5+
// return Double.valueOf( b.getDouble() );
return new Double( b.getDouble() );
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x64 ); // 'd'
db.putDouble( ((Double) o).doubleValue() );
}
public byte getTypeTag( Object o )
{
return 0x64; // 'd'
}
public int getAtomSize( Object o )
throws IOException
{
return 8;
}
}
private class DoubleAsFloatAtom
// implements Atom
extends Atom
{
protected DoubleAsFloatAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
// requires Java 1.5+
// return Float.valueOf( (float) b.getDouble() );
return new Float( b.getDouble() );
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x66 ); // 'f'
db.putFloat( ((Double) o).floatValue() );
}
public byte getTypeTag( Object o )
{
return 0x66; // 'f'
}
public int getAtomSize( Object o )
throws IOException
{
return 4;
}
}
private class LongAsIntegerAtom
// implements Atom
extends Atom
{
protected LongAsIntegerAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
// requires Java 1.5+
// return Integer.valueOf( (int) b.getLong() );
return new Integer( (int) b.getLong() );
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x69 ); // 'i'
db.putInt( ((Long) o).intValue() );
}
public byte getTypeTag( Object o )
{
return 0x69; // 'i'
}
public int getAtomSize( Object o )
throws IOException
{
return 4;
}
}
private class StringAtom
// implements Atom
extends Atom
{
// private String lastString;
//// private ByteBuffer lastBuf;
// private byte[] lastBuf;
protected StringAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
final int pos1 = b.position();
// final int lim = b.limit();
final String s;
final int pos2;
final byte[] bytes;
final int len;
while( b.get() != 0 ) ;
pos2 = b.position() - 1;
// b.limit( pos2 - 1 );
b.position( pos1 );
//final byte[] test = new byte[ b.limit() - pos ];
//b.get( test );
//s = new String( test );
// s = charsetDecoder.decode( b ).toString();
len = pos2 - pos1;
bytes = new byte[ len ];
b.get( bytes, 0, len );
s = new String( bytes, charsetName );
// b.limit( lim );
b.position( (pos2 + 4) & ~3 );
// skipToAlign( b );
return s;
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x73 ); // 's'
final String s = (String) o; // cassting seems tp be faster tan toString()!
db.put( s.getBytes( charsetName )); // faster than using Charset or CharsetEncoder
terminateAndPadToAlign( db );
}
public byte getTypeTag( Object o )
{
return 0x73; // 's'
}
public int getAtomSize( Object o )
throws IOException
{
final String s = (String) o;
return( (s.getBytes( charsetName ).length + 4) & ~3 );
}
}
private class BlobAtom
// implements Atom
extends Atom
{
protected BlobAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
final byte[] blob = new byte[ b.getInt() ];
b.get( blob );
skipToAlign( b );
return blob;
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
final byte[] blob = (byte[]) o;
tb.put( (byte) 0x62 ); // 'b'
db.putInt( blob.length );
db.put( blob );
padToAlign( db );
}
public byte getTypeTag( Object o )
{
return 0x62; // 'b'
}
public int getAtomSize( Object o )
throws IOException
{
return( (((byte[]) o).length + 7) & ~3 );
}
}
private class PacketAtom
// implements Atom
extends Atom
{
protected PacketAtom() { /* empty */ }
public Object decodeAtom( byte typeTag, ByteBuffer b )
throws IOException
{
throw new IOException( "Not supported" );
}
public void encodeAtom( Object o, ByteBuffer tb, ByteBuffer db )
throws IOException
{
tb.put( (byte) 0x62 ); // 'b'
final int pos = db.position();
final int pos2 = pos + 4;
db.position( pos2 );
encode( (OSCPacket) o, db ); // XXX
db.putInt( pos, db.position() - pos2 );
}
public byte getTypeTag( Object o )
{
return 0x62; // 'b'
}
public int getAtomSize( Object o )
throws IOException
{
return( getSize( (OSCPacket) o ) + 4 );
}
}
}