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.IOException;
021    import java.io.OutputStream;
022    import java.io.FilterOutputStream;
023    
024    /**
025     * An implementation of a FilterOutputStream that encodes the
026     * stream data in BASE64 encoding format.  This version does the
027     * encoding "on the fly" rather than encoding a single block of
028     * data.  Since this version is intended for use by the MimeUtilty class,
029     * it also handles line breaks in the encoded data.
030     */
031    public class Base64EncoderStream extends FilterOutputStream {
032    
033        // our Filtered stream writes everything out as byte data.  This allows the CRLF sequence to
034        // be written with a single call.
035        protected static final byte[] CRLF = { '\r', '\n' };
036    
037        // our hex encoder utility class.
038        protected Base64Encoder encoder = new Base64Encoder();
039    
040        // our default for line breaks
041        protected static final int DEFAULT_LINEBREAK = 76;
042    
043        // Data can only be written out in complete units of 3 bytes encoded as 4.  Therefore, we need to buffer
044        // as many as 2 bytes to fill out an encoding unit.
045    
046        // the buffered byte count
047        protected int bufferedBytes = 0;
048    
049        // we'll encode this part once it is filled up.
050        protected byte[] buffer = new byte[3];
051    
052    
053        // the size we process line breaks at.  If this is Integer.MAX_VALUE, no line breaks are handled.
054        protected int lineBreak;
055    
056        // the number of encoded characters we've written to the stream, which determines where we
057        // insert line breaks.
058        protected int outputCount;
059    
060        /**
061         * Create a Base64 encoder stream that wraps a specifed stream
062         * using the default line break size.
063         *
064         * @param out    The wrapped output stream.
065         */
066        public Base64EncoderStream(OutputStream out) {
067            this(out, DEFAULT_LINEBREAK);
068        }
069    
070    
071        public Base64EncoderStream(OutputStream out, int lineBreak) {
072            super(out);
073            // lines are processed only in multiple of 4, so round this down.
074            this.lineBreak = (lineBreak / 4) * 4 ;
075        }
076    
077        // in order for this to work, we need to override the 3 different signatures for write
078    
079        public void write(int ch) throws IOException {
080            // store this in the buffer.
081            buffer[bufferedBytes++] = (byte)ch;
082            // if the buffer is filled, encode these bytes
083            if (bufferedBytes == 3) {
084                // check for room in the current line for this character
085                checkEOL(4);
086                // write these directly to the stream.
087                encoder.encode(buffer, 0, 3, out);
088                bufferedBytes = 0;
089                // and update the line length checkers
090                updateLineCount(4);
091            }
092        }
093    
094        public void write(byte [] data) throws IOException {
095            write(data, 0, data.length);
096        }
097    
098        public void write(byte [] data, int offset, int length) throws IOException {
099            // if we have something in the buffer, we need to write enough bytes out to flush
100            // those into the output stream AND continue on to finish off a line.  Once we're done there
101            // we can write additional data out in complete blocks.
102            while ((bufferedBytes > 0 || outputCount > 0) && length > 0) {
103                write(data[offset++]);
104                length--;
105            }
106    
107            if (length > 0) {
108                // no linebreaks requested?  YES!!!!!, we can just dispose of the lot with one call.
109                if (lineBreak == Integer.MAX_VALUE) {
110                    encoder.encode(data, offset, length, out);
111                }
112                else {
113                    // calculate the size of a segment we can encode directly as a line.
114                    int segmentSize = (lineBreak / 4) * 3;
115    
116                    // write this out a block at a time, with separators between.
117                    while (length > segmentSize) {
118                        // encode a segment
119                        encoder.encode(data, offset, segmentSize, out);
120                        // write an EOL marker
121                        out.write(CRLF);
122                        offset += segmentSize;
123                        length -= segmentSize;
124                    }
125    
126                    // any remainder we write out a byte at a time to manage the groupings and
127                    // the line count appropriately.
128                    if (length > 0) {
129                        while (length > 0) {
130                            write(data[offset++]);
131                            length--;
132                        }
133                    }
134                }
135            }
136        }
137    
138        public void close() throws IOException {
139            flush();
140            out.close();
141        }
142    
143        public void flush() throws IOException {
144            if (bufferedBytes > 0) {
145                encoder.encode(buffer, 0, bufferedBytes, out);
146                bufferedBytes = 0;
147            }
148        }
149    
150    
151        /**
152         * Check for whether we're about the reach the end of our
153         * line limit for an update that's about to occur.  If we will
154         * overflow, then a line break is inserted.
155         *
156         * @param required The space required for this pending write.
157         *
158         * @exception IOException
159         */
160        private void checkEOL(int required) throws IOException {
161            if (lineBreak != Integer.MAX_VALUE) {
162                // if this write would exceed the line maximum, add a linebreak to the stream.
163                if (outputCount + required > lineBreak) {
164                    out.write(CRLF);
165                    outputCount = 0;
166                }
167            }
168        }
169    
170        /**
171         * Update the counter of characters on the current working line.
172         * This is conditional if we're not working with a line limit.
173         *
174         * @param added  The number of characters just added.
175         */
176        private void updateLineCount(int added) {
177            if (lineBreak != Integer.MAX_VALUE) {
178                outputCount += added;
179            }
180        }
181    }
182