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