001    
002    /*
003     * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: OutputStreamWriter.java 162 2011-10-24 20:42:37Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.io;
009    
010    import java.io.FilterOutputStream;
011    import java.io.IOException;
012    import java.io.OutputStream;
013    
014    /**
015     * Serializes zero or more {@link OutputStream}s inside a single underlying {@link OutputStream}. The results can be
016     * deserialized as equally many distinct {@link java.io.InputStream}s on the other end using an {@link InputStreamReader}.
017     * Each {@link OutputStream} may contain an arbitrary amount of data.
018     *
019     * <p>
020     * To use this class, invoke {@link #start} to start a new {@link OutputStream}, write to it by writing to this
021     * class normally, and then use {@link #stop} to close the current {@link OutputStream}. A new, subsequent {@link OutputStream}
022     * is created by invoking {@link #start} again.
023     * </p>
024     *
025     * <p>
026     * Each {@link OutputStream} written in this way will be read as distinct {@link java.io.InputStream} by the
027     * {@link InputStreamReader} at the other end.
028     * </p>
029     *
030     * <p>
031     * Instances of this class are thread safe.
032     * </p>
033     *
034     * @see InputStreamReader
035     */
036    public class OutputStreamWriter extends FilterOutputStream {
037    
038        private final RandomEscape randomEscape = new RandomEscape();
039    
040        private int escape;
041        private boolean started;
042        private boolean closed;
043    
044        /**
045         * Constructor.
046         *
047         * @param output the underlying {@link OutputStream} that will carry nested {@link OutputStream}s within it
048         */
049        public OutputStreamWriter(OutputStream output) {
050            super(output);
051            this.escape = this.randomEscape.next();
052        }
053    
054        /**
055         * Start a new {@link OutputStream}.
056         *
057         * @throws IOException if this instance is closed
058         * @throws IOException if an {@link OutputStream} is already started
059         */
060        public synchronized void start() throws IOException {
061            if (this.closed)
062                throw new IOException("this instance is closed");
063            if (this.started)
064                throw new IOException("already started");
065            this.started = true;
066        }
067    
068        /**
069         * End the current {@link OutputStream}. This flushes the underlying output.
070         * A new {@link OutputStream} will be created upon the next invocation of {@link #start}.
071         *
072         * @throws IOException if this instance is closed
073         * @throws IOException if no {@link OutputStream} is currently started
074         * @throws IOException if the underlying {@link OutputStream} throws an exception
075         */
076        public synchronized void stop() throws IOException {
077            if (this.closed)
078                throw new IOException("this instance is closed");
079            if (!this.started)
080                throw new IOException("not started");
081            this.started = false;
082            this.writeControl(InputStreamReader.CONTROL_SEPARATOR);
083            this.flush();
084        }
085    
086        /**
087         * Close this instance. Does nothing if already closed.
088         * If there an {@link OutputStream} is already started when this method is invoked, it will be implicitly
089         * {@linkplain #stop stopped}.
090         *
091         * <p>
092         * This ends the current {@link OutputStream} and closes the underlying input.
093         * </p>
094         *
095         * @throws IOException if an there is an error closing the underlying {@link OutputStream}
096         */
097        @Override
098        public synchronized void close() throws IOException {
099            if (this.closed)
100                return;
101            if (this.started) {
102                this.started = false;
103                this.writeControl(InputStreamReader.CONTROL_SEPARATOR);
104            }
105            this.closed = true;
106            this.out.close();
107        }
108    
109        @Override
110        public synchronized void write(int ch) throws IOException {
111            if (this.closed)
112                throw new IOException("this instance is closed");
113            if (!this.started)
114                this.started = true;
115            if ((ch & 0xff) == this.escape) {
116                this.writeControl(InputStreamReader.CONTROL_ESCAPE);
117                return;
118            }
119            this.out.write(ch);
120        }
121    
122        private synchronized void writeControl(int control) throws IOException {
123            this.out.write(this.escape);
124            this.out.write(this.escape ^ control);
125            this.escape = this.randomEscape.next();
126        }
127    }
128