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