/*
* Copyright (C) 2003-2014, C. Ramakrishnan / Illposed Software.
* All rights reserved.
*
* This code is licensed under the BSD 3-Clause license.
* See file LICENSE (or LICENSE.html) for more information.
*/
package com.illposed.osc.utility;
import com.illposed.osc.OSCImpulse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Date;
/**
* OSCJavaToByteArrayConverter is a helper class that translates
* from Java types to their byte stream representations according to
* the OSC spec.
*
* The implementation is based on
* <a href="http://www.emergent.de">Markus Gaelli</a> and
* Iannis Zannos's OSC implementation in Squeak (a Smalltalk dialect).
*
* This version includes bug fixes and improvements from
* Martin Kaltenbrunner and Alex Potsides.
*
* @author Chandrasekhar Ramakrishnan
* @author Martin Kaltenbrunner
* @author Alex Potsides
*/
public class OSCJavaToByteArrayConverter {
/**
* baseline NTP time if bit-0=0 is 7-Feb-2036 @ 06:28:16 UTC
*/
protected static final long MSB_0_BASE_TIME = 2085978496000L;
/**
* baseline NTP time if bit-0=1 is 1-Jan-1900 @ 01:00:00 UTC
*/
protected static final long MSB_1_BASE_TIME = -2208988800000L;
private final ByteArrayOutputStream stream;
/** Used to encode message addresses and string parameters. */
private Charset charset;
private final byte[] intBytes;
private final byte[] longintBytes;
public OSCJavaToByteArrayConverter() {
this.stream = new ByteArrayOutputStream();
this.charset = Charset.defaultCharset();
this.intBytes = new byte[4];
this.longintBytes = new byte[8];
}
/**
* Returns the character set used to encode message addresses
* and string parameters.
* @return the character-encoding-set used by this converter
*/
public Charset getCharset() {
return charset;
}
/**
* Sets the character set used to encode message addresses
* and string parameters.
* @param charset the desired character-encoding-set to be used by this converter
*/
public void setCharset(Charset charset) {
this.charset = charset;
}
/**
* Line up the Big end of the bytes to a 4 byte boundary.
* @return byte[]
* @param bytes byte[]
*/
public static byte[] alignBigEndToFourByteBoundry(byte[] bytes) {
int mod = bytes.length % 4;
// if the remainder == 0 then return the bytes otherwise pad the bytes
// to lineup correctly
if (mod == 0) {
return bytes;
}
int pad = 4 - mod;
byte[] newBytes = new byte[pad + bytes.length];
// for (int i = 0; i < pad; i++)
// newBytes[i] = 0;
// for (int i = 0; i < bytes.length; i++)
// newBytes[pad + i] = bytes[i];
System.arraycopy(bytes, 0, newBytes, pad, bytes.length);
return newBytes;
}
/**
* Pad the stream to have a size divisible by 4.
*/
public void appendNullCharToAlignStream() {
final int alignmentOverlap = stream.size() % 4;
final int padLen = (4 - alignmentOverlap) % 4;
for (int pci = 0; pci < padLen; pci++) {
stream.write(0);
}
}
/**
* Convert the contents of the output stream to a byte array.
* @return the byte array containing the byte stream
*/
public byte[] toByteArray() {
return stream.toByteArray();
}
/**
* Write bytes into the byte stream.
* @param bytes bytes to be written
*/
public void write(byte[] bytes) {
writeInteger32ToByteArray(bytes.length);
writeUnderHandler(bytes);
appendNullCharToAlignStream();
}
/**
* Write an integer into the byte stream.
* @param i the integer to be written
*/
public void write(int i) {
writeInteger32ToByteArray(i);
}
/**
* Write a float into the byte stream.
* @param f floating point number to be written
*/
public void write(Float f) {
writeInteger32ToByteArray(Float.floatToIntBits(f));
}
/**
* Write a double into the byte stream (8 bytes).
* @param d double precision floating point number to be written
*/
public void write(Double d) {
writeInteger64ToByteArray(Double.doubleToRawLongBits(d));
}
/**
* @param i the integer to be written
*/
public void write(Integer i) {
writeInteger32ToByteArray(i);
}
/**
* @param l the double precision integer to be written
*/
public void write(Long l) {
writeInteger64ToByteArray(l);
}
/**
* @param timestamp the timestamp to be written
*/
public void write(Date timestamp) {
writeInteger64ToByteArray(javaToNtpTimeStamp(timestamp.getTime()));
}
/**
* Converts a Java time-stamp to a 64-bit NTP time representation.
* This code was copied in from the "Apache Jakarta Commons - Net" library,
* which is licensed under the
* <a href="http://www.apache.org/licenses/LICENSE-2.0.html">ASF 2.0 license</a>.
* The original source file can be found
* <a href="http://svn.apache.org/viewvc/commons/proper/net/trunk/src/main/java/org/apache/commons/net/ntp/TimeStamp.java?view=co">here</a>.
* @param javaTime Java time-stamp, as returned by {@link Date#getTime()}
* @return NTP time-stamp representation of the Java time value.
*/
protected static long javaToNtpTimeStamp(long javaTime) {
final boolean useBase1 = javaTime < MSB_0_BASE_TIME; // time < Feb-2036
final long baseTime;
if (useBase1) {
baseTime = javaTime - MSB_1_BASE_TIME; // dates <= Feb-2036
} else {
// if base0 needed for dates >= Feb-2036
baseTime = javaTime - MSB_0_BASE_TIME;
}
long seconds = baseTime / 1000;
final long fraction = ((baseTime % 1000) * 0x100000000L) / 1000;
if (useBase1) {
seconds |= 0x80000000L; // set high-order bit if msb1baseTime 1900 used
}
final long ntpTime = seconds << 32 | fraction;
return ntpTime;
}
/**
* Write a string into the byte stream.
* @param aString the string to be written
*/
public void write(String aString) {
/*
XXX to be revised ...
int stringLength = aString.length();
// this is a deprecated method -- should use get char and convert
// the chars to bytes
// aString.getBytes(0, stringLength, stringBytes, 0);
aString.getChars(0, stringLength, stringChars, 0);
// pad out to align on 4 byte boundry
int mod = stringLength % 4;
int pad = 4 - mod;
for (int i = 0; i < pad; i++)
stringChars[stringLength++] = 0;
// convert the chars into bytes and write them out
for (int i = 0; i < stringLength; i++) {
stringBytes[i] = (byte) (stringChars[i] & 0x00FF);
}
stream.write(stringBytes, 0, stringLength);
*/
byte[] stringBytes = aString.getBytes(charset);
// pad out to align on 4 byte boundry
int mod = aString.length() % 4;
int pad = 4 - mod;
byte[] newBytes = new byte[pad + stringBytes.length];
System.arraycopy(stringBytes, 0, newBytes, 0, stringBytes.length);
try {
stream.write(newBytes);
} catch (IOException e) {
throw new RuntimeException("You're screwed:"
+ " IOException writing to a ByteArrayOutputStream", e);
}
}
/**
* Write a char into the byte stream, and ensure it is 4 byte aligned again.
* @param c the character to be written
*/
public void write(Character c) {
stream.write((char) c);
appendNullCharToAlignStream();
}
/**
* Write a char into the byte stream.
* CAUTION, this does not ensure 4 byte alignment (it actually breaks it)!
* @param c the character to be written
*/
public void write(char c) {
stream.write(c);
}
/**
* Write an object into the byte stream.
* @param anObject one of Float, Double, String, Character, Integer, Long,
* or array of these.
*/
public void write(Object anObject) {
// Can't do switch on class
if (anObject instanceof Collection) {
Collection<Object> theArray = (Collection<Object>) anObject;
for (Object entry : theArray) {
write(entry);
}
} else if (anObject instanceof Float) {
write((Float) anObject);
} else if (anObject instanceof Double) {
write((Double) anObject);
} else if (anObject instanceof String) {
write((String) anObject);
} else if (anObject instanceof byte[]) {
write((byte[]) anObject);
} else if (anObject instanceof Character) {
write((Character) anObject);
} else if (anObject instanceof Integer) {
write((Integer) anObject);
} else if (anObject instanceof Long) {
write((Long) anObject);
} else if (anObject instanceof Date) {
write((Date) anObject);
} else if (anObject instanceof OSCImpulse) {
// Write nothing here, as all the info is already contained in the type ('I').
} else if (anObject instanceof Boolean) {
// Write nothing here, as all the info is already contained in the type ('T' or 'F').
} else if (anObject == null) {
// Write nothing here, as all the info is already contained in the type ('N').
} else {
throw new RuntimeException("Do not know how to write an object of class: "
+ anObject.getClass());
}
}
/**
* Write the OSC specification type tag for the type a certain Java type
* converts to.
* @param c Class of a Java object in the arguments
*/
public void writeType(Class c) {
// A big ol' else-if chain -- what's polymorphism mean, again?
// I really wish I could extend the base classes!
if (Integer.class.equals(c)) {
stream.write('i');
} else if (Long.class.equals(c)) {
stream.write('h');
} else if (Date.class.equals(c)) {
stream.write('t');
} else if (Float.class.equals(c)) {
stream.write('f');
} else if (Double.class.equals(c)) {
stream.write('d');
} else if (String.class.equals(c)) {
stream.write('s');
} else if (byte[].class.equals(c)) {
stream.write('b');
} else if (Character.class.equals(c)) {
stream.write('c');
} else if (OSCImpulse.class.equals(c)) {
stream.write('I');
} else {
throw new RuntimeException("Do not know the OSC type for the java class: " + c);
}
}
/**
* Write the types for an array element in the arguments.
* @param array array of base Objects
*/
private void writeTypesArray(Collection<Object> array) {
// A big ol' case statement in a for loop -- what's polymorphism mean,
// again?
// I really wish I could extend the base classes!
for (Object element : array) {
if (Boolean.TRUE.equals(element)) {
// Create a way to deal with Boolean type objects
stream.write('T');
} else if (Boolean.FALSE.equals(element)) {
stream.write('F');
} else {
// this is an object -- write the type for the class
writeType(element.getClass());
}
}
}
/**
* Write types for the arguments.
* @param types the arguments to an OSCMessage
*/
public void writeTypes(Collection<Object> types) {
for (Object type : types) {
if (null == type) {
stream.write('N');
} else if (type instanceof Collection) {
// If the array at i is a type of array, write a '['.
// This is used for nested arguments.
stream.write('[');
// fill the [] with the SuperCollider types corresponding to
// the object (e.g., Object of type String needs -s).
// XXX Why not call this function, recursively? The only reason would be, to not allow nested arrays, but the specification does not say anythign about them not being allowed.
writeTypesArray((Collection<Object>) type);
// close the array
stream.write(']');
} else if (Boolean.TRUE.equals(type)) {
stream.write('T');
} else if (Boolean.FALSE.equals(type)) {
stream.write('F');
} else {
// go through the array and write the superCollider types as shown
// in the above method.
// The classes derived here are used as the arg to the above method.
writeType(type.getClass());
}
}
// align the stream with padded bytes
appendNullCharToAlignStream();
}
/**
* Write bytes to the stream, catching IOExceptions and converting them to
* RuntimeExceptions.
* @param bytes to be written to the stream
*/
private void writeUnderHandler(byte[] bytes) {
try {
stream.write(bytes);
} catch (IOException e) {
throw new RuntimeException("You're screwed:"
+ " IOException writing to a ByteArrayOutputStream");
}
}
/**
* Write a 32 bit integer to the byte array without allocating memory.
* @param value a 32 bit integer.
*/
private void writeInteger32ToByteArray(int value) {
//byte[] intBytes = new byte[4];
//I allocated the this buffer globally so the GC has less work
intBytes[3] = (byte)value; value >>>= 8;
intBytes[2] = (byte)value; value >>>= 8;
intBytes[1] = (byte)value; value >>>= 8;
intBytes[0] = (byte)value;
try {
stream.write(intBytes);
} catch (IOException ex) {
throw new RuntimeException("You're screwed:"
+ " IOException writing to a ByteArrayOutputStream", ex);
}
}
/**
* Write a 64 bit integer to the byte array without allocating memory.
* @param value a 64 bit integer.
*/
private void writeInteger64ToByteArray(long value) {
longintBytes[7] = (byte)value; value >>>= 8;
longintBytes[6] = (byte)value; value >>>= 8;
longintBytes[5] = (byte)value; value >>>= 8;
longintBytes[4] = (byte)value; value >>>= 8;
longintBytes[3] = (byte)value; value >>>= 8;
longintBytes[2] = (byte)value; value >>>= 8;
longintBytes[1] = (byte)value; value >>>= 8;
longintBytes[0] = (byte)value;
try {
stream.write(longintBytes);
} catch (IOException ex) {
throw new RuntimeException("You're screwed:"
+ " IOException writing to a ByteArrayOutputStream", ex);
}
}
}