/** * Copyright (c) 2011-2015 Exxeleron GmbH * * 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.exxeleron.qjava; import java.io.DataInputStream; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Provides deserialization from q IPC protocol. * <p> * Methods of {@link QReader} are not thread safe. * </p> */ public abstract class QReader { private final static String PROTOCOL_DEBUG_ENV = "QJAVA_PROTOCOL_DEBUG"; protected DataInputStream stream; protected ByteInputStream reader; private String encoding; protected byte[] header; protected byte[] rawData; /** * Sets the input stream for deserialization. * * @param stream * Input stream containing serialized messages */ void setStream( final DataInputStream stream ) { this.stream = stream; } /** * Sets the string encoding for deserialization. * * @param encoding * Encoding used for deserialization of string data */ void setEncoding( final String encoding ) { this.encoding = encoding; reader = new ByteInputStream(encoding, ByteOrder.nativeOrder()); } /** * Retrieves string encoding * * @return charset name */ protected String getEncoding() { return encoding; } /** * Reads next message from the stream and returns a deserialized object. * * @param raw * indicates whether reply should be parsed or return as raw data * @return {@link QMessage} instance encapsulating a deserialized message. * * @throws IOException * @throws QException */ public QMessage read( final boolean raw ) throws IOException, QException { header = new byte[8]; stream.readFully(header, 0, 8); reader.wrap(header); final ByteOrder endianess = reader.get() == 0 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; final QConnection.MessageType messageType = QConnection.MessageType.getMessageType(reader.get()); final boolean compressed = reader.get() == 1; reader.get(); // skip 1 byte reader.setOrder(endianess); final int messageSize = reader.getInt(); int dataSize = Math.max(messageSize - 8, 0); // read message rawData = new byte[dataSize]; stream.readFully(rawData, 0, dataSize); if ( raw ) { return new QMessage(rawData, messageType, endianess, compressed, raw, messageSize, dataSize); } byte[] data = rawData; if ( compressed ) { data = uncompress(rawData, endianess); dataSize = data.length; } reader.wrap(data); reader.setOrder(endianess); try { return new QMessage(readObject(), messageType, endianess, compressed, raw, messageSize, dataSize); } catch ( final QReaderException e ) { protocolDebug(e); throw e; } catch ( final RuntimeException e ) { protocolDebug(e); throw e; } } /** * Conditionally dumps IPC stream to file in case of exception while parsing. * * @param e * thrown exception */ protected void protocolDebug( final Exception e ) { if ( System.getenv().containsKey(PROTOCOL_DEBUG_ENV) ) { final String debugPath = System.getenv(PROTOCOL_DEBUG_ENV) + File.separator + PROTOCOL_DEBUG_ENV + "." + System.currentTimeMillis(); PrintWriter out = null; try { out = new PrintWriter(debugPath); out.write(Utils.getHex(header)); out.write(Utils.getHex(rawData)); out.write("\n"); e.printStackTrace(out); } catch ( final Exception ex ) { // ignore } finally { if ( out != null ) { try { out.close(); } catch ( final Exception ex ) { // ignore } } } } } /** * Uncompresses the IPC stream. * * @param compressedData * compressed data * @param endianess * endianess of the stream * @return uncompressed stream * @throws QException * in case of uncompression error */ protected byte[] uncompress( final byte[] compressedData, final ByteOrder endianess ) throws QException { // size of the uncompressed message is encoded on first 4 bytes // size has to be decreased by header length (8 bytes) final ByteBuffer byteBuffer = ByteBuffer.wrap(compressedData, 0, 4); byteBuffer.order(endianess); final int uncompressedSize = -8 + byteBuffer.getInt(); if ( uncompressedSize <= 0 ) { throw new QReaderException("Error while data uncompression."); } final byte[] uncompressed = new byte[uncompressedSize]; final int[] buffer = new int[256]; short i = 0; int n = 0, r = 0, f = 0, s = 0, p = 0, d = 4; while ( s < uncompressedSize ) { if ( i == 0 ) { f = 0xff & compressedData[d++]; i = 1; } if ( (f & i) != 0 ) { r = buffer[0xff & compressedData[d++]]; uncompressed[s++] = uncompressed[r++]; uncompressed[s++] = uncompressed[r++]; n = 0xff & compressedData[d++]; for ( int m = 0; m < n; m++ ) { uncompressed[s + m] = uncompressed[r + m]; } } else { uncompressed[s++] = compressedData[d++]; } while ( p < s - 1 ) { buffer[(0xff & uncompressed[p]) ^ (0xff & uncompressed[p + 1])] = p++; } if ( (f & i) != 0 ) { p = s += n; } i *= 2; if ( i == 256 ) { i = 0; } } return uncompressed; } /** * Parses a Java object from the IPC stream. * * @return parsed representation * @throws QException * in case of parsing error * @throws IOException * in case of IO error */ protected abstract Object readObject() throws QException, IOException; }