Java tutorial
/* * Copyright 2010-2011 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 com.googlecode.jcimd; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.LinkedList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * <p> * Serializes/deserializes CIMD packets in the following format: * </p> * <pre> * HEADER Parameter List TRAILER * <STX>ZZ:NNN<TAB>PPP:Parameter value<TAB>...CC<ETX> * ^ ^ ^ ^ ^ ^ ^ * | | | | | | | * | | | | | | +-- (end of text) * | | | | | +-- check sum (two bytes) * | | | | + parameter code (three bytes) * | | | | * | | | +-- delimiter * | | | * | | +-- packet number (three bytes) * | +-- operation code (two bytes) * +-- (start of text) * </pre> * <p> * The packet sequence number is generated using an optional * {@link PacketSequenceNumberGenerator}. If none is specified, then all packets * are expected to have sequence numbers. Otherwise, an exception is thrown. * <p> * The <STX>, <TAB>, ":" (COLON), and <ETX> bytes are * <em>not</em> part of the {@link Packet packet bean}. Instead, it is added upon * serialization by this class. * <p> * To protect against buffer overflow, this class uses a {@link #setMaxMessageSize(int) * maximum message size} which defaults to 4096 (1024 * 4) bytes. * * @author Lorenzo Dee * * @see #setSequenceNumberGenerator(PacketSequenceNumberGenerator) */ public class PacketSerializer { public static final byte STX = 0x02; public static final byte TAB = '\t'; // ASCII value is 9 public static final byte COLON = ':'; // ASCII value is 58 public static final byte ETX = 0x03; public static final byte NUL = 0x00; public static final int END_OF_STREAM = -1; private static final int DEFAULT_MAX_SIZE = 1024 * 4; private final Log logger; private static final Log clLogger = LogFactory.getLog(PacketSerializer.class); private final boolean useChecksum; private int maxMessageSize = DEFAULT_MAX_SIZE; private PacketSequenceNumberGenerator sequenceNumberGenerator; /** * Constructs a serializer that uses and expects a two-byte checksum. */ public PacketSerializer() { this(null, true); } /** * Constructs a serializer with the given name that uses * and expects a two-byte checksum. * @param name name of this serializer (used in logging) */ public PacketSerializer(String name) { this(name, true); } /** * Constructs a serializer with the given name that will use * two-byte checksum based on flag. * @param name name of this serializer (used in logging) * @param useChecksum flag to indicate to use checksum */ public PacketSerializer(String name, boolean useChecksum) { if (name != null) { this.logger = LogFactory.getLog(this.getClass().getName() + "." + name); } else { this.logger = LogFactory.getLog(this.getClass()); } this.useChecksum = useChecksum; } public int getMaxMessageSize() { return maxMessageSize; } public void setMaxMessageSize(int maxMessageSize) { if (maxMessageSize <= 0) { throw new IllegalArgumentException("maxMessageSize must be greater than zero"); } this.maxMessageSize = maxMessageSize; } public PacketSequenceNumberGenerator getSequenceNumberGenerator() { return sequenceNumberGenerator; } public void setSequenceNumberGenerator(PacketSequenceNumberGenerator sequenceNumberGenerator) { this.sequenceNumberGenerator = sequenceNumberGenerator; } public void serialize(Packet packet, OutputStream outputStream) throws IOException { doSerializePacket(packet, sequenceNumberGenerator, useChecksum, logger, outputStream); } public static void serializePacket(Packet packet, PacketSequenceNumberGenerator sequenceNumberGenerator, boolean useChecksum, OutputStream outputStream) throws IOException { doSerializePacket(packet, sequenceNumberGenerator, useChecksum, clLogger, outputStream); } private static void doSerializePacket(Packet packet, PacketSequenceNumberGenerator sequenceNumberGenerator, boolean useChecksum, Log logger, OutputStream outputStream) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Sending " + packet); } byte[] bytes = serializeToByteArray(packet, sequenceNumberGenerator, logger); outputStream.write(bytes); if (useChecksum) { int checkSum = calculateCheckSum(bytes); AsciiUtils.writeIntAsHexAsciiBytes(checkSum, outputStream, 2); } outputStream.write(ETX); } private static byte[] serializeToByteArray(Packet packet, PacketSequenceNumberGenerator sequenceNumberGenerator, Log logger) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(STX); AsciiUtils.writeIntAsAsciiBytes(packet.getOperationCode(), outputStream, 2); outputStream.write(COLON); Integer sequenceNumber = packet.getSequenceNumber(); if (sequenceNumber == null) { if (logger.isTraceEnabled()) { logger.trace("No sequence number in packet, generating one..."); } if (sequenceNumberGenerator != null) { sequenceNumber = sequenceNumberGenerator.nextSequence(); if (logger.isTraceEnabled()) { logger.trace("Generated " + sequenceNumber + " as sequence number"); } } else { String message = "No sequence number generator. " + "Please see PacketSerializer#setSequenceNumberGenerator(" + "PacketSequenceNumberGenerator)"; logger.error(message); throw new IOException(message); } } AsciiUtils.writeIntAsAsciiBytes(sequenceNumber, outputStream, 3); outputStream.write(TAB); for (Parameter parameter : packet.getParameters()) { AsciiUtils.writeIntAsAsciiBytes(parameter.getNumber(), outputStream, 3); outputStream.write(COLON); AsciiUtils.writeStringAsAsciiBytes(parameter.getValue(), outputStream); outputStream.write(TAB); } return outputStream.toByteArray(); } /** * Calculates the check sum of the given bytes. * * @param bytes the array from which a check sum is calculated * @return the check sum */ private static int calculateCheckSum(byte[] bytes) { return calculateCheckSum(bytes, 0, bytes.length); } /** * Calculates the check sum of the given range of bytes. * * @param bytes the array from which a check sum is calculated * @param from the initial index of the range to be copied, inclusive * @param to the final index of the range to be copied, exclusive. * (This index may lie outside the array.) * @return the check sum */ private static int calculateCheckSum(byte[] bytes, int from, int to) { int sum = 0; for (int i = from; i < to; i++) { sum += bytes[i]; sum &= 0xFF; } return sum; } public Packet deserialize(InputStream inputStream) throws IOException { return doDeserializePacket(inputStream, getMaxMessageSize(), useChecksum, logger); } public static Packet deserializePacket(InputStream inputStream, boolean useChecksum) throws IOException { return doDeserializePacket(inputStream, DEFAULT_MAX_SIZE, useChecksum, clLogger); } private static Packet doDeserializePacket(InputStream inputStream, int maxMessageSize, boolean useChecksum, Log logger) throws IOException { ByteArrayOutputStream temp = new ByteArrayOutputStream(); int b; while ((b = inputStream.read()) != END_OF_STREAM) { // Any data transmitted between packets SHALL be ignored. if (b == STX) { temp.write(b); break; } } if (b != STX) { //throw new SoftEndOfStreamException(); throw new IOException("End of stream reached and still no <STX> byte"); } // Read the input stream until ETX while ((b = inputStream.read()) != END_OF_STREAM) { temp.write(b); if (b == ETX) { break; } if (temp.size() >= maxMessageSize) { // Protect from buffer overflow throw new IOException( "Buffer overflow reached at " + temp.size() + " byte(s) and still no <ETX> byte"); } } if (b != ETX) { throw new IOException("End of stream reached and still no <ETX> byte"); } // Parse contents of "temp" (it contains the entire CIMD message // including STX and ETX bytes). byte bytes[] = temp.toByteArray(); if (logger.isTraceEnabled()) { logger.trace("Received " + bytes.length + " byte(s)"); } if (useChecksum) { // Read two (2) bytes, just before the ETX byte. StringBuilder buffer = new StringBuilder(2); buffer.append((char) bytes[bytes.length - 3]); buffer.append((char) bytes[bytes.length - 2]); try { int checksum = Integer.valueOf(buffer.toString(), 16); int expectedChecksum = calculateCheckSum(bytes, 0, bytes.length - 3); if (checksum != expectedChecksum) { throw new IOException("Checksum error: expecting " + expectedChecksum + " but got " + checksum); } } catch (NumberFormatException e) { throw new IOException("Checksum error: expecting HEX digits, but got " + buffer); } } // Deserialize bytes, minus STX, CC (check sum), and ETX. int end = useChecksum ? bytes.length - 3 : bytes.length - 1; Packet packet = deserializeFromByteArray(bytes, 1, end); if (logger.isDebugEnabled()) { logger.debug("Received " + packet); } return packet; } private static Packet deserializeFromByteArray(byte[] bytes, int from, int to) throws IOException { StringBuilder buffer = new StringBuilder(); int i = from; // Read the operation code and packet number i = readToBufferUntil(bytes, i, to, 2, buffer, COLON); int operationCode = Integer.valueOf(buffer.toString()); buffer.setLength(0); i = readToBufferUntil(bytes, i, to, -1, buffer, TAB); int sequenceNumber = Integer.valueOf(buffer.toString()); buffer.setLength(0); // Read the parameters List<Parameter> parameters = new LinkedList<Parameter>(); while (i < to) { i = readToBufferUntil(bytes, i, to, 3, buffer, COLON); int parameterType = Integer.valueOf(buffer.toString()); buffer.setLength(0); i = readToBufferUntil(bytes, i, to, -1, buffer, TAB); String parameterValue = buffer.toString(); buffer.setLength(0); parameters.add(new Parameter(parameterType, parameterValue)); } return new Packet(operationCode, sequenceNumber, parameters.toArray(new Parameter[0])); } /** * Reads bytes and appends to buffer until the delimiter is reached, * or the <em>to</em> is reached, or a reserved characters is reached. * The reserved characters 0x00 (NUL), 0x02 (STX), 0x03 (ETX), 0x09 * (TAB) are not allowed in any parameter. * * @param bytes the array of bytes to read * @param from the initial index of the range to be read, inclusive * @param to the final index of the range to be read, exclusive. * (This index may lie outside the array.) * @param maxOffset the maximum offset that can be read before reaching * delimiter. If this offset is exceeded, and no delimiter was * reached, an exception will be thrown. * @param buffer the buffer to append to * @param delimiter the delimiter to reach * @return the index (between <em>from</em> and <em>to</em>) * when the delimiter was reached * @throws IOException if a reserved character was reached, and it is * not the expected <em>delimiter</em>. */ private static int readToBufferUntil(byte[] bytes, int from, int to, int maxOffset, StringBuilder buffer, byte delimiter) throws IOException { int i = from; while ((i < to) && (bytes[i] != delimiter) // The reserved characters 0x00 (NUL), 0x02 (STX), // 0x03 (ETX), 0x09 (TAB) are not allowed in any parameter && (bytes[i] != NUL) && (bytes[i] != STX) && (bytes[i] != ETX) && (bytes[i] != TAB)) { buffer.append((char) bytes[i]); i++; if ((maxOffset > 0) && ((i - from) > maxOffset)) { throw new IOException("Expecting 0x" + Integer.toHexString(delimiter) + " within " + maxOffset + " byte(s), " + "but got 0x" + Integer.toHexString(bytes[i - 1])); } } if (bytes[i] != delimiter) { throw new IOException("Expecting 0x" + Integer.toHexString(delimiter) + " but got 0x" + Integer.toHexString(bytes[i])); } else { i++; } return i; } }