    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  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
    018package org.apache.commons.ssl;
    019
    020import java.io.FilterOutputStream;
    021import java.io.IOException;
    022import java.io.OutputStream;
    023
    024/**
    025 * Provides Base64 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength
    026 * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate
    027 * constructor.
    028 * <p>
    029 * The default behaviour of the Base64OutputStream is to ENCODE, whereas the default behaviour of the Base64InputStream
    030 * is to DECODE. But this behaviour can be overridden by using a different constructor.
    031 * </p>
    032 * <p>
    033 * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
    034 * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
    035 * </p>
    036 * <p>
    037 * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
    038 * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
    039 * </p>
    040 *
    041 * @author Apache Software Foundation
    042 * @version $Id$
    043 * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
    044 * @since 1.4
    045 */
    046public class Base64OutputStream extends FilterOutputStream {
    047    private final boolean doEncode;
    048
    049    private final Base64 base64;
    050
    051    private final byte[] singleByte = new byte[1];
    052
    053    /**
    054     * Creates a Base64OutputStream such that all data written is Base64-encoded to the original provided OutputStream.
    055     *
    056     * @param out
    057     *            OutputStream to wrap.
    058     */
    059    public Base64OutputStream(OutputStream out) {
    060        this(out, true);
    061    }
    062
    063    /**
    064     * Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the
    065     * original provided OutputStream.
    066     *
    067     * @param out
    068     *            OutputStream to wrap.
    069     * @param doEncode
    070     *            true if we should encode all data written to us, false if we should decode.
    071     */
    072    public Base64OutputStream(OutputStream out, boolean doEncode) {
    073        super(out);
    074        this.doEncode = doEncode;
    075        this.base64 = new Base64();
    076    }
    077
    078    /**
    079     * Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the
    080     * original provided OutputStream.
    081     *
    082     * @param out
    083     *            OutputStream to wrap.
    084     * @param doEncode
    085     *            true if we should encode all data written to us, false if we should decode.
    086     * @param lineLength
    087     *            If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
    088     *            nearest multiple of 4). If lineLength <=0, the encoded data is not divided into lines. If doEncode is
    089     *            false, lineLength is ignored.
    090     * @param lineSeparator
    091     *            If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
    092     *            If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
    093     */
    094    public Base64OutputStream(OutputStream out, boolean doEncode, int lineLength, byte[] lineSeparator) {
    095        super(out);
    096        this.doEncode = doEncode;
    097        this.base64 = new Base64(lineLength, lineSeparator);
    098    }
    099
    100    /**
    101     * Writes the specified <code>byte</code> to this output stream.
    102     *
    103     * @param i
    104     *            source byte
    105     * @throws IOException
    106     *             if an I/O error occurs.
    107     */
    108    public void write(int i) throws IOException {
    109        singleByte[0] = (byte) i;
    110        write(singleByte, 0, 1);
    111    }
    112
    113    /**
    114     * Writes <code>len</code> bytes from the specified <code>b</code> array starting at <code>offset</code> to this
    115     * output stream.
    116     *
    117     * @param b
    118     *            source byte array
    119     * @param offset
    120     *            where to start reading the bytes
    121     * @param len
    122     *            maximum number of bytes to write
    123     *
    124     * @throws IOException
    125     *             if an I/O error occurs.
    126     * @throws NullPointerException
    127     *             if the byte array parameter is null
    128     * @throws IndexOutOfBoundsException
    129     *             if offset, len or buffer size are invalid
    130     */
    131    public void write(byte b[], int offset, int len) throws IOException {
    132        if (b == null) {
    133            throw new NullPointerException();
    134        } else if (offset < 0 || len < 0) {
    135            throw new IndexOutOfBoundsException();
    136        } else if (offset > b.length || offset + len > b.length) {
    137            throw new IndexOutOfBoundsException();
    138        } else if (len > 0) {
    139            if (doEncode) {
    140                base64.encode(b, offset, len);
    141            } else {
    142                base64.decode(b, offset, len);
    143            }
    144            flush(false);
    145        }
    146    }
    147
    148    /**
    149     * Flushes this output stream and forces any buffered output bytes to be written out to the stream. If propogate is
    150     * true, the wrapped stream will also be flushed.
    151     *
    152     * @param propogate
    153     *            boolean flag to indicate whether the wrapped OutputStream should also be flushed.
    154     * @throws IOException
    155     *             if an I/O error occurs.
    156     */
    157    private void flush(boolean propogate) throws IOException {
    158        int avail = base64.avail();
    159        if (avail > 0) {
    160            byte[] buf = new byte[avail];
    161            int c = base64.readResults(buf, 0, avail);
    162            if (c > 0) {
    163                out.write(buf, 0, c);
    164            }
    165        }
    166        if (propogate) {
    167            out.flush();
    168        }
    169    }
    170
    171    /**
    172     * Flushes this output stream and forces any buffered output bytes to be written out to the stream.
    173     *
    174     * @throws IOException
    175     *             if an I/O error occurs.
    176     */
    177    public void flush() throws IOException {
    178        flush(true);
    179    }
    180
    181    /**
    182     * Closes this output stream and releases any system resources associated with the stream.
    183     *
    184     * @throws IOException
    185     *             if an I/O error occurs.
    186     */
    187    public void close() throws IOException {
    188        // Notify encoder of EOF (-1).
    189        if (doEncode) {
    190            base64.encode(singleByte, 0, -1);
    191        } else {
    192            base64.decode(singleByte, 0, -1);
    193        }
    194        flush();
    195        out.close();
    196    }
    197
    198}