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