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