001    /**
002     *
003     * Copyright 2003-2006 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package org.apache.geronimo.mail.util;
019    
020    import java.io.FilterOutputStream;
021    import java.io.IOException;
022    import java.io.OutputStream;
023    import java.io.PrintStream;
024    
025    /**
026     * An implementation of a FilterOutputStream that encodes the
027     * stream data in UUencoding format.  This version does the
028     * encoding "on the fly" rather than encoding a single block of
029     * data.  Since this version is intended for use by the MimeUtilty class,
030     * it also handles line breaks in the encoded data.
031     */
032    public class UUEncoderStream extends FilterOutputStream {
033    
034        // default values included on the "begin" prefix of the data stream.
035        protected static final int DEFAULT_MODE = 644;
036        protected static final String DEFAULT_NAME = "encoder.buf";
037    
038        protected static final int MAX_CHARS_PER_LINE = 45;
039    
040        // the configured name on the "begin" command.
041        protected String name;
042        // the configured mode for the "begin" command.
043        protected int mode;
044    
045        // since this is a filtering stream, we need to wait until we have the first byte written for encoding
046        // to write out the "begin" marker.  A real pain, but necessary.
047        protected boolean beginWritten = false;
048    
049    
050        // our encoder utility class.
051        protected UUEncoder encoder = new UUEncoder();
052    
053        // Data is generally written out in 45 character lines, so we're going to buffer that amount before
054        // asking the encoder to process this.
055    
056        // the buffered byte count
057        protected int bufferedBytes = 0;
058    
059        // we'll encode this part once it is filled up.
060        protected byte[] buffer = new byte[45];
061    
062        /**
063         * Create a Base64 encoder stream that wraps a specifed stream
064         * using the default line break size.
065         *
066         * @param out    The wrapped output stream.
067         */
068        public UUEncoderStream(OutputStream out) {
069            this(out, DEFAULT_NAME, DEFAULT_MODE);
070        }
071    
072    
073        /**
074         * Create a Base64 encoder stream that wraps a specifed stream
075         * using the default line break size.
076         *
077         * @param out    The wrapped output stream.
078         * @param name   The filename placed on the "begin" command.
079         */
080        public UUEncoderStream(OutputStream out, String name) {
081            this(out, name, DEFAULT_MODE);
082        }
083    
084    
085        public UUEncoderStream(OutputStream out, String name, int mode) {
086            super(out);
087            // fill in the name and mode information.
088            this.name = name;
089            this.mode = mode;
090        }
091    
092    
093        private void checkBegin() throws IOException {
094            if (!beginWritten) {
095                // grumble...OutputStream doesn't directly support writing String data.  We'll wrap this in
096                // a PrintStream() to accomplish the task of writing the begin command.
097    
098                PrintStream writer = new PrintStream(out);
099                // write out the stream with a CRLF marker
100                writer.print("begin " + mode + " " + name + "\r\n");
101                writer.flush();
102                beginWritten = true;
103            }
104        }
105    
106        private void writeEnd() throws IOException {
107            PrintStream writer = new PrintStream(out);
108            // write out the stream with a CRLF marker
109            writer.print("\nend\r\n");
110            writer.flush();
111        }
112    
113        private void flushBuffer() throws IOException {
114            // make sure we've written the begin marker first
115            checkBegin();
116            // ask the encoder to encode and write this out.
117            if (bufferedBytes != 0) {
118                encoder.encode(buffer, 0, bufferedBytes, out);
119                // reset the buffer count
120                bufferedBytes = 0;
121            }
122        }
123    
124        private int bufferSpace() {
125            return MAX_CHARS_PER_LINE - bufferedBytes;
126        }
127    
128        private boolean isBufferFull() {
129            return bufferedBytes >= MAX_CHARS_PER_LINE;
130        }
131    
132    
133        // in order for this to work, we need to override the 3 different signatures for write
134    
135        public void write(int ch) throws IOException {
136            // store this in the buffer.
137            buffer[bufferedBytes++] = (byte)ch;
138    
139            // if we filled this up, time to encode and write to the output stream.
140            if (isBufferFull()) {
141                flushBuffer();
142            }
143        }
144    
145        public void write(byte [] data) throws IOException {
146            write(data, 0, data.length);
147        }
148    
149        public void write(byte [] data, int offset, int length) throws IOException {
150            // first check to see how much space we have left in the buffer, and copy that over
151            int copyBytes = Math.min(bufferSpace(), length);
152    
153            System.arraycopy(buffer, bufferedBytes, data, offset, copyBytes);
154            bufferedBytes += copyBytes;
155            offset += copyBytes;
156            length -= copyBytes;
157    
158            // if we filled this up, time to encode and write to the output stream.
159            if (isBufferFull()) {
160                flushBuffer();
161            }
162    
163            // we've flushed the leading part up to the line break.  Now if we have complete lines
164            // of data left, we can have the encoder process all of these lines directly.
165            if (length >= MAX_CHARS_PER_LINE) {
166                int fullLinesLength = (length / MAX_CHARS_PER_LINE) * MAX_CHARS_PER_LINE;
167                // ask the encoder to encode and write this out.
168                encoder.encode(data, offset, fullLinesLength, out);
169                offset += fullLinesLength;
170                length -= fullLinesLength;
171            }
172    
173            // ok, now we're down to a potential trailing bit we need to move into the
174            // buffer for later processing.
175    
176            if (length > 0) {
177                System.arraycopy(buffer, 0, data, offset, length);
178                bufferedBytes += length;
179                offset += length;
180                length -= length;
181            }
182        }
183    
184        public void flush() throws IOException {
185            // flush any unencoded characters we're holding.
186            flushBuffer();
187            // write out the data end marker
188            writeEnd();
189            // and flush the output stream too so that this data is available.
190            out.flush();
191        }
192    
193        public void close() throws IOException {
194            // flush all of the streams and close the target output stream.
195            flush();
196            out.close();
197        }
198    
199    }
200    
201