/*
* Hamsam - Instant Messaging API
* Copyright (C) 2003 Raghu K
*
* 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
*/
package hamsam.protocol.yahoo;
import java.io.IOException;
import java.util.Vector;
import java.util.Enumeration;
import hamsam.net.Connection;
import hamsam.exception.IllegalArgumentException;
/**
* A packet is the unit of communication between Yahoo server and
* client.
*
* <p>
* A packet consists of a header and a data section. The header is fixed length
* (20 bytes) while the data can have any length between 0 to 2^16 - 1 bytes.
* The header has the following format.
*
* <p>
* <table border="1">
* <tr>
* <th>Field Name</th>
* <th>Meaning</th>
* <th>Length</th>
* </tr>
* <tr>
* <td>Protocol ID</td>
* <td>
* This is an identifier used to identify Yahoo packets. It is always
* four ASCII characters - "YMSG"
* </td>
* <td>4 bytes</td>
* </tr>
* <tr>
* <td>Protocol Version</td>
* <td>
* This is used to identify the version of the protocol used. The version
* used here is 0x000b0000. This value is present only in packets that are
* sent from client to server, in packets received by the client from server,
* this is always 0.
* </td>
* <td>4 bytes</td>
* </tr>
* <tr>
* <td>Data Length</td>
* <td>
* This is the length of the data section in bytes, in big-endian order.
* </td>
* <td>2 bytes</td>
* </tr>
* <tr>
* <td>Service Type</td>
* <td>
* To distinguish different operations, Yahoo uses different service codes,
* all these are defined in the {@link Constants Constants} interface.
* </td>
* <td>2 bytes</td>
* </tr>
* <tr>
* <td>Status Indicator</td>
* <td>
* In case of a response from the server, indicates the status
* of the request (success/failure/etc.). For a request, it is 0
* in most cases, except for packets that set the user's status
* (set status, typing notify, etc.). All these values are defined in
* the {@link Constants Constants} interface.
* </td>
* <td>4 bytes</td>
* </tr>
* <tr>
* <td>Session ID</td>
* <td>
* The session identifier is a unique number to identify the protocol
* session. When the client sends the first packet, this is set to 0.
* The server responds with a session identifier. This value is used
* for all further packets, by the client as well as the server.
* </td>
* <td>4 bytes</td>
* </tr>
* </table>
*
* <p>
* The header is followed by a data section that is a collection of key value pairs
* A key may have multiple values associated. A key is a numeric string. Packet stores
* a key as a numeric value in ASCII character set. A key is terminated by the
* special byte sequence 0xc0 0x80. This is followed by a value, which is again
* terminated by 0xc0 0x80.
*
* @author Raghu
*/
public class Packet
{
private long version;
/**
* The service type of this packet.
*/
private int service;
/**
* The status indicator of this packet.
*/
private long status;
/**
* Session identifier for this packet.
*/
private long sessionID;
/**
* The keys and values in the data section is put in this
* vector. The first element is a key, the second is its
* value, the third is the second key, while fourth is
* the second key's value and so on.
*/
private Vector data;
/**
* Construct a packet with the specified parameters.
*
* @param service the service type of this packet.
* @param status the status indicator of this packet.
* @param sessionID the sessionID for this packet.
*/
public Packet(int service, long status, long sessionID)
{
this.service = service;
this.status = status;
this.sessionID = sessionID;
this.version = 0x000b0000;
data = new Vector();
}
/**
* Construct a packet from an array of characters that represents the
* packet.
*
* @param data the array of characters that form the packet.
* @throws IllegalArgumentException if the data does not form a valid packet.
*/
public Packet(char[] data) throws IllegalArgumentException
{
if(data.length < 20)
throw new IllegalArgumentException("Yahoo packet is too small, size = " + data.length);
/* header starts with 'YMSG' */
if(data[0] != 'Y' || data[1] != 'M' || data[2] != 'S' || data[3] != 'G')
throw new IllegalArgumentException("Invalid yahoo packet header (No YMSG found)");
/* get the version */
version = ((long)toUnsigned(data[4]) << 24) +
((long)toUnsigned(data[5]) << 16) +
((long)toUnsigned(data[6]) << 8) +
toUnsigned(data[7]);
/* get the length of the data portion */
int dataLength = (toUnsigned(data[8]) << 8) + toUnsigned(data[9]);
if(data.length != dataLength + 20)
throw new IllegalArgumentException("Yahoo packet size is not correct, header specifies a packet size of " + (dataLength + 20) + ", but actual packet size is " + data.length);
/* get the service type */
service = (toUnsigned(data[10]) << 8) + toUnsigned(data[11]);
/* get the status */
status = ((long)toUnsigned(data[12]) << 24) +
((long)toUnsigned(data[13]) << 16) +
((long)toUnsigned(data[14]) << 8) +
toUnsigned(data[15]);
/* get the status */
sessionID = ((long)toUnsigned(data[16]) << 24) +
((long)toUnsigned(data[17]) << 16) +
((long)toUnsigned(data[18]) << 8) +
toUnsigned(data[19]);
/* create the hashtable */
boolean gettingKey = true;
int key = 0, valueStart = 20;
String value;
int i = 20;
while(i < data.length - 1)
{
int dataCurrent = toUnsigned(data[i]);
int dataNext = toUnsigned(data[i + 1]);
if(gettingKey)
{
// check for a key
if(dataCurrent != 0xc0)
key = key * 10 + dataCurrent - '0';
else if(dataNext == 0x80)
{
gettingKey = false;
valueStart = i + 2;
i++;
}
else
throw new IllegalArgumentException("Invalid packet (No end signature found)");
}
else
{
// get value
if(dataCurrent == 0xc0 && dataNext == 0x80)
{
value = new String(data, valueStart, i - valueStart);
this.data.add(new Integer(key));
this.data.add(value);
gettingKey = true;
key = 0;
i++;
}
}
i++;
}
}
/**
* Build a packet from the bytes read from a connection.
*
* @param conn the connection from which the packet is to be read.
* @throws IllegalArgumentException if the data read does not form a valid packet.
* @throws IOException if an I/O error occurs.
*/
public Packet(Connection conn) throws IllegalArgumentException, IOException
{
data = new Vector();
byte[] header = new byte[20];
int count = 0;
/* Read the header */
while(count != header.length)
{
int ret = conn.read(header, count, header.length - count);
if(ret == -1)
throw new IllegalArgumentException("Stream closed without enough data");
count += ret;
}
/* header starts with 'YMSG' */
if(header[0] != 'Y' || header[1] != 'M' || header[2] != 'S' || header[3] != 'G')
throw new IllegalArgumentException("Invalid yahoo packet header (No YMSG found)");
/* get the version */
version = ((long)toUnsigned(header[4]) << 24) +
((long)toUnsigned(header[5]) << 16) +
((long)toUnsigned(header[6]) << 8) +
toUnsigned(header[7]);
/* get the length of the data portion */
int dataLength = (toUnsigned(header[8]) << 8) + toUnsigned(header[9]);
/* get the service type */
service = (toUnsigned(header[10]) << 8) + toUnsigned(header[11]);
/* get the status */
status = ((long)toUnsigned(header[12]) << 24) +
((long)toUnsigned(header[13]) << 16) +
((long)toUnsigned(header[14]) << 8) +
toUnsigned(header[15]);
/* get the status */
sessionID = ((long)toUnsigned(header[16]) << 24) +
((long)toUnsigned(header[17]) << 16) +
((long)toUnsigned(header[18]) << 8) +
toUnsigned(header[19]);
/* now load the data portion */
byte[] dataArray = new byte[dataLength];
count = 0;
while(count != dataArray.length)
{
count += conn.read(dataArray, count, dataArray.length - count);
}
/* create the hashtable */
boolean gettingKey = true;
int key = 0, valueStart = 0;
String value;
int i = 0;
while(i < count - 1)
{
int data = toUnsigned(dataArray[i]);
int dataNext = toUnsigned(dataArray[i + 1]);
if(gettingKey)
{
// check for a key
if(data != 0xc0)
key = key * 10 + data - '0';
else if(dataNext == 0x80)
{
gettingKey = false;
valueStart = i + 2;
i++;
}
else
throw new IllegalArgumentException("Invalid packet (No end signature found)");
}
else
{
// get value
if(data == 0xc0 && dataNext == 0x80)
{
value = new String(dataArray, valueStart, i - valueStart);
this.data.add(new Integer(key));
this.data.add(value);
gettingKey = true;
key = 0;
i++;
}
}
i++;
}
}
/**
* Convert this packet to a byte array which can be sent to
* Yahoo server.
*
* @return a byte array representing this packet.
*/
public byte[] toBytes()
{
// compute length of data portion
int dataLength = 0;
Enumeration all = data.elements();
while(all.hasMoreElements())
{
Object elem = all.nextElement();
dataLength += elem.toString().length() + 2;
}
byte[] ret = new byte[dataLength + 20];
ret[0] = 'Y'; ret[1] = 'M'; ret[2] = 'S'; ret[3] = 'G';
/* version */
ret[4] = (byte)((version >> 24) & 0xff);
ret[5] = (byte)((version >> 16) & 0xff);
ret[6] = (byte)((version >> 8) & 0xff);
ret[7] = (byte)(version & 0xff);
/* data length */
ret[8] = (byte)((dataLength >> 8) & 0xff);
ret[9] = (byte)(dataLength & 0xff);
/* service type */
ret[10] = (byte)((service >> 8) & 0xff);
ret[11] = (byte)(service & 0xff);
/* status */
ret[12] = (byte)((status >> 24) & 0xff);
ret[13] = (byte)((status >> 16) & 0xff);
ret[14] = (byte)((status >> 8) & 0xff);
ret[15] = (byte)(status & 0xff);
/* session id */
ret[16] = (byte)((sessionID >> 24) & 0xff);
ret[17] = (byte)((sessionID >> 16) & 0xff);
ret[18] = (byte)((sessionID >> 8) & 0xff);
ret[19] = (byte)(sessionID & 0xff);
/* data */
int index = 20;
all = data.elements();
while(all.hasMoreElements())
{
Object elem = all.nextElement();
byte[] elemData = elem.toString().getBytes();
for(int i = 0; i < elemData.length; i++)
ret[index++] = elemData[i];
ret[index++] = (byte)0xc0;
ret[index++] = (byte)0x80;
}
return ret;
}
/**
* Add a key value pair to the data section of this
* packet.
*
* @param key the key to be added.
* @param val the value to be added.
*/
public synchronized void putData(int key, String val)
{
data.add(new Integer(key));
data.add(new String(val));
}
/**
* Get the number of key value pairs present in this packet.
*
* @return the number of key value pairs.
*/
public int getDataSize()
{
return data.size() / 2;
}
/**
* Get the key from the key value pair at a specified index.
*
* @return the key at the specified index.
* @throws ArrayIndexOutOfBoundsException if the index is out of range.
*/
public int getKey(int index) throws ArrayIndexOutOfBoundsException
{
return ((Integer) data.elementAt(index * 2)).intValue();
}
/**
* Get the value from the key value pair at a specified index.
*
* @return the value at the specified index.
* @throws ArrayIndexOutOfBoundsException if the index is out of range.
*/
public String getValue(int index)
{
return new String((String) data.elementAt(index * 2 + 1));
}
/**
* Returns the service type of this packet.
*
* @return service type of this packet.
*/
public int getService()
{
return service;
}
/**
* Returns the status indicator of this packet.
*
* @return status indicator of this packet.
*/
public long getStatus()
{
return status;
}
/**
* Returns the session identifier of this packet.
*
* @return the session identiier of thsi packet.
*/
public long getSessionID()
{
return sessionID;
}
/**
* Returns a string representation of this packet.
*
* @return a string representation of this packet.
*/
public String toString()
{
String ret = new String("YahooPacket { ");
ret += "version = " + version + ", service = " + service + ", status = " + status + ", session id = " + sessionID;
ret += ", data = \n";
Enumeration all = data.elements();
while(all.hasMoreElements())
{
Object key = all.nextElement();
ret += key.toString() + " = ";
Object value = all.nextElement();
ret += value.toString() + "\n";
}
ret += " }";
return ret;
}
/**
* Convert a signed byte to unsigned byte.
*
* @param val the signed byte to be converted.
* @return the equivalent unsigned value.
*/
private int toUnsigned(int val)
{
return val < 0 ? 256 + val : val;
}
}