/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.integration.ip.tcp.serializer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Reads data in an InputStream to a byte[]; data must be preceded by * a binary length (network byte order, not included in resulting byte[]). * * Writes a byte[] to an OutputStream after a binary length. * The length field contains the length of data following the length * field. (network byte order). * * The default length field is a 4 byte signed integer. During deserialization, * negative values will be rejected. * Other options are an unsigned byte, and unsigned short. * * For other header formats, override {@link #readHeader(InputStream)} and * {@link #writeHeader(OutputStream, int)}. * * @author Gary Russell * @since 2.0 */ public class ByteArrayLengthHeaderSerializer extends AbstractByteArraySerializer { /** * Default length-header field, allows for data up to 2**31-1 bytes. */ public static final int HEADER_SIZE_INT = 4; // default /** * A single unsigned byte, for data up to 255 bytes. */ public static final int HEADER_SIZE_UNSIGNED_BYTE = 1; /** * An unsigned short, for data up to 2**16 bytes. */ public static final int HEADER_SIZE_UNSIGNED_SHORT = 2; private final int headerSize; private final Log logger = LogFactory.getLog(this.getClass()); /** * Constructs the serializer using {@link #HEADER_SIZE_INT} */ public ByteArrayLengthHeaderSerializer() { this(HEADER_SIZE_INT); } /** * Constructs the serializer using the supplied header size. * Valid header sizes are {@link #HEADER_SIZE_INT} (default), * {@link #HEADER_SIZE_UNSIGNED_BYTE} and {@link #HEADER_SIZE_UNSIGNED_SHORT} * @param headerSize The header size. */ public ByteArrayLengthHeaderSerializer(int headerSize) { if (headerSize != HEADER_SIZE_INT && headerSize != HEADER_SIZE_UNSIGNED_BYTE && headerSize != HEADER_SIZE_UNSIGNED_SHORT) { throw new IllegalArgumentException("Illegal header size:" + headerSize); } this.headerSize = headerSize; } /** * Reads the header from the stream and then reads the provided length * from the stream and returns the data in a byte[]. Throws an * IOException if the length field exceeds the maxMessageSize. * Throws a {@link SoftEndOfStreamException} if the stream * is closed between messages. * * @param inputStream The input stream. * @throws IOException Any IOException. */ @Override public byte[] deserialize(InputStream inputStream) throws IOException { int messageLength = this.readHeader(inputStream); if (this.logger.isDebugEnabled()) { this.logger.debug("Message length is " + messageLength); } byte[] messagePart = null; try { if (messageLength > this.maxMessageSize) { throw new IOException("Message length " + messageLength + " exceeds max message length: " + this.maxMessageSize); } messagePart = new byte[messageLength]; read(inputStream, messagePart, false); return messagePart; } catch (IOException e) { publishEvent(e, messagePart, -1); throw e; } catch (RuntimeException e) { publishEvent(e, messagePart, -1); throw e; } } /** * Writes the byte[] to the output stream, preceded by a 4 byte * length in network byte order (big endian). * * @param bytes The bytes. * @param outputStream The output stream. */ @Override public void serialize(byte[] bytes, OutputStream outputStream) throws IOException { this.writeHeader(outputStream, bytes.length); outputStream.write(bytes); } /** * Reads data from the socket and puts the data in buffer. Blocks until * buffer is full or a socket timeout occurs. * * @param inputStream The input stream. * @param buffer the buffer into which the data should be read * @param header true if we are reading the header * @return {@code < 0} if socket closed and not in the middle of a message * @throws IOException Any IOException. */ protected int read(InputStream inputStream, byte[] buffer, boolean header) throws IOException { int lengthRead = 0; int needed = buffer.length; while (lengthRead < needed) { int len; len = inputStream.read(buffer, lengthRead, needed - lengthRead); if (len < 0 && header && lengthRead == 0) { return len; } if (len < 0) { throw new IOException("Stream closed after " + lengthRead + " of " + needed); } lengthRead += len; if (this.logger.isDebugEnabled()) { this.logger.debug("Read " + len + " bytes, buffer is now at " + lengthRead + " of " + needed); } } return 0; } /** * Writes the header, according to the header format. * @param outputStream The output stream. * @param length The length. * @throws IOException Any IOException. */ protected void writeHeader(OutputStream outputStream, int length) throws IOException { ByteBuffer lengthPart = ByteBuffer.allocate(this.headerSize); switch (this.headerSize) { case HEADER_SIZE_INT: lengthPart.putInt(length); break; case HEADER_SIZE_UNSIGNED_BYTE: if (length > 0xff) { throw new IllegalArgumentException("Length header:" + this.headerSize + " too short to accommodate message length:" + length); } lengthPart.put((byte) length); break; case HEADER_SIZE_UNSIGNED_SHORT: if (length > 0xffff) { throw new IllegalArgumentException("Length header:" + this.headerSize + " too short to accommodate message length:" + length); } lengthPart.putShort((short) length); break; default: throw new IllegalArgumentException("Bad header size:" + this.headerSize); } outputStream.write(lengthPart.array()); } /** * Reads the header and returns the length of the data part. * * @param inputStream The input stream. * @return The length of the data part. * @throws IOException Any IOException. * @throws SoftEndOfStreamException if socket closes * before any length data read. */ protected int readHeader(InputStream inputStream) throws IOException { byte[] lengthPart = new byte[this.headerSize]; try { int status = read(inputStream, lengthPart, true); if (status < 0) { throw new SoftEndOfStreamException("Stream closed between payloads"); } int messageLength; switch (this.headerSize) { case HEADER_SIZE_INT: messageLength = ByteBuffer.wrap(lengthPart).getInt(); if (messageLength < 0) { throw new IllegalArgumentException("Length header:" + messageLength + " is negative"); } break; case HEADER_SIZE_UNSIGNED_BYTE: messageLength = ByteBuffer.wrap(lengthPart).get() & 0xff; break; case HEADER_SIZE_UNSIGNED_SHORT: messageLength = ByteBuffer.wrap(lengthPart).getShort() & 0xffff; break; default: throw new IllegalArgumentException("Bad header size:" + this.headerSize); } return messageLength; } catch (SoftEndOfStreamException e) { throw e; } catch (IOException e) { publishEvent(e, lengthPart, -1); throw e; } catch (RuntimeException e) { publishEvent(e, lengthPart, -1); throw e; } } }