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