34

Having two InputStreams in Java, is there a way to merge them so you end with one InputStream that gives you the output of both streams? How?

Jonas
  • 121,568
  • 97
  • 310
  • 388
Pablo Fernandez
  • 279,434
  • 135
  • 377
  • 622
  • 3
    Merge in what way exactly? Seamlessly continue reading from a second stream after the first has been read from? I'm not very familiar with Java, but in C# you could do this easily enough by implementing a class inheriting from Stream containing references to both base streams and then overriding the Read method. – Noldorin Apr 17 '09 at 12:45

5 Answers5

58

As commented, it's not clear what you mean by merge.

Taking available input "randomly" from either is complicated by InputStream.available not necessarily giving you a useful answer and blocking behaviour of streams. You would need two threads to be reading from the streams and then passing back data through, say, java.io.Piped(In|Out)putStream (although those classes have issues). Alternatively for some types of stream it may be possible to use a different interface, for instance java.nio non-blocking channels.

If you want the full contents of the first input stream followed by the second: new java.io.SequenceInputStream(s1, s2).

Tom Hawtin - tackline
  • 145,806
  • 30
  • 211
  • 305
  • 4
    Oh, very nice, I just learnt something new. SequenceInputStream is essentially identical to my CatInputStream, but using legacy Enumerations instead of, say, a LinkedList. :-) – C. K. Young Apr 17 '09 at 13:03
  • As a hack to the first part of your answer, it's hard to solve in the general case but for specific cases of FileInputStream (and maybe sockets too?) you can instanceof/cast and create a channel out of it. (The other streams can use Channels.newChannel to create a consistent interface, but won't have the non-blocking qualities required.) – C. K. Young Apr 17 '09 at 13:18
  • 2
    Collections.enumeration is your friend. I forgot a part of my first part - will edit. – Tom Hawtin - tackline Apr 17 '09 at 14:37
  • Actually FileChannel is not a SelectableChannel, although SocketChannel is. So I believe you're a bit stuck with files. Until "More NIO Features" in JDK7 (probably). – Tom Hawtin - tackline Apr 17 '09 at 14:43
  • NIO2 ftw; it will feature asynchronous I/O too, which will make implementing this merged-channel business much more fun (a callback on any input channel causes a callback to the merged channel). :-) – C. K. Young Apr 17 '09 at 15:35
  • My desk used to be next to that of the spec lead. :-) – Tom Hawtin - tackline Apr 17 '09 at 16:04
  • Hah, what a small world. :-D Alan Bateman got in touch with me late last year, regarding my efforts to port NIO2 to IcedTea (I even have an SO question about it); sadly on that front, I since got a full-time job and never found the time to return to it. But, the good news is that the work's not wasted; GNU/Andrew from Red Hat (who does a lot of work with IcedTea) has since picked it up: http://blog.fuseyism.com/index.php/2008/11/21/icedtea-18-released/ – C. K. Young Apr 18 '09 at 05:52
23

java.io.SequenceInputStream might be what you need. It accepts an enumeration of streams, and will output the contents of the first stream, then the second, and so on until all streams are empty.

4

You can write a custom InputStream implementation that does this. Example:

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;

public class CatInputStream extends InputStream {
    private final Deque<InputStream> streams;

    public CatInputStream(InputStream... streams) {
        this.streams = new LinkedList<InputStream>();
        Collections.addAll(this.streams, streams);
    }

    private void nextStream() throws IOException {
        streams.removeFirst().close();
    }

    @Override
    public int read() throws IOException {
        int result = -1;
        while (!streams.isEmpty()
                && (result = streams.getFirst().read()) == -1) {
            nextStream();
        }
        return result;
    }

    @Override
    public int read(byte b[], int off, int len) throws IOException {
        int result = -1;
        while (!streams.isEmpty()
                && (result = streams.getFirst().read(b, off, len)) == -1) {
            nextStream();
        }
        return result;
    }

    @Override
    public long skip(long n) throws IOException {
        long skipped = 0L;
        while (skipped < n && !streams.isEmpty()) {
            int thisSkip = streams.getFirst().skip(n - skipped);
            if (thisSkip > 0)
                skipped += thisSkip;
            else
                nextStream();
        }
        return skipped;
    }

