All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jibx.ws.encoding.dime.DimeOutputBuffer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2007-2009, Sosnoski Software Associates Limited. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
 * disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 * following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of
 * JiBX nor the names of its contributors may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jibx.ws.encoding.dime;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jibx.runtime.impl.IOutByteBuffer;

/**
 * Byte buffer for output using DIME encoding. DIME messages are sent as a sequence of one or more parts. Each part is
 * sent as one or more chunks. Each chunk starts with a header giving the length and blocking flags, and potentially
 * other information such as content type and message identifier. In this implementation, each buffer of output data is
 * written as a separate record, which allows some simplifications; in particular, space for the record header can
 * always be set aside at the start of the buffer, with the header details filled in once the buffer has been filled and
 * is ready for output. Parts must be fully written before moving on to the next part (using {@link #flush()}), and
 * messages must be fully written (using {@link #finish()}) before moving on to the next message. Information about each
 * part must be configured (using the {@link #nextPart(String, int, String)} method) before output of data for the part
 * begins.
 * 
 * @author Dennis M. Sosnoski
 */
public class DimeOutputBuffer implements IOutByteBuffer
{
    private static final Log s_logger = LogFactory.getLog(DimeOutputBuffer.class);
    
    /** Byte buffer used for output. */
    private IOutByteBuffer m_byteBuffer;
    
    /** Cached reference to byte array used by buffer. */
    private byte[] m_buffer;
    
    /** Number of bytes in current header. This is only used for logging purposes. */
    private int m_headerSize;
    
    /** Base offset for start of buffer data (actual start of header). */
    private int m_base;
    
    /** Current offset for adding bytes to buffer. */
    private int m_offset;
    
    /** Message state. */
    private int m_messageState;
    
    /** Message ended flag. This flag is set by {@link #endMessage()} and checked by {@link #flush()}, to avoid the need
     for writing a separate header for the end of the message. */
    private boolean m_ended;
    
    /**
     * Constructor.
     */
    public DimeOutputBuffer() {
        m_messageState = DimeCommon.MESSAGE_END;
    }
    
    /**
     * Set the byte buffer. If a different buffer was set previously that buffer is closed, with any errors ignored.
     *
     * @param buff buffer
     */
    public void setBuffer(IOutByteBuffer buff) {
        if (m_byteBuffer != null && m_byteBuffer != buff) {
            try {
                m_byteBuffer.finish();
            } catch (IOException e) { /* nothing to be done */ }
        }
        m_byteBuffer = buff;
        m_buffer = buff.getBuffer();
        m_offset = m_base = buff.getOffset();
        if (s_logger.isDebugEnabled()) {
            s_logger.debug("Set buffer to instance of " + buff.getClass().getName() + " with base " + m_offset);
        }
    }
    
    /**
     * Initialize output for next message. This must be called after the last message is completed, but before starting
     * the first part in the next message. It must be followed by a call to {@link #nextPart(String, int, String)}
     * before actually writing any data.
     */
    public void nextMessage() {
        if (m_messageState == DimeCommon.MESSAGE_END) {
            m_messageState = DimeCommon.MESSAGE_START;
            m_ended = false;
            s_logger.debug("Advanced to next message");
        } else {
            throw new IllegalStateException("Internal error - cannot start message until previous message finished");
        }
    }
    
    /**
     * Initialize output for next message part. This must be called after the last part is completed, but before
     * starting any output of data for the next part.
     * 
     * @param id part identifier (null if none)
     * @param typecode code for type of type information (values from {@link DimeCommon})
     * @param type type text (null if none)
     */
    public void nextPart(String id, int typecode, String type) {
        if (m_messageState == DimeCommon.MESSAGE_END) {
            throw new IllegalStateException("Internal error - cannot start part after message finished");
        } else if (m_messageState == DimeCommon.MESSAGE_CHUNK) {
            throw new IllegalStateException("Internal error - cannot start part until last record finished");
        } else {
            m_offset = initFirstHeader(id, typecode, type);
            s_logger.debug("Advanced to next record");
        }
    }
    
    /**
     * Fill a short value at a specified offset.
     * 
     * @param offset
     * @param value
     */
    private void fillShort(int offset, int value) {
        m_buffer[offset + 1] = (byte)value;
        m_buffer[offset] = (byte)(value >> 8);
    }
    
