View Javadoc
1   package com.ericsson.research.trap.impl;
2   
3   /*
4    * ##_BEGIN_LICENSE_##
5    * Transport Abstraction Package (trap)
6    * ----------
7    * Copyright (C) 2014 Ericsson AB
8    * ----------
9    * Redistribution and use in source and binary forms, with or without modification,
10   * are permitted provided that the following conditions are met:
11   * 
12   * 1. Redistributions of source code must retain the above copyright notice, this
13   *    list of conditions and the following disclaimer.
14   * 
15   * 2. Redistributions in binary form must reproduce the above copyright notice,
16   *    this list of conditions and the following disclaimer in the documentation
17   *    and/or other materials provided with the distribution.
18   * 
19   * 3. Neither the name of the Ericsson AB nor the names of its contributors
20   *    may be used to endorse or promote products derived from this software without
21   *    specific prior written permission.
22   * 
23   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
26   * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28   * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
31   * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
32   * OF THE POSSIBILITY OF SUCH DAMAGE.
33   * ##_END_LICENSE_##
34   */
35  
36  import java.io.ByteArrayOutputStream;
37  import java.io.IOException;
38  import java.io.UnsupportedEncodingException;
39  import java.nio.ByteBuffer;
40  import java.nio.charset.Charset;
41  import java.util.zip.DeflaterOutputStream;
42  import java.util.zip.InflaterOutputStream;
43  
44  import com.ericsson.research.trap.spi.TrapConstants;
45  import com.ericsson.research.trap.spi.TrapMessage;
46  import com.ericsson.research.trap.utils.StringUtil;
47  
48  public class TrapMessageImpl implements TrapMessage
49  {
50  	
51  	protected byte[]		data			= new byte[] {};
52  	protected String		authString		= null;
53  	protected Format		format			= TrapConstants.MESSAGE_FORMAT_DEFAULT;
54  	protected Operation		op				= Operation.OK;
55  	private int				messageId		= 0;
56  	private boolean			compressed		= false;
57  	private int				channelId		= 0;
58  	
59  	public static final int	HEADER_SIZE		= 16;
60  	
61  	public TrapMessageImpl()
62  	{
63  	}
64  	
65  	public TrapMessageImpl(byte[] data) throws UnsupportedEncodingException
66  	{
67  		this.deserialize(data, 0, data.length);
68  	}
69  	
70  	/* (non-Javadoc)
71  	 * @see com.ericsson.research.trap.impl.TrapMessage#serialize()
72  	 */
73  	
74  	public byte[] serialize() throws IOException
75  	{
76  		// Make sure the data we have is serialized.
77  		// Yeah, I know this looks really odd
78  		
79  		if (this.format == Format.SEVEN_BIT_SAFE)
80  		{
81  			ByteArrayOutputStream bos = new ByteArrayOutputStream();
82  			this.serialize7bit(bos);
83  			return bos.toByteArray();
84  		}
85  		else
86  			return this.serialize8bit();
87  		
88  	}
89  	
90  	protected byte[] serialize8bit() throws UnsupportedEncodingException, IOException
91  	{
92  		
93  		// Make 8-bit assertions
94  		if (this.data.length >= Math.pow(2, 32))
95  			throw new IllegalStateException("Asked to serialize more than 2^32 bytes data into a 8-bit Trap message");
96  		
97  		byte b1 = 0, b2 = 0;
98  		
99  		byte[] mData = this.getCompressedData();
100 		
101 		// First byte: |1|0| MESSAGEOP |
102 		b1 |= this.op.getOp() | 0x80;
103 		
104 		// Second byte: compressed | RSV1
105 		b2 = 0;
106 		if (this.compressed)
107 			b2 |= 0x80;
108 		
109 		int authLen = (this.authString != null ? this.authString.length() : 0);
110 		
111 		ByteBuffer buf = ByteBuffer.allocate(HEADER_SIZE + authLen + mData.length);
112 		buf.put(b1);
113 		buf.put(b2);
114 		
115 		// Bytes 3-4 : AuthLen
116 		buf.putShort((short) authLen);
117 		
118 		// Bytes 5-8: MessageID
119 		buf.putInt(this.getMessageId());
120 		
121 		// Byte 9: RSV2
122 		buf.put((byte) 0);
123 		
124 		// Byte 10: ChannelID
125 		buf.put((byte) this.channelId);
126 		
127 		// Byte 11-12: RSV3
128 		buf.putShort((short) 0);
129 		
130 		buf.putInt(mData.length);
131 		
132 		if (authLen > 0)
133 			buf.put(this.authString.getBytes("UTF-8"));
134 		
135 		buf.put(mData);
136 		
137 		/*// Third byte: Bits 3 - 9 of authLen
138 		bos.write((authLen >>> 8) & 0xFF);
139 		
140 		// Fourth byte: Bits 10 - 16 of authLen
141 		bos.write(authLen & 0xFF);
142 		
143 		// Transport ID!
144 		this.writeInt8(this.getMessageId(), bos, true);
145 		this.writeInt8(this.transportId, bos, true);
146 		this.writeInt8(mData.length, bos, false);
147 		
148 		if (authLen > 0)
149 			bos.write(this.authString.getBytes("UTF-8"));
150 		
151 		bos.write(mData);*/
152 		
153 		return buf.array();
154 	}
155 	
156 	protected void serialize7bit(ByteArrayOutputStream bos) throws IOException
157 	{
158 		
159 		// Make 7-bit assertions
160 		if (this.data.length >= Math.pow(2, 28))
161 			throw new IllegalStateException("Asked to serialize more than 2^28 bytes data into a 7-bit Trap message");
162 		
163 		byte b = 0;
164 		
165 		// First byte: |0|0| MESSAGEOP |
166 		b |= this.op.getOp();
167 		bos.write(b);
168 		
169 		int authLen = (this.authString != null ? this.authString.length() : 0);
170 		
171 		// Second byte: First two bits of authLen
172 		// Compatibility note: 7-bit mode does not gain channels/compression.
173 		bos.write(this.getBits(authLen, 17, 18));
174 		
175 		// Third byte: Bits 3 - 9 of authLen
176 		bos.write(this.getBits(authLen, 19, 25));
177 		
178 		// Fourth byte: Bits 10 - 16 of authLen
179 		bos.write(this.getBits(authLen, 26, 32));
180 		
181 		// Transport ID!
182 		this.writeInt7(this.getMessageId(), bos, true);
183 		this.writeInt7(0, bos, true);
184 		this.writeInt7(this.data.length, bos, false);
185 		
186 		if (authLen > 0)
187 			bos.write(this.authString.getBytes("UTF-8"));
188 		
189 		bos.write(this.data);
190 	}
191 	
192 	private void writeInt7(int src, ByteArrayOutputStream bos, boolean signed)
193 	{
194 		bos.write(this.getBits(src, 5, 11));
195 		bos.write(this.getBits(src, 12, 18));
196 		bos.write(this.getBits(src, 19, 25));
197 		bos.write(this.getBits(src, 26, 32));
198 	}
199 	
200 	/* (non-Javadoc)
201 	 * @see com.ericsson.research.trap.impl.TrapMessage#deserialize(byte[])
202 	 */
203 	
204 	public int deserialize(byte[] rawData, int offset, int length) throws UnsupportedEncodingException
205 	{
206 		
207 		if ((offset + length) > rawData.length)
208 			throw new IllegalArgumentException("Offset and length specified exceed the buffer");
209 		
210 		if (length < 16)
211 			return -1;
212 		
213 		int authLen;
214 		int contentLen;
215 		
216 		if ((rawData[offset + 0] & 0x80) != 0)
217 		{
218 			// 8-bit
219 			this.format = Format.REGULAR;
220 			this.op = Operation.getType(rawData[offset + 0] & 0x3F);
221 			this.compressed = (rawData[offset + 1] & 0x80) != 0;
222 			
223 			/*
224 			 * Byte values are sign extended to 32 bits before any any bitwise operations are performed on the value. Thus, if b[0] contains the value 0xff, and x is initially 0, then the code ((x << 8) | b[0]) will sign extend 0xff to get 0xffffffff, and thus give the value 0xffffffff as the result.
225 
226 				In particular, the following code for packing a byte array into an int is badly wrong:
227 
228 			int result = 0;
229 			for(int i = 0; i < 4; i++)
230 			result = ((result << 8) | b[i]);
231 
232 			The following idiom will work instead:
233 
234 			int result = 0;
235 			for(int i = 0; i < 4; i++)
236 			result = ((result << 8) | (b[i] & 0xff));
237 			 */
238 			
239 			/*
240 			authLen = ((rawData[offset + 2] & 0xFF) << 8) | (rawData[offset + 3] & 0xFF);
241 			this.messageId = ((rawData[offset + 4] & 0xFF) << 24) | ((rawData[offset + 5] & 0xFF) << 16) | ((rawData[offset + 6] & 0xFF) << 8) | (rawData[offset + 7] & 0xFF);
242 			this.transportId = ((rawData[offset + 8] & 0xFF) << 24) | ((rawData[offset + 9] & 0xFF) << 16) | ((rawData[offset + 10] & 0xFF) << 8) | (rawData[offset + 11] & 0xFF);
243 			contentLen = ((rawData[offset + 12] & 0xFF) << 24) | ((rawData[offset + 13] & 0xFF) << 16) | ((rawData[offset + 14] & 0xFF) << 8) | (rawData[offset + 15] & 0xFF);
244 			*/
245 			
246 			ByteBuffer hs = ByteBuffer.wrap(rawData, offset + 2, TrapMessageImpl.HEADER_SIZE - 2);
247 			authLen = hs.getShort() & 0xFFFF;
248 			this.messageId = hs.getInt();
249 			
250 			// Skip RSV2 (8 bits)
251 			hs.get();
252 			this.channelId = hs.get();
253 			
254 			// Skip RSV3 (16 bits)
255 			hs.getShort();
256 			
257 			contentLen = hs.getInt();
258 			//throw new IllegalArgumentException();
259 			
260 		}
261 		else
262 		{
263 			// 7-bit
264 			this.format = Format.SEVEN_BIT_SAFE;
265 			this.op = Operation.getType(rawData[offset + 0] & 0x3F);
266 			
267 			authLen = ((rawData[offset + 1] & 0x03) << 14) | ((rawData[offset + 2] & 0x7F) << 7) | ((rawData[offset + 3] & 0x7F) << 0);
268 			this.messageId = ((rawData[offset + 4] & 0x7F) << 21) | ((rawData[offset + 5] & 0x7F) << 14) | ((rawData[offset + 6] & 0x7F) << 7) | ((rawData[offset + 7] & 0x7F) << 0);
269 			//this.transportId = ((rawData[offset + 8] & 0x7F) << 21) | ((rawData[offset + 9] & 0x7F) << 14) | ((rawData[offset + 10] & 0x7F) << 7) | ((rawData[offset + 11] & 0x7F) << 0);
270 			
271 			contentLen = ((rawData[offset + 12] & 0x7F) << 21) | ((rawData[offset + 13] & 0x7F) << 14) | ((rawData[offset + 14] & 0x7F) << 7) | ((rawData[offset + 15] & 0x7F) << 0);
272 			
273 			this.compressed = false;
274 			this.channelId = 0;
275 		}
276 		
277 		// Verify that there's enough remaining content to read the message.
278 		int messageSize = 16 + authLen + contentLen;
279 		
280 		if (length < messageSize)
281 			return -1; // Cannot successfully read the remaining values.
282 			
283 		// Range of authHeader = (16, authLen)
284 		int startByte = offset + 16;
285 		
286 		// We have an authentication header!
287 		if (authLen > 0)
288 		{
289 			
290 			// Make a new string of the appropriate bytes
291 			this.authString = new String(rawData, startByte, authLen, Charset.forName("UTF-8"));
292 			startByte += authLen;
293 		}
294 		else
295 		{
296 			this.authString = null;
297 		}
298 		
299 		if (!this.compressed)
300 		{
301 			// Copy the data
302 			//Arrays.copyOfRange(rawData, startByte, startByte+contentLen);
303 			this.data = new byte[contentLen];
304 			System.arraycopy(rawData, startByte, this.data, 0, contentLen);
305 		}
306 		else
307 		{
308 			ByteArrayOutputStream bos = new ByteArrayOutputStream();
309 			InflaterOutputStream ios = new InflaterOutputStream(bos);
310 			
311 			try
312 			{
313 				ios.write(rawData, startByte, contentLen);
314 				ios.flush();
315 				this.data = bos.toByteArray();
316 				
317 				ios.close();
318 				bos.close();
319 			}
320 			catch (IOException e)
321 			{
322 				throw new RuntimeException(e);
323 			}
324 		}
325 		
326 		// The number of bytes consumed. This allows multiple messages to be parsed from the same data block.
327 		return messageSize;
328 	}
329 	
330 	private int getBits(int src, int startBit, int endBit)
331 	{
332 		int mask = (int) (Math.pow(2, (endBit - startBit) + 1) - 1);
333 		mask = mask << (32 - endBit);
334 		int rv = (src & mask) >> (32 - endBit);
335 		return rv;
336 	}
337 	
338 	/* (non-Javadoc)
339 	 * @see com.ericsson.research.trap.impl.TrapMessage#getData()
340 	 */
341 	
342 	public byte[] getData()
343 	{
344 		return this.data;
345 	}
346 	
347 	/* (non-Javadoc)
348 	 * @see com.ericsson.research.trap.impl.TrapMessage#setData(byte[])
349 	 */
350 	
351 	public TrapMessage setData(byte[] data)
352 	{
353 		this.data = data;
354 		return this;
355 	}
356 	
357 	/* (non-Javadoc)
358 	 * @see com.ericsson.research.trap.impl.TrapMessage#getAuthData()
359 	 */
360 	
361 	public String getAuthData()
362 	{
363 		return this.authString;
364 	}
365 	
366 	/* (non-Javadoc)
367 	 * @see com.ericsson.research.trap.impl.TrapMessage#setAuthData(java.lang.String)
368 	 */
369 	
370 	public TrapMessage setAuthData(String authData)
371 	{
372 		if ((authData != null) && (authData.length() > 65535))
373 			throw new IllegalArgumentException("Cannot have an AuthString more than 65535 bytes");
374 		
375 		if (authData == null || authData.length() == 0)
376 			this.authString = null;
377 		else
378 			this.authString = authData;
379 		
380 		return this;
381 	}
382 	
383 	/* (non-Javadoc)
384 	 * @see com.ericsson.research.trap.impl.TrapMessage#getFormat()
385 	 */
386 	
387 	public Format getFormat()
388 	{
389 		return this.format;
390 	}
391 	
392 	/* (non-Javadoc)
393 	 * @see com.ericsson.research.trap.impl.TrapMessage#setFormat(com.ericsson.research.trap.impl.TrapMessageImpl.Format)
394 	 */
395 	
396 	public TrapMessage setFormat(Format format)
397 	{
398 		this.format = format;
399 		return this;
400 	}
401 	
402 	/* (non-Javadoc)
403 	 * @see com.ericsson.research.trap.impl.TrapMessage#getOp()
404 	 */
405 	
406 	public Operation getOp()
407 	{
408 		return this.op;
409 	}
410 	
411 	/* (non-Javadoc)
412 	 * @see com.ericsson.research.trap.impl.TrapMessage#setOp(com.ericsson.research.trap.impl.TrapMessageImpl.Operation)
413 	 */
414 	
415 	public TrapMessage setOp(Operation op)
416 	{
417 		this.op = op;
418 		return this;
419 	}
420 	
421 	public int getMessageId()
422 	{
423 		return this.messageId;
424 	}
425 	
426 	public TrapMessage setMessageId(int messageId)
427 	{
428 		this.messageId = messageId;
429 		return this;
430 	}
431 	
432 	public long length()
433 	{
434 		int l = HEADER_SIZE;
435 		if (this.authString != null)
436 			l += StringUtil.toUtfBytes(this.authString).length;
437 		
438 		if (this.getCompressedData() != null)
439 			l += this.getCompressedData().length;
440 		return l;
441 	}
442 	
443 	public String toString()
444 	{
445 		return this.getOp() + "/C" + this.getChannel() + "/" + this.getMessageId() + (this.data != null ? "/" + this.data.length : "");
446 	}
447 	
448 	public TrapMessage setCompressed(boolean isCompressed)
449 	{
450 		this.compressed = isCompressed;
451 		return this;
452 	}
453 	
454 	public boolean isCompressed()
455 	{
456 		return this.compressed;
457 	}
458 	
459 	public TrapMessage setChannel(int channelID)
460 	{
461 		if (channelID < 0 || channelID > 63)
462 			throw new IllegalArgumentException("Channel ID " + channelID + " outside the allowed range.");
463 		
464 		this.channelId = channelID;
465 		return this;
466 	}
467 	
468 	public int getChannel()
469 	{
470 		return this.getFormat() == Format.REGULAR ? this.channelId : 0;
471 	}
472 	
473 	public byte[] getCompressedData()
474 	{
475 		
476 		if (!this.isCompressed())
477 			return this.getData();
478 		
479 		try
480 		{
481 			ByteArrayOutputStream tos = new ByteArrayOutputStream();
482 			DeflaterOutputStream dos = new DeflaterOutputStream(tos);
483 			
484 			dos.write(this.getData());
485 			dos.finish();
486 			dos.flush();
487 			
488 			byte[] mData = tos.toByteArray();
489 			
490 			dos.close();
491 			tos.close();
492 			dos = null;
493 			tos = null;
494 			
495 			return mData;
496 		}
497 		catch (IOException e)
498 		{
499 			return this.getData();
500 		}
501 	}
502 }