    @Override
    public int available() throws IOException {
        return streams.isEmpty() ? 0 : streams.getFirst().available();
    }

    @Override
    public void close() throws IOException {
        while (!streams.isEmpty())
            nextStream();
    }
}

This code isn't tested, so your mileage may vary.

C. K. Young
  • 219,335
  • 46
  • 382
  • 435
  • Doesn't this do the same thing as SequenceInputStream, as suggested by Merzbow? – Sam Barnum Apr 17 '09 at 14:59
  • Sorry, but tackline suggested SequenceInputStream first (and I +1'd him for that). In SO, the earliest good answer wins; you never know if later answers are plagiarisms. Also, read my comment on tackline's answer for a comparison between SequenceInputStream and CatInputStream (I do take his point about using Collections.enumeration). – C. K. Young Apr 17 '09 at 15:39
  • If the person who downvoted my answer did so because of my last comment, I apologise; I should explain better: if I write an answer that turns out to be a duplicate (or subset) of an earlier-posted answer, I usually delete it, knowing that I will never receive any points for it. On SO, it really is "Fastest Gun in the West" (search for that question title). – C. K. Young Apr 17 '09 at 15:51
0

Here is an MVar implementation specific to byte arrays (make sure to add your own package definition). From here, it is trivial to write an input stream on merged streams. I can post that too if requested.

import java.nio.ByteBuffer;

public final class MVar {

  private static enum State {
    EMPTY, ONE, MANY
  }

  private final Object lock;

  private State state;

  private byte b;

  private ByteBuffer bytes;
  private int length;

  public MVar() {
    lock = new Object();
    state = State.EMPTY;
  }

  public final void put(byte b) {
    synchronized (lock) {
      while (state != State.EMPTY) {
        try {
          lock.wait();
        } catch (InterruptedException e) {}
      }
      this.b = b;
      state = State.ONE;
      lock.notifyAll();
    }
  }

  public final void put(byte[] bytes, int offset, int length) {
    if (length == 0) {
      return;
    }
    synchronized (lock) {
      while (state != State.EMPTY) {
        try {
          lock.wait();
        } catch (InterruptedException e) {}
      }
      this.bytes = ByteBuffer.allocateDirect(length);
      this.bytes.put(bytes, offset, length);
      this.bytes.position(0);
      this.length = length;
      state = State.MANY;
      lock.notifyAll();
    }
  }

  public final byte take() {
    synchronized (lock) {
      while (state == State.EMPTY) {
        try {
          lock.wait();
        } catch (InterruptedException e) {}
      }
      switch (state) {
      case ONE: {
        state = State.EMPTY;
        byte b = this.b;
        lock.notifyAll();
        return b;
      }
      case MANY: {
        byte b = bytes.get();
        state = --length <= 0 ? State.EMPTY : State.MANY;
        lock.notifyAll();
        return b;
      }
      default:
        throw new AssertionError();
      }
    }
  }

  public final int take(byte[] bytes, int offset, int length) {
    if (length == 0) {
      return 0;
    }
    synchronized (lock) {
      while (state == State.EMPTY) {
        try {
          lock.wait();
        } catch (InterruptedException e) {}
      }
      switch (state) {
      case ONE:
        bytes[offset] = b;
        state = State.EMPTY;
        lock.notifyAll();
        return 1;
      case MANY:
        if (this.length > length) {
          this.bytes.get(bytes, offset, length);
          this.length = this.length - length;
          synchronized (lock) {
            lock.notifyAll();
          }
          return length;
        }
        this.bytes.get(bytes, offset, this.length);
        this.bytes = null;
        state = State.EMPTY;
        length = this.length;
        lock.notifyAll();
        return length;
      default:
        throw new AssertionError();
      }
    }
  }
}
ScootyPuff
  • 1,335
  • 9
  • 18
0

Not that I can think of. You would probably have to read the contents of the two stream into a byte[] and then create a ByteArrayInputStream from that.

willcodejavaforfood
  • 43,223
  • 17
  • 81
  • 111