1/*
2 * Copyright (C) 2009 Google Inc.  All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.polo.wire.xml;
18
19import java.io.ByteArrayInputStream;
20import java.io.ByteArrayOutputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.OutputStream;
24
25/**
26 * Representation of a message sent by the XML protocol.
27 */
28public class XmlMessageWrapper {
29
30    /**
31     * Number of bytes in the header for the "receiver id" field.
32     */
33    private static final int HEADER_FIELD_RECEIVER_ID_LENGTH = 32;
34
35    /**
36     * Number of bytes in the header for the "payload length" field.
37     */
38    private static final int HEADER_FIELD_PAYLOAD_LENGTH = 4;
39
40    /**
41     * Number of bytes in the header for the "protocol version" field.
42     */
43    private static final int HEADER_FIELD_PROTOCOL_VERSION_LENGTH = 2;
44
45    /**
46     * Number of bytes in the header reserved for future use.
47     */
48    private static final int HEADER_FIELD_PADDING_LENGTH = 25;
49
50    private static final int HEADER_SIZE = 64;
51
52    /**
53     * The id of the receiver.
54     */
55    private String mReceiverId;
56
57    /**
58     * Protocol version.
59     */
60    private int mProtocolVersion;
61
62    /**
63     * Creator ID
64     */
65    private byte mCreatorId;
66
67    /**
68     * XML message.
69     */
70    private byte[] mPayload;
71
72    public XmlMessageWrapper(String recieverId, int protocolVersion,
73            byte creatorId, byte[] payload) {
74        mReceiverId = recieverId;
75        mProtocolVersion = protocolVersion;
76        mCreatorId = creatorId;
77        mPayload = payload;
78    }
79
80    /**
81     * Writes the serialized form of this message to an {@link OutputStream}
82     *
83     * @param  outputStream  the destination output stream
84     * @throws IOException  if an error occurred during write
85     */
86    public void serializeToOutputStream(OutputStream outputStream)
87            throws IOException {
88        // Receiver ID
89        outputStream.write(stringToBytesPadded(mReceiverId,
90                HEADER_FIELD_RECEIVER_ID_LENGTH));
91
92        // Payload length
93        outputStream.write(intToBigEndianIntBytes(mPayload.length));
94
95        // Protocol version
96        outputStream.write(intToBigEndianShortBytes(mProtocolVersion));
97
98        // Creator ID
99        outputStream.write(mCreatorId);
100
101        // Padding
102        byte[] pad = new byte[HEADER_FIELD_PADDING_LENGTH];
103        outputStream.write(pad);
104
105        // Payload
106        outputStream.write(mPayload);
107    }
108
109    /**
110     * Returns the serialized form of this message in a newly-allocated byte
111     * array.
112     *
113     * @return  a new byte array
114     * @throws  IOException  if an error occurred during write
115     */
116    public byte[] serializeToByteArray() throws IOException {
117        int len = mPayload.length + HEADER_SIZE;
118        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(len);
119        serializeToOutputStream(outputStream);
120        return outputStream.toByteArray();
121    }
122
123    /**
124     * Construct a new {@link XmlMessageWrapper} from an InputStream.
125     *
126     * @param stream  the {@link InputStream} to read
127     * @return  a new {@link XmlMessageWrapper}
128     * @throws IOException  if an error occurs during read
129     */
130    public static XmlMessageWrapper fromInputStream(InputStream stream)
131            throws IOException {
132        String receiverId = new String(readBytes(stream,
133                HEADER_FIELD_RECEIVER_ID_LENGTH));
134        receiverId = receiverId.replace("\0", "");
135
136        byte[] payloadLenBytes = readBytes(stream, HEADER_FIELD_PAYLOAD_LENGTH);
137        long payloadLen = intBigEndianBytesToLong(payloadLenBytes);
138
139        int protocolVersion = shortBigEndianBytesToInt(readBytes(stream,
140                HEADER_FIELD_PROTOCOL_VERSION_LENGTH));
141
142        byte createorId = readBytes(stream, 1)[0];
143        byte[] padding = readBytes(stream, HEADER_FIELD_PADDING_LENGTH);
144        byte[] payload = readBytes(stream, (int)payloadLen);
145
146        return new XmlMessageWrapper(receiverId, protocolVersion, createorId,
147                payload);
148
149    }
150
151    /**
152     * Get creator id to indicate the program is playing on TV1 or TV2.
153     */
154    public byte getCreatorId() {
155        return mCreatorId;
156    }
157
158    public byte[] getPayload() {
159        return mPayload;
160    }
161
162    /**
163     * Get the message payload as an {@link InputStream}.
164     */
165    public InputStream getPayloadStream() {
166        return new ByteArrayInputStream(mPayload);
167    }
168
169    /**
170     * Converts a 4-byte array of bytes to an unsigned long value.
171     */
172    private static final long intBigEndianBytesToLong(byte[] input) {
173        assert (input.length == 4);
174        long ret = (long)(input[0]) & 0xff;
175        ret <<= 8;
176        ret |= (long)(input[1]) & 0xff;
177        ret <<= 8;
178        ret |= (long)(input[2]) & 0xff;
179        ret <<= 8;
180        ret |= (long)(input[3]) & 0xff;
181        return ret;
182    }
183
184    /**
185     * Converts an integer value to the big endian 4-byte representation.
186     */
187    public static final byte[] intToBigEndianIntBytes(int intVal) {
188        byte[] outBuf = new byte[4];
189        outBuf[0] = (byte)((intVal >> 24) & 0xff);
190        outBuf[1] = (byte)((intVal >> 16) & 0xff);
191        outBuf[2] = (byte)((intVal >> 8) & 0xff);
192        outBuf[3] = (byte)(intVal & 0xff);
193        return outBuf;
194    }
195
196    /**
197     * Converts a 2-byte array of bytes to an unsigned long value.
198     */
199    public static final int shortBigEndianBytesToInt(byte[] input) {
200        assert (input.length == 2);
201        int ret = (input[0]) & 0xff;
202        ret <<= 8;
203        ret |= input[1] & 0xff;
204        return ret;
205    }
206
207    /**
208     * Converts an integer value to the 2-byte short representation.  The two
209     * most significant bytes are ignored.
210     */
211    public static final byte[] intToBigEndianShortBytes(int intVal) {
212        byte[] outBuf = new byte[2];
213        outBuf[0] = (byte)((intVal >> 8) & 0xff);
214        outBuf[1] = (byte)(intVal & 0xff);
215        return outBuf;
216    }
217
218    /**
219     * Converts a string to a byte sequence of exactly byteLen bytes,
220     * padding with null characters if needed.
221     *
222     * @param byteLen  the size of the byte array to return
223     * @return  a byte array
224     */
225    public static final byte[] stringToBytesPadded(String string, int byteLen) {
226        byte[] outBuf = new byte[byteLen];
227        byte[] stringBytes = string.getBytes();
228
229        for (int i=0; i < outBuf.length; i++) {
230            if (i < stringBytes.length) {
231                outBuf[i] = stringBytes[i];
232            } else {
233                outBuf[i] = '\0';
234            }
235        }
236        return outBuf;
237    }
238
239    /**
240     * Reads an exact number of bytes from an input stream.
241     *
242     * @param stream  the stream to read
243     * @param numBytes  the number of bytes desired
244     * @return  a byte array of results
245     * @throws IOException  if an error occurred during read, or stream closed
246     */
247    private static byte[] readBytes(InputStream stream, int numBytes)
248            throws IOException {
249        byte buffer[] = new byte[numBytes];
250        int bytesRead = 0;
251
252        while (bytesRead < numBytes) {
253            int inc = stream.read(buffer, bytesRead, numBytes - bytesRead);
254            if (inc < 0) {
255                throw new IOException("Stream closed while reading.");
256            }
257            bytesRead += inc;
258        }
259
260        return buffer;
261    }
262}
263