001 /**
002 *
003 * Copyright 2003-2004 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