View Javadoc

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