1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package org.apache.geronimo.mail.util;
21
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.io.FilterOutputStream;
25
26 /**
27 * An implementation of a FilterOutputStream that encodes the
28 * stream data in BASE64 encoding format. This version does the
29 * encoding "on the fly" rather than encoding a single block of
30 * data. Since this version is intended for use by the MimeUtilty class,
31 * it also handles line breaks in the encoded data.
32 */
33 public class Base64EncoderStream extends FilterOutputStream {
34
35 // our Filtered stream writes everything out as byte data. This allows the CRLF sequence to
36 // be written with a single call.
37 protected static final byte[] CRLF = { '\r', '\n' };
38
39 // our hex encoder utility class.
40 protected Base64Encoder encoder = new Base64Encoder();
41
42 // our default for line breaks
43 protected static final int DEFAULT_LINEBREAK = 76;
44
45 // Data can only be written out in complete units of 3 bytes encoded as 4. Therefore, we need to buffer
46 // as many as 2 bytes to fill out an encoding unit.
47
48 // the buffered byte count
49 protected int bufferedBytes = 0;
50
51 // we'll encode this part once it is filled up.
52 protected byte[] buffer = new byte[3];
53
54
55 // the size we process line breaks at. If this is Integer.MAX_VALUE, no line breaks are handled.
56 protected int lineBreak;
57
58 // the number of encoded characters we've written to the stream, which determines where we
59 // insert line breaks.
60 protected int outputCount;
61
62 /**
63 * Create a Base64 encoder stream that wraps a specifed stream
64 * using the default line break size.
65 *
66 * @param out The wrapped output stream.
67 */
68 public Base64EncoderStream(OutputStream out) {
69 this(out, DEFAULT_LINEBREAK);
70 }
71
72
73 public Base64EncoderStream(OutputStream out, int lineBreak) {
74 super(out);
75 // lines are processed only in multiple of 4, so round this down.
76 this.lineBreak = (lineBreak / 4) * 4 ;
77 }
78
79 // in order for this to work, we need to override the 3 different signatures for write
80
81 public void write(int ch) throws IOException {
82 // store this in the buffer.
83 buffer[bufferedBytes++] = (byte)ch;
84 // if the buffer is filled, encode these bytes
85 if (bufferedBytes == 3) {
86 // check for room in the current line for this character
87 checkEOL(4);
88 // write these directly to the stream.
89 encoder.encode(buffer, 0, 3, out);
90 bufferedBytes = 0;
91 // and update the line length checkers
92 updateLineCount(4);
93 }
94 }
95
96 public void write(byte [] data) throws IOException {
97 write(data, 0, data.length);
98 }
99
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