    /**
     * Fill a string value (as UTF-8 bytes) at a specified offset.
     * 
     * @param offset
     * @param text
     * @return offset past end of string value
     */
    private int fillString(int offset, String text) {
        if (text != null && text.length() > 0) {
            try {
                byte[] byts = text.getBytes("UTF-8");
                System.arraycopy(byts, 0, m_buffer, offset, byts.length);
                offset += byts.length;
            } catch (UnsupportedEncodingException e) {
                /* nothing to be done */
            }
        }
        return offset;
    }
    
    /**
     * Fill zero padding bytes to a multiple of four bytes.
     * 
     * @param offset
     * @return offset after padding added
     */
    private int fillPadding(int offset) {
        while ((offset - m_base & 3) != 0) {
            m_buffer[offset++] = 0;
        }
        return offset;
    }
    
    /**
     * Initialize the DIME record header at the start of the buffer for a new record. This sets the fill offset to the
     * rounded boundary following the end of the header. It must be called before any output is stored to the buffer.
     * 
     * @param id part identifier (null if none)
     * @param typecode code for type of type information (values from {@link DimeCommon})
     * @param type type text (null if none)
     * @return data start offset, following the header
     */
    private int initFirstHeader(String id, int typecode, String type) {
        int offset = m_base + DimeCommon.HEADER_SIZE;
        switch (m_messageState) {
            case DimeCommon.MESSAGE_START:
            case DimeCommon.MESSAGE_MIDDLE:
                
                // build the header with variable-length fields
                m_buffer[m_base + 1] = (byte)typecode;
                int fill = fillString(offset, id);
                fillShort(m_base + 4, fill - offset);
                offset = fillPadding(fill);
                fill = fillString(offset, type);
                fillShort(m_base + 6, fill - offset);
                offset = fillPadding(fill);
                break;
                
            default:
                throw new IllegalStateException("Internal error - not in valid header state");
                
        }
        m_headerSize = offset;
        if (s_logger.isDebugEnabled()) {
            s_logger.debug("Initialized new record header ending at offset " + offset + " with id " + id
                + ", typecode " + typecode + ", type " + type);
        }
        return offset;
    }
    
    /**
     * Initialize the DIME record header at the start of the buffer for a non-initial record chunk. This sets the fill
     * offset to the rounded boundary following the end of the header. It must be called before any output is stored to
     * the buffer.
     * 
     * @return data start offset, following the header
     */
    private int initFollowHeader() {
        for (int i = 1; i <  DimeCommon.HEADER_SIZE; i++) {
            m_buffer[m_base + i] = 0;
        }
        m_headerSize = m_base + DimeCommon.HEADER_SIZE;
        int offset = m_base + m_headerSize;
        if (s_logger.isDebugEnabled()) {
            s_logger.debug("Initialized chunk record header ending at offset " + offset);
        }
        return offset;
    }
    
    /**
     * Finish the DIME record header. This fills in the final details for the previously-initialized header.
     * 
     * @param length number of bytes in block
     * @param last final block flag
     * @param end final part in message flag (ignored unless last is true)
     */
    private void finishHeader(int length, boolean last, boolean end) {
        
        // configure the flags in first byte of header
        int byt = DimeCommon.VERSION_VALUE;
        if (m_messageState == DimeCommon.MESSAGE_START) {
            byt |= DimeCommon.MESSAGE_BEGIN_FLAG;
            m_messageState = DimeCommon.MESSAGE_MIDDLE;
        } else if (m_messageState == DimeCommon.MESSAGE_END) {
            throw new IllegalStateException("Internal error - cannot write after message finished");
        }
        if (last) {
            if (end) {
                byt |= DimeCommon.MESSAGE_END_FLAG;
            }
        } else {
            byt |= DimeCommon.CHUNK_FLAG;
        }
        m_buffer[m_base] = (byte)byt;
        
        // fill in the record size
        fillShort(m_base + 8, length >> 16);
        fillShort(m_base + 10, length);
        if (s_logger.isDebugEnabled()) {
            s_logger.debug("Finished header:" + DimeCommon.dumpBytes(m_buffer, 0, m_headerSize));
        }
        m_headerSize = 0;
    }
    
