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.codec.binary;
019
020import static org.apache.commons.codec.binary.BaseNCodec.EOF;
021
022import java.io.FilterOutputStream;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.util.Objects;
026
027import org.apache.commons.codec.binary.BaseNCodec.Context;
028
029/**
030 * Abstract superclass for Base-N output streams.
031 * <p>
032 * To write the EOF marker without closing the stream, call {@link #eof()} or use an <a
033 * href="https://commons.apache.org/proper/commons-io/">Apache Commons IO</a> <a href=
034 * "https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/output/CloseShieldOutputStream.html"
035 * >CloseShieldOutputStream</a>.
036 * </p>
037 *
038 * @since 1.5
039 */
040public class BaseNCodecOutputStream extends FilterOutputStream {
041
042    private final boolean doEncode;
043
044    private final BaseNCodec baseNCodec;
045
046    private final byte[] singleByte = new byte[1];
047
048    private final Context context = new Context();
049
050    /**
051     * TODO should this be protected?
052     *
053     * @param outputStream the underlying output or null.
054     * @param basedCodec a BaseNCodec.
055     * @param doEncode true to encode, false to decode, TODO should be an enum?
056     */
057    public BaseNCodecOutputStream(final OutputStream outputStream, final BaseNCodec basedCodec, final boolean doEncode) {
058        super(outputStream);
059        this.baseNCodec = basedCodec;
060        this.doEncode = doEncode;
061    }
062
063    /**
064     * Closes this output stream and releases any system resources associated with the stream.
065     * <p>
066     * To write the EOF marker without closing the stream, call {@link #eof()} or use an
067     * <a href="https://commons.apache.org/proper/commons-io/">Apache Commons IO</a> <a href=
068     * "https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/output/CloseShieldOutputStream.html"
069     * >CloseShieldOutputStream</a>.
070     * </p>
071     *
072     * @throws IOException
073     *             if an I/O error occurs.
074     */
075    @Override
076    public void close() throws IOException {
077        eof();
078        flush();
079        out.close();
080    }
081
082    /**
083     * Writes EOF.
084     *
085     * @since 1.11
086     */
087    public void eof() {
088        // Notify encoder of EOF (-1).
089        if (doEncode) {
090            baseNCodec.encode(singleByte, 0, EOF, context);
091        } else {
092            baseNCodec.decode(singleByte, 0, EOF, context);
093        }
094    }
095
096    /**
097     * Flushes this output stream and forces any buffered output bytes to be written out to the stream.
098     *
099     * @throws IOException
100     *             if an I/O error occurs.
101     */
102    @Override
103    public void flush() throws IOException {
104        flush(true);
105    }
106
107    /**
108     * Flushes this output stream and forces any buffered output bytes to be written out to the stream. If propagate is
109     * true, the wrapped stream will also be flushed.
110     *
111     * @param propagate
112     *            boolean flag to indicate whether the wrapped OutputStream should also be flushed.
113     * @throws IOException
114     *             if an I/O error occurs.
115     */
116    private void flush(final boolean propagate) throws IOException {
117        final int avail = baseNCodec.available(context);
118        if (avail > 0) {
119            final byte[] buf = new byte[avail];
120            final int c = baseNCodec.readResults(buf, 0, avail, context);
121            if (c > 0) {
122                out.write(buf, 0, c);
123            }
124        }
125        if (propagate) {
126            out.flush();
127        }
128    }
129
130    /**
131     * Returns true if decoding behavior is strict. Decoding will raise an
132     * {@link IllegalArgumentException} if trailing bits are not part of a valid encoding.
133     *
134     * <p>
135     * The default is false for lenient encoding. Decoding will compose trailing bits
136     * into 8-bit bytes and discard the remainder.
137     * </p>
138     *
139     * @return true if using strict decoding
140     * @since 1.15
141     */
142    public boolean isStrictDecoding() {
143        return baseNCodec.isStrictDecoding();
144    }
145
146    /**
147     * Writes {@code len} bytes from the specified {@code b} array starting at {@code offset} to this
148     * output stream.
149     *
150     * @param array
151     *            source byte array
152     * @param offset
153     *            where to start reading the bytes
154     * @param len
155     *            maximum number of bytes to write
156     *
157     * @throws IOException
158     *             if an I/O error occurs.
159     * @throws NullPointerException
160     *             if the byte array parameter is null
161     * @throws IndexOutOfBoundsException
162     *             if offset, len or buffer size are invalid
163     */
164    @Override
165    public void write(final byte[] array, final int offset, final int len) throws IOException {
166        Objects.requireNonNull(array, "array");
167        if (offset < 0 || len < 0) {
168            throw new IndexOutOfBoundsException();
169        }
170        if (offset > array.length || offset + len > array.length) {
171            throw new IndexOutOfBoundsException();
172        }
173        if (len > 0) {
174            if (doEncode) {
175                baseNCodec.encode(array, offset, len, context);
176            } else {
177                baseNCodec.decode(array, offset, len, context);
178            }
179            flush(false);
180        }
181    }
182
183    /**
184     * Writes the specified {@code byte} to this output stream.
185     *
186     * @param i
187     *            source byte
188     * @throws IOException
189     *             if an I/O error occurs.
190     */
191    @Override
192    public void write(final int i) throws IOException {
193        singleByte[0] = (byte) i;
194        write(singleByte, 0, 1);
195    }
196
197}