001    /*
002     * SeekableByteArrayOutputStream
003     * 
004     * Copyright (c) 2002 Marco Schmidt.
005     * All rights reserved.
006     */
007    
008    package net.sourceforge.jiu.util;
009    
010    import java.io.IOException;
011    import java.io.OutputStream;
012    
013    /**
014     * An extension of {@link java.io.OutputStream} that writes data to an internal
015     * byte array, resizing it when necessary.
016     * Similar to {@link java.io.ByteArrayOutputStream}, but also enables seeking and truncating.
017     *
018     * @author Marco Schmidt
019     * @since 0.10.0
020     */
021    public class SeekableByteArrayOutputStream extends OutputStream
022    {
023            private byte[] buffer;
024            private boolean closed;
025            private int incrementStep;
026            private int offset;
027            private int size;
028    
029            /**
030             * Creates a new object of this class, setting initial capacity and increment size
031             * to default values.
032             */
033            public SeekableByteArrayOutputStream()
034            {
035                    this(1024, 1024);
036            }
037    
038            /**
039             * Creates a new object of this class, setting initial capacity to the argument 
040             * value.
041             * The increment size is set to the initial capacity as well if that value is larger
042             * than 0.
043             * Otherwise it is set to a default value.
044             * @param initialCapacity the number of bytes that are allocated in this constructor (0 or larger)
045             */
046            public SeekableByteArrayOutputStream(int initialCapacity)
047            {
048                    this(initialCapacity, initialCapacity == 0 ? 1024 : initialCapacity);
049            }
050    
051            /**
052             * Creates a new object of this class, setting initial capacity and increment 
053             * to the argument values.
054             * @param initialCapacity the number of bytes that are allocated in this constructor (0 or larger)
055             * @param increment the number of bytes by which the internal byte array is increased if it is full (1 or larger)
056             */
057            public SeekableByteArrayOutputStream(int initialCapacity, int increment)
058            {
059                    if (initialCapacity < 0)
060                    {
061                            throw new IllegalArgumentException("Value for initial capacity must not be negative.");
062                    }
063                    if (increment < 1)
064                    {
065                            throw new IllegalArgumentException("Value for increment must be 1 or larger.");
066                    }
067                    buffer = new byte[initialCapacity];
068                    incrementStep = increment;
069                    offset = 0;
070            }
071    
072            /**
073             * Closes this output stream.
074             * After a call to this method, all write attempts will result in an exception.
075             */
076            public void close() throws IOException
077            {
078                    closed = true;
079            }
080    
081            private void ensureSpace(int numBytes) throws IOException
082            {
083                    if (closed)
084                    {
085                            throw new IOException("Stream was closed already. Cannot write to closed stream.");
086                    }
087                    if (numBytes < 0)
088                    {
089                            throw new IllegalArgumentException("Cannot write negative number of bytes (" + numBytes + ").");
090                    }
091                    if (buffer.length - offset < numBytes)
092                    {
093                            increaseBuffer(Math.max(buffer.length + incrementStep, offset + numBytes));
094                    }
095            }
096    
097            /**
098             * Returns the current offset in the output stream.
099             * Larger than or equal to 0 and smaller than or equal to {@link #getSize}.
100             * @return current position in the output stream, 0-based
101             */
102            public int getPosition()
103            {
104                    return offset;
105            }
106    
107            /**
108             * Returns the current size of the output stream.
109             * @return size of the output stream in bytes (0 or larger)
110             */
111            public int getSize()
112            {
113                    return size;
114            }
115    
116            private void increaseBuffer(int newLength)
117            {
118                    if (newLength <= buffer.length)
119                    {
120                            return;
121                    }
122                    byte[] temp = new byte[newLength];
123                    System.arraycopy(buffer, 0, temp, 0, offset);
124                    buffer = temp;
125            }
126    
127            /**
128             * Sets the current position in the output stream to the argument.
129             * @param newOffset new offset into the file, must be >= 0 and <= {@link #getSize}
130             * @throws IOException if the argument is invalid
131             */
132            public void seek(int newOffset) throws IOException
133            {
134                    if (newOffset < 0)
135                    {
136                            throw new IOException("Cannot seek to negative offset (" + newOffset + ").");
137                    }
138                    if (newOffset > size)
139                    {
140                            throw new IOException("Cannot seek to offset " + newOffset + ", stream has only " + size + " byte(s).");
141                    }
142                    offset = newOffset;
143            }
144    
145            /**
146             * Allocates a new <code>byte[]</code> object, copies {@link #getSize} bytes
147             * from the internal byte array to that new array and returns the array.
148             * @return a copy of the byte[] data stored internally
149             */
150            public byte[] toByteArray()
151            {
152                    byte[] result = new byte[size];
153                    System.arraycopy(buffer, 0, result, 0, size);
154                    return result;
155            }
156    
157            /**
158             * Removes all bytes after the current position.
159             * After a call to this method, {@link #getSize} is equal to {@link #getPosition}.
160             */
161            public void truncate()
162            {
163                    size = offset;
164            }
165    
166            /**
167             * Writes the least significant eight bits of the argument <code>int</code> to the internal array.
168             * @param b int variable that stores the byte value to be written
169             */
170            public void write(int b) throws IOException
171            {
172                    ensureSpace(1);
173                    buffer[offset++] = (byte)b;
174                    if (offset > size)
175                    {
176                            size = offset;
177                    }
178            }
179            
180            /**
181             * Write the complete argument array to this stream.
182             * Copies the data to the internal byte array.
183             * Simply calls <code>write(data, 0, data.length);</code>.
184             * @param data array to be copied to this stream
185             */
186            public void write(byte[] data) throws IOException
187            {
188                    write(data, 0, data.length);
189            }
190    
191            /**
192             * Write some bytes from the argument array to this stream.
193             * Copies num bytes starting at src[srcOffset] to this stream.
194             * @param src the array from which data is copied
195             * @param srcOffset int index into that array pointing to the first byte to be copied
196             * @param num number of bytes to be copied
197             */
198            public void write(byte[] src, int srcOffset, int num) throws IOException
199            {
200                    ensureSpace(num);
201                    System.arraycopy(src, srcOffset, buffer, offset, num);
202                    offset += num;
203                    if (offset > size)
204                    {
205                            size = offset;
206                    }
207            }
208    
209            /** 
210             * Writes the bytes in the internal byte array to the argument output stream.
211             * A call to this method has the same effect as 
212             * <pre>
213             * byte[] copy = toByteArray();
214             * out.write(copy, 0, copy.length);
215             * </pre>
216             * However, you with this method you save the allocation of an additional byte array
217             * and the copying to that new array.
218             * @param out the output stream to which this stream's content is copied
219             * @throws IOException if out has a problem writing the bytes
220             */
221            public void writeTo(OutputStream out) throws IOException
222            {
223                    out.write(buffer, 0, size);
224            }
225    }