    /**
     * Format current buffer data as a record chunk. This finishes the header for the data and updates the buffer offset
     * to reflect the data and padding, but does not actually force the data out.
     * 
     * @param last final chunk flag
     * @param end final part in message flag (ignored unless last is true)
     * @throws IOException
     */
    private void createChunk(boolean last, boolean end) throws IOException {
        
        // handle writing the block
        int length = m_offset - m_base;
        finishHeader(length - m_headerSize, last, end);
        int total = fillPadding(length);
        m_offset = m_base + total;
        m_byteBuffer.setOffset(m_offset);
        if (s_logger.isDebugEnabled()) {
            s_logger.debug("Created chunk of length " + total);
        }
        
        // handle message state changes
        if (last) {
            if (end) {
                m_messageState = DimeCommon.MESSAGE_END;
            } else {
                m_messageState = DimeCommon.MESSAGE_MIDDLE;
            }
        } else {
            m_messageState = DimeCommon.MESSAGE_CHUNK;
        }
    }
    
    /**
     * End the current message. This sets the current data as the final block of a message. A call to {@link #flush()}
     * is needed following this call to actually write the data and flush the output.
     * 
     * @throws IOException on I/O error
     */
    public void endMessage() throws IOException {
        s_logger.debug("Ending message");
        m_ended = true;
    }
    
    //
    // IOutByteBuffer implementation
    
    /**
     * Get the byte array buffer.
     * 
     * @return array
     */
    public byte[] getBuffer() {
        return m_buffer;
    }
    
    /**
     * Get the index of the next byte to be written. After writing data, the {@link #setOffset(int)} method must be used
     * to update the current offset before any other operations are performed on the buffer.
     * 
     * @return offset
     */
    public int getOffset() {
        return m_offset;
    }
    
    /**
     * Set the current offset. This must be used to update the stored buffer state after reading any data.
     * 
     * @param offset offset
     */
    public void setOffset(int offset) {
        m_offset = offset;
    }
    
    /**
     * Free at least some number of bytes of space in the byte array.
     * 
     * @param reserve offset of data to be preserved in buffer (nothing preserved if greater than or equal to current
     * offset)
     * @param size desired number of bytes
     * @throws IOException on I/O error
     */
    public void free(int reserve, int size) throws IOException {
        if (m_buffer.length - m_offset < size) {
            int adjsize = (size + DimeCommon.HEADER_SIZE + 3) & -4;
            if (reserve >= m_offset) {
                
                // no reserve, just write the existing data as a record and verify space available
                if (s_logger.isDebugEnabled()) {
                    s_logger.debug("Creating chunk of size " + size);
                }
                createChunk(false, false);
                m_byteBuffer.free(m_offset, adjsize);
                m_buffer = m_byteBuffer.getBuffer();
                m_base = m_byteBuffer.getOffset();
                m_offset = initFollowHeader();
                
            } else if (reserve > DimeCommon.DEFAULT_BUFFER_SIZE / 4) {
                
                // data to be preserved, but enough going to be worth writing as separate block
                if (s_logger.isDebugEnabled()) {
                    s_logger.debug("Copying to free " + size + " with offset " + m_offset + " and reserve " + reserve);
                }
                int keep = m_offset - reserve;
                byte[] byts = new byte[keep];
                System.arraycopy(m_buffer, reserve, byts, 0, keep);
                m_offset = reserve;
                createChunk(false, false);
                m_byteBuffer.free(m_offset, adjsize);
                m_buffer = m_byteBuffer.getBuffer();
                m_base = m_byteBuffer.getOffset();
                m_offset = initFollowHeader();
                System.arraycopy(byts, 0, m_buffer, m_offset, keep);
                m_offset += keep;
                
            } else {
                
                // data to be preserved, but not enough to write - just force resize
                if (s_logger.isDebugEnabled()) {
                    s_logger.debug("Resizing to free " + size + " with offset " + m_offset + " and reserve " + reserve);
                }
                m_byteBuffer.setOffset(m_offset);
                m_byteBuffer.free(0, adjsize);
                m_buffer = m_byteBuffer.getBuffer();
                
            }
        }
    }
    
    /**
     * Empty the buffer. Writes all data from the buffer as the final chunk of a record.
     * 
     * @throws IOException on I/O error
     */
    public void flush() throws IOException {
        s_logger.debug("Flushing output");
        if (m_messageState != DimeCommon.MESSAGE_END) {
            if (m_headerSize == 0) {
                nextPart(null, DimeCommon.TYPE_NONE, null);
            }
            createChunk(true, m_ended);
        }
        m_byteBuffer.flush();
    }
    
    /**
     * Complete usage of the current buffer. This method should be called whenever the application is done writing to
     * the buffer. It writes the current data as the final block of a message.
     * 
     * @throws IOException on I/O error
     */
    public void finish() throws IOException {
        endMessage();
        flush();
        m_byteBuffer.finish();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy