Index: lams_central/src/java/org/lamsfoundation/lams/util/BufferOverflowException.java =================================================================== RCS file: /usr/local/cvsroot/lams_central/src/java/org/lamsfoundation/lams/util/BufferOverflowException.java,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lams_central/src/java/org/lamsfoundation/lams/util/BufferOverflowException.java 10 Nov 2008 13:59:14 -0000 1.1 @@ -0,0 +1,79 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id: BufferOverflowException.java,v 1.1 2008/11/10 13:59:14 andreyb Exp $ */ +package org.lamsfoundation.lams.util; + +/* + * Buffer Overflow Exception + * Copyright (C) 2002 Stephen Ostermiller + * http://ostermiller.org/contact.pl?regarding=Java+Utilities + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * See COPYING.TXT for details. + */ + +import java.io.IOException; + +/** + * An indication that there was a buffer overflow. + * + * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities + * @since ostermillerutils 1.00.00 + */ +public class BufferOverflowException extends IOException { + + /** + * Serial version ID + */ + private static final long serialVersionUID = -322401823167626048L; + + /** + * Create a new Exception + * + * @since ostermillerutils 1.00.00 + */ + public BufferOverflowException(){ + super(); + } + + /** + * Create a new Exception with the given message. + * + * @param msg Error message. + * + * @since ostermillerutils 1.00.00 + */ + public BufferOverflowException(String msg){ + super(msg); + } +} + \ No newline at end of file Index: lams_central/src/java/org/lamsfoundation/lams/util/CircularByteBuffer.java =================================================================== RCS file: /usr/local/cvsroot/lams_central/src/java/org/lamsfoundation/lams/util/CircularByteBuffer.java,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lams_central/src/java/org/lamsfoundation/lams/util/CircularByteBuffer.java 10 Nov 2008 13:59:14 -0000 1.1 @@ -0,0 +1,837 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id: CircularByteBuffer.java,v 1.1 2008/11/10 13:59:14 andreyb Exp $ */ +package org.lamsfoundation.lams.util; + +/* + * Circular Byte Buffer + * Copyright (C) 2002 Stephen Ostermiller + * http://ostermiller.org/contact.pl?regarding=Java+Utilities + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * See COPYING.TXT for details. + */ + + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Implements the Circular Buffer producer/consumer model for bytes. + * More information about this class is available from ostermiller.org. + *
+ * Using this class is a simpler alternative to using a PipedInputStream + * and a PipedOutputStream. PipedInputStreams and PipedOutputStreams don't support the + * mark operation, don't allow you to control buffer sizes that they use, + * and have a more complicated API that requires instantiating two + * classes and connecting them. + *
+ * This class is thread safe. + * + * @see CircularCharBuffer + * @see CircularObjectBuffer + * + * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities + * @since ostermillerutils 1.00.00 + */ +public class CircularByteBuffer { + + /** + * The default size for a circular byte buffer. + * + * @since ostermillerutils 1.00.00 + */ + private final static int DEFAULT_SIZE = 1024; + + /** + * A buffer that will grow as things are added. + * + * @since ostermillerutils 1.00.00 + */ + public final static int INFINITE_SIZE = -1; + + /** + * The circular buffer. + *
+ * The actual capacity of the buffer is one less than the actual length + * of the buffer so that an empty and a full buffer can be + * distinguished. An empty buffer will have the markPostion and the + * writePosition equal to each other. A full buffer will have + * the writePosition one less than the markPostion. + *
+ * There are three important indexes into the buffer: + * The readPosition, the writePosition, and the markPosition. + * If the InputStream has never been marked, the readPosition and + * the markPosition should always be the same. The bytes + * available to be read go from the readPosition to the writePosition, + * wrapping around the end of the buffer. The space available for writing + * goes from the write position to one less than the markPosition, + * wrapping around the end of the buffer. The bytes that have + * been saved to support a reset() of the InputStream go from markPosition + * to readPosition, wrapping around the end of the buffer. + * + * @since ostermillerutils 1.00.00 + */ + protected byte[] buffer; + /** + * Index of the first byte available to be read. + * + * @since ostermillerutils 1.00.00 + */ + protected volatile int readPosition = 0; + /** + * Index of the first byte available to be written. + * + * @since ostermillerutils 1.00.00 + */ + protected volatile int writePosition = 0; + /** + * Index of the first saved byte. (To support stream marking.) + * + * @since ostermillerutils 1.00.00 + */ + protected volatile int markPosition = 0; + /** + * Number of bytes that have to be saved + * to support mark() and reset() on the InputStream. + * + * @since ostermillerutils 1.00.00 + */ + protected volatile int markSize = 0; + /** + * If this buffer is infinite (should resize itself when full) + * + * @since ostermillerutils 1.00.00 + */ + protected volatile boolean infinite = false; + /** + * True if a write to a full buffer should block until the buffer + * has room, false if the write method should throw an IOException + * + * @since ostermillerutils 1.00.00 + */ + protected boolean blockingWrite = true; + /** + * The InputStream that can empty this buffer. + * + * @since ostermillerutils 1.00.00 + */ + protected InputStream in = new CircularByteBufferInputStream(); + /** + * true if the close() method has been called on the InputStream + * + * @since ostermillerutils 1.00.00 + */ + protected boolean inputStreamClosed = false; + /** + * The OutputStream that can fill this buffer. + * + * @since ostermillerutils 1.00.00 + */ + protected OutputStream out = new CircularByteBufferOutputStream(); + /** + * true if the close() method has been called on the OutputStream + * + * @since ostermillerutils 1.00.00 + */ + protected boolean outputStreamClosed = false; + + /** + * Make this buffer ready for reuse. The contents of the buffer + * will be cleared and the streams associated with this buffer + * will be reopened if they had been closed. + * + * @since ostermillerutils 1.00.00 + */ + public void clear(){ + synchronized (this){ + readPosition = 0; + writePosition = 0; + markPosition = 0; + outputStreamClosed = false; + inputStreamClosed = false; + } + } + + /** + * Retrieve a OutputStream that can be used to fill + * this buffer. + *
+ * Write methods may throw a BufferOverflowException if + * the buffer is not large enough. A large enough buffer + * size must be chosen so that this does not happen or + * the caller must be prepared to catch the exception and + * try again once part of the buffer has been consumed. + * + * + * @return the producer for this buffer. + * + * @since ostermillerutils 1.00.00 + */ + public OutputStream getOutputStream(){ + return out; + } + + /** + * Retrieve a InputStream that can be used to empty + * this buffer. + *
+ * This InputStream supports marks at the expense + * of the buffer size. + * + * @return the consumer for this buffer. + * + * @since ostermillerutils 1.00.00 + */ + public InputStream getInputStream(){ + return in; + } + + /** + * Get number of bytes that are available to be read. + *
+ * Note that the number of bytes available plus + * the number of bytes free may not add up to the + * capacity of this buffer, as the buffer may reserve some + * space for other purposes. + * + * @return the size in bytes of this buffer + * + * @since ostermillerutils 1.00.00 + */ + public int getAvailable(){ + synchronized (this){ + return available(); + } + } + + /** + * Get the number of bytes this buffer has free for + * writing. + *
+ * Note that the number of bytes available plus + * the number of bytes free may not add up to the + * capacity of this buffer, as the buffer may reserve some + * space for other purposes. + * + * @return the available space in bytes of this buffer + * + * @since ostermillerutils 1.00.00 + */ + public int getSpaceLeft(){ + synchronized (this){ + return spaceLeft(); + } + } + + /** + * Get the capacity of this buffer. + *
+ * Note that the number of bytes available plus + * the number of bytes free may not add up to the + * capacity of this buffer, as the buffer may reserve some + * space for other purposes. + * + * @return the size in bytes of this buffer + * + * @since ostermillerutils 1.00.00 + */ + public int getSize(){ + synchronized (this){ + return buffer.length; + } + } + + /** + * double the size of the buffer + * + * @since ostermillerutils 1.00.00 + */ + private void resize(){ + byte[] newBuffer = new byte[buffer.length * 2]; + int marked = marked(); + int available = available(); + if (markPosition <= writePosition){ + // any space between the mark and + // the first write needs to be saved. + // In this case it is all in one piece. + int length = writePosition - markPosition; + System.arraycopy(buffer, markPosition, newBuffer, 0, length); + } else { + int length1 = buffer.length - markPosition; + System.arraycopy(buffer, markPosition, newBuffer, 0, length1); + int length2 = writePosition; + System.arraycopy(buffer, 0, newBuffer, length1, length2); + } + buffer = newBuffer; + markPosition = 0; + readPosition = marked; + writePosition = marked + available; + } + + /** + * Space available in the buffer which can be written. + * + * @since ostermillerutils 1.00.00 + */ + private int spaceLeft(){ + if (writePosition < markPosition){ + // any space between the first write and + // the mark except one byte is available. + // In this case it is all in one piece. + return (markPosition - writePosition - 1); + } + // space at the beginning and end. + return ((buffer.length - 1) - (writePosition - markPosition)); + } + + /** + * Bytes available for reading. + * + * @since ostermillerutils 1.00.00 + */ + private int available(){ + if (readPosition <= writePosition){ + // any space between the first read and + // the first write is available. In this case i + // is all in one piece. + return (writePosition - readPosition); + } + // space at the beginning and end. + return (buffer.length - (readPosition - writePosition)); + } + + /** + * Bytes saved for supporting marks. + * + * @since ostermillerutils 1.00.00 + */ + private int marked(){ + if (markPosition <= readPosition){ + // any space between the markPosition and + // the first write is marked. In this case i + // is all in one piece. + return (readPosition - markPosition); + } + // space at the beginning and end. + return (buffer.length - (markPosition - readPosition)); + } + + /** + * If we have passed the markSize reset the + * mark so that the space can be used. + * + * @since ostermillerutils 1.00.00 + */ + private void ensureMark(){ + if (marked() >= markSize){ + markPosition = readPosition; + markSize = 0; + } + } + + /** + * Create a new buffer with a default capacity. + * Writing to a full buffer will block until space + * is available rather than throw an exception. + * + * @since ostermillerutils 1.00.00 + */ + public CircularByteBuffer(){ + this (DEFAULT_SIZE, true); + } + + /** + * Create a new buffer with given capacity. + * Writing to a full buffer will block until space + * is available rather than throw an exception. + *
+ * Note that the buffer may reserve some bytes for + * special purposes and capacity number of bytes may + * not be able to be written to the buffer. + *
+ * Note that if the buffer is of INFINITE_SIZE it will + * neither block or throw exceptions, but rather grow + * without bound. + * + * @param size desired capacity of the buffer in bytes or CircularByteBuffer.INFINITE_SIZE. + * + * @since ostermillerutils 1.00.00 + */ + public CircularByteBuffer(int size){ + this (size, true); + } + + /** + * Create a new buffer with a default capacity and + * given blocking behavior. + * + * @param blockingWrite true writing to a full buffer should block + * until space is available, false if an exception should + * be thrown instead. + * + * @since ostermillerutils 1.00.00 + */ + public CircularByteBuffer(boolean blockingWrite){ + this (DEFAULT_SIZE, blockingWrite); + } + + /** + * Create a new buffer with the given capacity and + * blocking behavior. + *
+ * Note that the buffer may reserve some bytes for + * special purposes and capacity number of bytes may + * not be able to be written to the buffer. + *
+ * Note that if the buffer is of INFINITE_SIZE it will + * neither block or throw exceptions, but rather grow + * without bound. + * + * @param size desired capacity of the buffer in bytes or CircularByteBuffer.INFINITE_SIZE. + * @param blockingWrite true writing to a full buffer should block + * until space is available, false if an exception should + * be thrown instead. + * + * @since ostermillerutils 1.00.00 + */ + public CircularByteBuffer(int size, boolean blockingWrite){ + if (size == INFINITE_SIZE){ + buffer = new byte[DEFAULT_SIZE]; + infinite = true; + } else { + buffer = new byte[size]; + infinite = false; + } + this.blockingWrite = blockingWrite; + } + + /** + * Class for reading from a circular byte buffer. + * + * @since ostermillerutils 1.00.00 + */ + protected class CircularByteBufferInputStream extends InputStream { + + /** + * Returns the number of bytes that can be read (or skipped over) from this + * input stream without blocking by the next caller of a method for this input + * stream. The next caller might be the same thread or or another thread. + * + * @return the number of bytes that can be read from this input stream without blocking. + * @throws IOException if the stream is closed. + * + * @since ostermillerutils 1.00.00 + */ + @Override public int available() throws IOException { + synchronized (CircularByteBuffer.this){ + if (inputStreamClosed) throw new IOException("InputStream has been closed, it is not ready."); + return (CircularByteBuffer.this.available()); + } + } + + /** + * Close the stream. Once a stream has been closed, further read(), available(), + * mark(), or reset() invocations will throw an IOException. Closing a + * previously-closed stream, however, has no effect. + * + * @throws IOException never. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void close() throws IOException { + synchronized (CircularByteBuffer.this){ + inputStreamClosed = true; + } + } + + /** + * Mark the present position in the stream. Subsequent calls to reset() will + * attempt to reposition the stream to this point. + *
+ * The readAheadLimit must be less than the size of circular buffer, otherwise + * this method has no effect. + * + * @param readAheadLimit Limit on the number of bytes that may be read while + * still preserving the mark. After reading this many bytes, attempting to + * reset the stream will fail. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void mark(int readAheadLimit) { + synchronized (CircularByteBuffer.this){ + //if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot mark a closed InputStream."); + if (buffer.length - 1 > readAheadLimit) { + markSize = readAheadLimit; + markPosition = readPosition; + } + } + } + + /** + * Tell whether this stream supports the mark() operation. + * + * @return true, mark is supported. + * + * @since ostermillerutils 1.00.00 + */ + @Override public boolean markSupported() { + return true; + } + + /** + * Read a single byte. + * This method will block until a byte is available, an I/O error occurs, + * or the end of the stream is reached. + * + * @return The byte read, as an integer in the range 0 to 255 (0x00-0xff), + * or -1 if the end of the stream has been reached + * @throws IOException if the stream is closed. + * + * @since ostermillerutils 1.00.00 + */ + @Override public int read() throws IOException { + while (true){ + synchronized (CircularByteBuffer.this){ + if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot read from a closed InputStream."); + int available = CircularByteBuffer.this.available(); + if (available > 0){ + int result = buffer[readPosition] & 0xff; + readPosition++; + if (readPosition == buffer.length){ + readPosition = 0; + } + ensureMark(); + return result; + } else if (outputStreamClosed){ + return -1; + } + } + try { + Thread.sleep(100); + } catch(Exception x){ + throw new IOException("Blocking read operation interrupted."); + } + } + } + + /** + * Read bytes into an array. + * This method will block until some input is available, + * an I/O error occurs, or the end of the stream is reached. + * + * @param cbuf Destination buffer. + * @return The number of bytes read, or -1 if the end of + * the stream has been reached + * @throws IOException if the stream is closed. + * + * @since ostermillerutils 1.00.00 + */ + @Override public int read(byte[] cbuf) throws IOException { + return read(cbuf, 0, cbuf.length); + } + + /** + * Read bytes into a portion of an array. + * This method will block until some input is available, + * an I/O error occurs, or the end of the stream is reached. + * + * @param cbuf Destination buffer. + * @param off Offset at which to start storing bytes. + * @param len Maximum number of bytes to read. + * @return The number of bytes read, or -1 if the end of + * the stream has been reached + * @throws IOException if the stream is closed. + * + * @since ostermillerutils 1.00.00 + */ + @Override public int read(byte[] cbuf, int off, int len) throws IOException { + while (true){ + synchronized (CircularByteBuffer.this){ + if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot read from a closed InputStream."); + int available = CircularByteBuffer.this.available(); + if (available > 0){ + int length = Math.min(len, available); + int firstLen = Math.min(length, buffer.length - readPosition); + int secondLen = length - firstLen; + System.arraycopy(buffer, readPosition, cbuf, off, firstLen); + if (secondLen > 0){ + System.arraycopy(buffer, 0, cbuf, off+firstLen, secondLen); + readPosition = secondLen; + } else { + readPosition += length; + } + if (readPosition == buffer.length) { + readPosition = 0; + } + ensureMark(); + return length; + } else if (outputStreamClosed){ + return -1; + } + } + try { + Thread.sleep(100); + } catch(Exception x){ + throw new IOException("Blocking read operation interrupted."); + } + } + } + + /** + * Reset the stream. + * If the stream has been marked, then attempt to reposition i + * at the mark. If the stream has not been marked, or more bytes + * than the readAheadLimit have been read, this method has no effect. + * + * @throws IOException if the stream is closed. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void reset() throws IOException { + synchronized (CircularByteBuffer.this){ + if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot reset a closed InputStream."); + readPosition = markPosition; + } + } + + /** + * Skip bytes. + * This method will block until some bytes are available, + * an I/O error occurs, or the end of the stream is reached. + * + * @param n The number of bytes to skip + * @return The number of bytes actually skipped + * @throws IllegalArgumentException if n is negative. + * @throws IOException if the stream is closed. + * + * @since ostermillerutils 1.00.00 + */ + @Override public long skip(long n) throws IOException, IllegalArgumentException { + while (true){ + synchronized (CircularByteBuffer.this){ + if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot skip bytes on a closed InputStream."); + int available = CircularByteBuffer.this.available(); + if (available > 0){ + int length = Math.min((int)n, available); + int firstLen = Math.min(length, buffer.length - readPosition); + int secondLen = length - firstLen; + if (secondLen > 0){ + readPosition = secondLen; + } else { + readPosition += length; + } + if (readPosition == buffer.length) { + readPosition = 0; + } + ensureMark(); + return length; + } else if (outputStreamClosed){ + return 0; + } + } + try { + Thread.sleep(100); + } catch(Exception x){ + throw new IOException("Blocking read operation interrupted."); + } + } + } + } + + /** + * Class for writing to a circular byte buffer. + * If the buffer is full, the writes will either block + * until there is some space available or throw an IOException + * based on the CircularByteBuffer's preference. + * + * @since ostermillerutils 1.00.00 + */ + protected class CircularByteBufferOutputStream extends OutputStream { + + /** + * Close the stream, flushing it first. + * This will cause the InputStream associated with this circular buffer + * to read its last bytes once it empties the buffer. + * Once a stream has been closed, further write() or flush() invocations + * will cause an IOException to be thrown. Closing a previously-closed stream, + * however, has no effect. + * + * @throws IOException never. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void close() throws IOException { + synchronized (CircularByteBuffer.this){ + if (!outputStreamClosed){ + flush(); + } + outputStreamClosed = true; + } + } + + /** + * Flush the stream. + * + * @throws IOException if the stream is closed. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void flush() throws IOException { + if (outputStreamClosed) throw new IOException("OutputStream has been closed; cannot flush a closed OutputStream."); + if (inputStreamClosed) throw new IOException("Buffer closed by inputStream; cannot flush."); + // this method needs to do nothing + } + + /** + * Write an array of bytes. + * If the buffer allows blocking writes, this method will block until + * all the data has been written rather than throw an IOException. + * + * @param cbuf Array of bytes to be written + * @throws BufferOverflowException if buffer does not allow blocking writes + * and the buffer is full. If the exception is thrown, no data + * will have been written since the buffer was set to be non-blocking. + * @throws IOException if the stream is closed, or the write is interrupted. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void write(byte[] cbuf) throws IOException { + write(cbuf, 0, cbuf.length); + } + + /** + * Write a portion of an array of bytes. + * If the buffer allows blocking writes, this method will block until + * all the data has been written rather than throw an IOException. + * + * @param cbuf Array of bytes + * @param off Offset from which to start writing bytes + * @param len - Number of bytes to write + * @throws BufferOverflowException if buffer does not allow blocking writes + * and the buffer is full. If the exception is thrown, no data + * will have been written since the buffer was set to be non-blocking. + * @throws IOException if the stream is closed, or the write is interrupted. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void write(byte[] cbuf, int off, int len) throws IOException { + while (len > 0){ + synchronized (CircularByteBuffer.this){ + if (outputStreamClosed) throw new IOException("OutputStream has been closed; cannot write to a closed OutputStream."); + if (inputStreamClosed) throw new IOException("Buffer closed by InputStream; cannot write to a closed buffer."); + int spaceLeft = spaceLeft(); + while (infinite && spaceLeft < len){ + resize(); + spaceLeft = spaceLeft(); + } + if (!blockingWrite && spaceLeft < len) throw new BufferOverflowException("CircularByteBuffer is full; cannot write " + len + " bytes"); + int realLen = Math.min(len, spaceLeft); + int firstLen = Math.min(realLen, buffer.length - writePosition); + int secondLen = Math.min(realLen - firstLen, buffer.length - markPosition - 1); + int written = firstLen + secondLen; + if (firstLen > 0){ + System.arraycopy(cbuf, off, buffer, writePosition, firstLen); + } + if (secondLen > 0){ + System.arraycopy(cbuf, off+firstLen, buffer, 0, secondLen); + writePosition = secondLen; + } else { + writePosition += written; + } + if (writePosition == buffer.length) { + writePosition = 0; + } + off += written; + len -= written; + } + if (len > 0){ + try { + Thread.sleep(100); + } catch(Exception x){ + throw new IOException("Waiting for available space in buffer interrupted."); + } + } + } + } + + /** + * Write a single byte. + * The byte to be written is contained in the 8 low-order bits of the + * given integer value; the 24 high-order bits are ignored. + * If the buffer allows blocking writes, this method will block until + * all the data has been written rather than throw an IOException. + * + * @param c number of bytes to be written + * @throws BufferOverflowException if buffer does not allow blocking writes + * and the buffer is full. + * @throws IOException if the stream is closed, or the write is interrupted. + * + * @since ostermillerutils 1.00.00 + */ + @Override public void write(int c) throws IOException { + boolean written = false; + while (!written){ + synchronized (CircularByteBuffer.this){ + if (outputStreamClosed) throw new IOException("OutputStream has been closed; cannot write to a closed OutputStream."); + if (inputStreamClosed) throw new IOException("Buffer closed by InputStream; cannot write to a closed buffer."); + int spaceLeft = spaceLeft(); + while (infinite && spaceLeft < 1){ + resize(); + spaceLeft = spaceLeft(); + } + if (!blockingWrite && spaceLeft < 1) throw new BufferOverflowException("CircularByteBuffer is full; cannot write 1 byte"); + if (spaceLeft > 0){ + buffer[writePosition] = (byte)(c & 0xff); + writePosition++; + if (writePosition == buffer.length) { + writePosition = 0; + } + written = true; + } + } + if (!written){ + try { + Thread.sleep(100); + } catch(Exception x){ + throw new IOException("Waiting for available space in buffer interrupted."); + } + } + } + } + } +} + + + \ No newline at end of file Index: lams_central/src/java/org/lamsfoundation/lams/util/PortraitUtils.java =================================================================== RCS file: /usr/local/cvsroot/lams_central/src/java/org/lamsfoundation/lams/util/PortraitUtils.java,v diff -u -r1.3 -r1.4 --- lams_central/src/java/org/lamsfoundation/lams/util/PortraitUtils.java 17 Sep 2006 06:12:06 -0000 1.3 +++ lams_central/src/java/org/lamsfoundation/lams/util/PortraitUtils.java 10 Nov 2008 13:59:14 -0000 1.4 @@ -24,11 +24,11 @@ /* $Id$ */ package org.lamsfoundation.lams.util; +import java.awt.Color; import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -38,50 +38,128 @@ /** * @author jliew - * + * @author Andrey Balan */ public class PortraitUtils { - private static Logger log = Logger.getLogger(PortraitUtils.class); - - /* - * Resize picture to specified width and height in pixels. Maintains aspect ratio. - * Output image in the format specified by formatName. - */ - public static ByteArrayInputStream resizePicture(InputStream is, int width, int height, String formatName) { - - ByteArrayOutputStream os = null; - try { - // create buffer for source image - BufferedImage bsrc = ImageIO.read(is); - log.debug("old width: "+bsrc.getWidth()+", old height: "+bsrc.getHeight()); - - // maintain aspect ratio - double widthScale = (double)width/bsrc.getWidth(); - double heightScale = (double)height/bsrc.getHeight(); - double minScale = Math.min(widthScale,heightScale); - double newWidth = minScale*(double)bsrc.getWidth(); - double newHeight = minScale*(double)bsrc.getHeight(); - log.debug("scaling picture by "+minScale+"... new width: "+newWidth+", new height: "+newHeight); - - // create buffer for resized image - BufferedImage bdest = new BufferedImage((int)newWidth, (int)newHeight, BufferedImage.TYPE_INT_RGB); - Graphics2D g = bdest.createGraphics(); - AffineTransform at = AffineTransform.getScaleInstance(minScale, minScale); - g.drawRenderedImage(bsrc,at); - - // write new picture into a buffer usable by content repository - os = new ByteArrayOutputStream(); - // As at Java 1.5, ImageIO does not support writing gif - if (formatName.equals("gif")) formatName = "jpg"; - log.debug("using format: "+formatName); - ImageIO.write(bdest,formatName,os); - // alternative may be to use a File on disk as the buffer - } catch (IOException e) { - log.error(e.getStackTrace()); - return null; + private static Logger log = Logger.getLogger(PortraitUtils.class); + + /** + * Reads the original image, creates a thumbnail and returns its input stream. + * largestDimension is the largest dimension of the thumbnail, the other dimension is scaled accordingly. Utilises + * weighted stepping method to gradually reduce the image size for better results, i.e. larger steps to start with + * then smaller steps to finish with. Note: always writes a JPEG because GIF is protected or something - so always + * make your outFilename end in 'jpg' PNG's with transparency are given white backgrounds + * + * @param is + * original image's input stream + * @param largestDimension + * the largest dimension of the thumbnail, the other dimension is scaled accordingly + * @return + * @throws UploadImageGalleryFileException + */ + public static InputStream resizePicture(InputStream is, int largestDimension) { + InputStream thumbnailInputStream = null; + try { + double scale; + int sizeDifference; + int originalImageLargestDim; + + BufferedImage inImage = ImageIO.read(is); + + // find biggest dimension + if (inImage.getWidth(null) > inImage.getHeight(null)) { + scale = (double) largestDimension / (double) inImage.getWidth(null); + sizeDifference = inImage.getWidth(null) - largestDimension; + originalImageLargestDim = inImage.getWidth(null); + } else { + scale = (double) largestDimension / (double) inImage.getHeight(null); + sizeDifference = inImage.getHeight(null) - largestDimension; + originalImageLargestDim = inImage.getHeight(null); + } + // create an image buffer to draw to + BufferedImage outImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); // arbitrary init so + // code compiles + Graphics2D g2d; + AffineTransform tx; + if (scale < 1.0d) // only scale if desired size is smaller than original + { + int numSteps = ((sizeDifference / 200) != 0) ? (sizeDifference / 200) : 1; + int stepSize = sizeDifference / numSteps; + int stepWeight = stepSize / 2; + int heavierStepSize = stepSize + stepWeight; + int lighterStepSize = stepSize - stepWeight; + int currentStepSize, centerStep; + double scaledW = inImage.getWidth(null); + double scaledH = inImage.getHeight(null); + if (numSteps % 2 == 1) { + centerStep = (int) Math.ceil(numSteps / 2d); // find the center step + } else { + centerStep = -1; // set it to -1 so it's ignored later } - return new ByteArrayInputStream(os.toByteArray()); + Integer intermediateSize = originalImageLargestDim, previousIntermediateSize = originalImageLargestDim; + Integer calculatedDim; + for (Integer i = 0; i < numSteps; i++) { + if (i + 1 != centerStep) // if this isn't the center step + { + if (i == numSteps - 1) // if this is the last step + { + // fix the stepsize to account for decimal place errors previously + currentStepSize = previousIntermediateSize - largestDimension; + } else { + if (numSteps - i > numSteps / 2) { + currentStepSize = heavierStepSize; + } else { + currentStepSize = lighterStepSize; + } + } + } else // center step, use natural step size + { + currentStepSize = stepSize; + } + intermediateSize = previousIntermediateSize - currentStepSize; + scale = (double) intermediateSize / (double) previousIntermediateSize; + scaledW = (int) scaledW * scale; + scaledH = (int) scaledH * scale; + outImage = new BufferedImage((int) scaledW, (int) scaledH, BufferedImage.TYPE_INT_RGB); + g2d = outImage.createGraphics(); + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, outImage.getWidth(), outImage.getHeight()); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + + tx = new AffineTransform(); + tx.scale(scale, scale); + g2d.drawImage(inImage, tx, null); + g2d.dispose(); + inImage = outImage; + previousIntermediateSize = intermediateSize; + } + } else { + // just copy the original + outImage = new BufferedImage(inImage.getWidth(null), inImage.getHeight(null), + BufferedImage.TYPE_INT_RGB); + g2d = outImage.createGraphics(); + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, outImage.getWidth(), outImage.getHeight()); + tx = new AffineTransform(); + tx.setToIdentity(); // use identity matrix so image is copied exactly + g2d.drawImage(inImage, tx, null); + g2d.dispose(); + } + + // buffer all data in a circular buffer of infinite size + CircularByteBuffer cbb = new CircularByteBuffer(CircularByteBuffer.INFINITE_SIZE); + ImageIO.write(outImage, "JPG", cbb.getOutputStream()); + cbb.getOutputStream().close(); + + thumbnailInputStream = cbb.getInputStream(); + + } catch (IOException e) { + PortraitUtils.log.error(e.getStackTrace()); + return null; } - + return thumbnailInputStream; + } + } Index: lams_central/src/java/org/lamsfoundation/lams/web/PortraitSaveAction.java =================================================================== RCS file: /usr/local/cvsroot/lams_central/src/java/org/lamsfoundation/lams/web/PortraitSaveAction.java,v diff -u -r1.8 -r1.9 --- lams_central/src/java/org/lamsfoundation/lams/web/PortraitSaveAction.java 20 Aug 2007 06:15:47 -0000 1.8 +++ lams_central/src/java/org/lamsfoundation/lams/web/PortraitSaveAction.java 10 Nov 2008 13:59:14 -0000 1.9 @@ -24,7 +24,7 @@ /* $Id$ */ package org.lamsfoundation.lams.web; -import java.io.ByteArrayInputStream; +import java.io.InputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -49,92 +49,92 @@ /** * @author jliew - * - * @struts:action path="/saveportrait" - * name="PortraitActionForm" - * input=".portrait" - * scope="request" - * validate="false" + * @author Andrey Balan * + * @struts:action path="/saveportrait" name="PortraitActionForm" input=".portrait" scope="request" validate="false" + * * @struts:action-forward name="profile" path="/index.do?state=active&tab=profile" * @struts:action-forward name="errors" path="/index.do?state=active&tab=portrait" */ public class PortraitSaveAction extends Action { - - private static Logger log = Logger.getLogger(PortraitSaveAction.class); - private static IUserManagementService service; - private static CentralToolContentHandler centralToolContentHandler; - private static int THUMBNAIL_WIDTH = 120; - private static int THUMBNAIL_HEIGHT = 120; - public ActionForward execute(ActionMapping mapping, - ActionForm form, - HttpServletRequest request, - HttpServletResponse response) throws Exception { - - if(isCancelled(request)){ - return mapping.findForward("profile"); - } - - ActionMessages errors = new ActionMessages(); - - WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet().getServletContext()); - centralToolContentHandler = (CentralToolContentHandler)wac.getBean("centralToolContentHandler"); - - PortraitActionForm portraitForm = (PortraitActionForm)form; - FormFile file = portraitForm.getFile(); - String fileName = file.getFileName(); - log.debug("got file: "+fileName+" of type: "+file.getContentType()+" with size: "+file.getFileSize()); - - User user = (User)getService().getUserByLogin(request.getRemoteUser()); + private static Logger log = Logger.getLogger(PortraitSaveAction.class); + private static IUserManagementService service; + private static CentralToolContentHandler centralToolContentHandler; + private static int LARGEST_DIMENSION = 120; - // check if file is an image using the MIME content type - String mediaType = file.getContentType().split("/",2)[0]; - if (!mediaType.equals("image")) { - errors.add("file",new ActionMessage("error.portrait.not.image")); - saveErrors(request, errors); - return mapping.findForward("errors"); - } - - // resize picture into new buffer - //String fileType = file.getFileName().split(".",2)[1]; - String fileType = fileName.substring(fileName.lastIndexOf('.')+1); - log.debug("fileType: "+fileType); - ByteArrayInputStream is = PortraitUtils.resizePicture(file.getInputStream(), THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, fileType); - if (is==null) { - errors.add("file",new ActionMessage("error.general.1")); - saveErrors(request, errors); - return mapping.findForward("errors"); - } - - // write to content repository - NodeKey node = null; - if (file!= null && !StringUtils.isEmpty(fileName)) { - try { - //InputStream is = file.getInputStream(); - node = centralToolContentHandler.uploadFile(is, fileName, file.getContentType(), IToolContentHandler.TYPE_ONLINE); - is.close(); - } catch (Exception e) { - request.setAttribute("errorMessage", e.getMessage()); - return mapping.findForward("error.system"); - } - } - - log.debug("saved file with uuid: "+node.getUuid()+" and version: "+node.getVersion()); - - // delete old portrait file (we only want to keep the user's current portrait) - if (user.getPortraitUuid()!=null) centralToolContentHandler.deleteFile(user.getPortraitUuid()); - user.setPortraitUuid(node.getUuid()); - getService().save(user); - - return mapping.findForward("profile"); + @Override + public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws Exception { + + if (isCancelled(request)) { + return mapping.findForward("profile"); } - - private IUserManagementService getService(){ - if(service==null){ - WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet().getServletContext()); - service = (IUserManagementService) ctx.getBean("userManagementService"); - } - return service; + + ActionMessages errors = new ActionMessages(); + + WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet() + .getServletContext()); + PortraitSaveAction.centralToolContentHandler = (CentralToolContentHandler) wac + .getBean("centralToolContentHandler"); + + PortraitActionForm portraitForm = (PortraitActionForm) form; + FormFile file = portraitForm.getFile(); + String fileName = file.getFileName(); + PortraitSaveAction.log.debug("got file: " + fileName + " of type: " + file.getContentType() + " with size: " + + file.getFileSize()); + + User user = getService().getUserByLogin(request.getRemoteUser()); + + // check if file is an image using the MIME content type + String mediaType = file.getContentType().split("/", 2)[0]; + if (!mediaType.equals("image")) { + errors.add("file", new ActionMessage("error.portrait.not.image")); + saveErrors(request, errors); + return mapping.findForward("errors"); } + + // resize picture + InputStream is = PortraitUtils.resizePicture(file.getInputStream(), PortraitSaveAction.LARGEST_DIMENSION); + if (is == null) { + errors.add("file", new ActionMessage("error.general.1")); + saveErrors(request, errors); + return mapping.findForward("errors"); + } + + // write to content repository + NodeKey node = null; + if ((file != null) && !StringUtils.isEmpty(fileName)) { + try { + fileName = fileName.substring(0, fileName.indexOf('.')) + ".jpg"; + node = PortraitSaveAction.centralToolContentHandler.uploadFile(is, fileName, file.getContentType(), + IToolContentHandler.TYPE_ONLINE); + is.close(); + } catch (Exception e) { + request.setAttribute("errorMessage", e.getMessage()); + return mapping.findForward("error.system"); + } + } + + PortraitSaveAction.log.debug("saved file with uuid: " + node.getUuid() + " and version: " + node.getVersion()); + + // delete old portrait file (we only want to keep the user's current portrait) + if (user.getPortraitUuid() != null) { + PortraitSaveAction.centralToolContentHandler.deleteFile(user.getPortraitUuid()); + } + user.setPortraitUuid(node.getUuid()); + getService().save(user); + + return mapping.findForward("profile"); + } + + private IUserManagementService getService() { + if (PortraitSaveAction.service == null) { + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServlet() + .getServletContext()); + PortraitSaveAction.service = (IUserManagementService) ctx.getBean("userManagementService"); + } + return PortraitSaveAction.service; + } } +