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    public class UUEncoderStream extends FilterOutputStream {
035    
036        // default values included on the "begin" prefix of the data stream.
037        protected static final int DEFAULT_MODE = 644;
038        protected static final String DEFAULT_NAME = "encoder.buf";
039    
040        protected static final int MAX_CHARS_PER_LINE = 45;
041    
042        // the configured name on the "begin" command.
043        protected String name;
044        // the configured mode for the "begin" command.
045        protected int mode;
046    
047        // since this is a filtering stream, we need to wait until we have the first byte written for encoding
048        // to write out the "begin" marker.  A real pain, but necessary.
049        protected boolean beginWritten = false;
050    
051    
052        // our encoder utility class.
053        protected UUEncoder encoder = new UUEncoder();
054    
055        // Data is generally written out in 45 character lines, so we're going to buffer that amount before
056        // asking the encoder to process this.
057    
058        // the buffered byte count
059        protected int bufferedBytes = 0;
060    
061        // we'll encode this part once it is filled up.
062        protected byte[] buffer = new byte[45];
063    
064        /**
065         * Create a Base64 encoder stream that wraps a specifed stream
066         * using the default line break size.
067         *
068         * @param out    The wrapped output stream.
069         */
070        public UUEncoderStream(OutputStream out) {
071            this(out, DEFAULT_NAME, DEFAULT_MODE);
072        }
073    
074    
075        /**
076         * Create a Base64 encoder stream that wraps a specifed stream
077         * using the default line break size.
078         *
079         * @param out    The wrapped output stream.
080         * @param name   The filename placed on the "begin" command.
081         */
082        public UUEncoderStream(OutputStream out, String name) {
083            this(out, name, DEFAULT_MODE);
084        }
085    
086    
087        public UUEncoderStream(OutputStream out, String name, int mode) {
088            super(out);
089            // fill in the name and mode information.
090            this.name = name;
091            this.mode = mode;
092        }
093    
094    
095        private void checkBegin() throws IOException {
096            if (!beginWritten) {
097                // grumble...OutputStream doesn't directly support writing String data.  We'll wrap this in
098                // a PrintStream() to accomplish the task of writing the begin command.
099    
100                PrintStream writer = new PrintStream(out);
101                // write out the stream with a CRLF marker
102                writer.print("begin " + mode + " " + name + "\r\n");
103                writer.flush();
104                beginWritten = true;
105            }
106        }
107    
108        private void writeEnd() throws IOException {
109            PrintStream writer = new PrintStream(out);
110            // write out the stream with a CRLF marker
111            writer.print("\nend\r\n");
112            writer.flush();
113        }
114    
115        private void flushBuffer() throws IOException {
116            // make sure we've written the begin marker first
117            checkBegin();
118            // ask the encoder to encode and write this out.
119            if (bufferedBytes != 0) {
120                encoder.encode(buffer, 0, bufferedBytes, out);
121                // reset the buffer count
122                bufferedBytes = 0;
123            }
124        }
125    
126        private int bufferSpace() {
127            return MAX_CHARS_PER_LINE - bufferedBytes;
128        }
129    
130        private boolean isBufferFull() {
131            return bufferedBytes >= MAX_CHARS_PER_LINE;
132        }
133    
134    
135        // in order for this to work, we need to override the 3 different signatures for write
136    
137        public void write(int ch) throws IOException {
138            // store this in the buffer.
139            buffer[bufferedBytes++] = (byte)ch;
140    
141            // if we filled this up, time to encode and write to the output stream.
142            if (isBufferFull()) {
143                flushBuffer();
144            }
145        }
146    
147        public void write(byte [] data) throws IOException {
148            write(data, 0, data.length);
149        }
150    
151        public void write(byte [] data, int offset, int length) throws IOException {
152            // first check to see how much space we have left in the buffer, and copy that over
153            int copyBytes = Math.min(bufferSpace(), length);
154    
155            System.arraycopy(buffer, bufferedBytes, data, offset, copyBytes);
156            bufferedBytes += copyBytes;
157            offset += copyBytes;
158            length -= copyBytes;
159    
160            // if we filled this up, time to encode and write to the output stream.
161            if (isBufferFull()) {
162                flushBuffer();
163            }
164    
165            // we've flushed the leading part up to the line break.  Now if we have complete lines
166            // of data left, we can have the encoder process all of these lines directly.
167            if (length >= MAX_CHARS_PER_LINE) {
168                int fullLinesLength = (length / MAX_CHARS_PER_LINE) * MAX_CHARS_PER_LINE;
169                // ask the encoder to encode and write this out.
170                encoder.encode(data, offset, fullLinesLength, out);
171                offset += fullLinesLength;
172                length -= fullLinesLength;
173            }
174    
175            // ok, now we're down to a potential trailing bit we need to move into the
176            // buffer for later processing.
177    
178            if (length > 0) {
179                System.arraycopy(buffer, 0, data, offset, length);
180                bufferedBytes += length;
181                offset += length;
182                length -= length;
183            }
184        }
185    
186        public void flush() throws IOException {
187            // flush any unencoded characters we're holding.
188            flushBuffer();
189            // write out the data end marker
190            writeEnd();
191            // and flush the output stream too so that this data is available.
192            out.flush();
193        }
194    
195        public void close() throws IOException {
196            // flush all of the streams and close the target output stream.
197            flush();
198            out.close();
199        }
200    
201    }
202    
203