/* * JBoss, Home of Professional Open Source. * * Copyright 2013 Red Hat, Inc. and/or its affiliates, and individual * contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.xnio.mock; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; import org.xnio.Buffers; import org.xnio.IoUtils; import org.xnio.OptionMap; import org.xnio.StreamConnection; import org.xnio.XnioIoThread; import org.xnio.XnioWorker; import org.xnio.channels.AssembledConnectedStreamChannel; import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; import org.xnio.conduits.Conduits; import org.xnio.conduits.ReadReadyHandler; import org.xnio.conduits.StreamSinkConduit; import org.xnio.conduits.StreamSourceConduit; import org.xnio.conduits.WriteReadyHandler; /** * Mock of a sink/source conduit.<p> * This channel mock will store everything that is written to it for later comparison, and allows feeding of bytes for * reading. * * @author <a href="mailto:frainone@redhat.com">Flavia Rainone</a> */ public class ConduitMock implements StreamSinkConduit, StreamSourceConduit, Mock{ // written stuff will be copied to this buffer private ByteBuffer writeBuffer = ByteBuffer.allocate(1000); // read stuff will be taken from this buffer private ByteBuffer readBuffer = ByteBuffer.allocate(10000); // if eof is true, read will return -1 if readBuffer is empty private boolean eof = false; // read stuff can only be read if read operations are enabled private boolean readsEnabled; // can only write when write operations are enabled private boolean writesEnabled = true; // terminateWrites() will be ignored if allowTerminateWrites is false private boolean allowTerminateWrites = true; // is flush enabled private boolean flushEnabled = true; // enables check for closed conduit (if an attempt to perform an operation is performed once this conduit is // closed, a ClosedChannelException will be thrown only if checkClosed is true) private boolean checkClosed = true; // is write operation resumed private boolean writesResumed = false; // is write operation awaken private boolean writesAwaken = false; // is write operation terminated private boolean writesTerminated = false; // is write operation truncated private boolean writesTruncated = false; // are all written contents flushed private boolean flushed = true; // is read operation resumed private boolean readsResumed = false; // is read operation awaken private boolean readsAwaken = false; // is read operation terminated private boolean readsTerminated = false; // indicates if this conduit is closed private boolean closed = false; // the worker private XnioWorker worker; // the executor private XnioIoThread executor; // read waiter private Thread readWaiter; // write waiter private Thread writeWaiter; // write ready handler // implement this when needed @SuppressWarnings("unused") private WriteReadyHandler writeReadyHandler; // read ready handler // implement this when needed @SuppressWarnings("unused") private ReadReadyHandler readReadyHandler; // any extra information regarding this channel used by tests private String info = null; public ConduitMock(XnioWorker worker, XnioIoThread xnioIoThread) { this.executor = xnioIoThread; this.worker = worker; } /** * Returns the executor. */ XnioIoThread getXnioIoThread() { return executor; } // implement this for handlers when needed // listener setters // private final ChannelListener.Setter<ConnectedStreamChannel> readListenerSetter = new ChannelListener.Setter<ConnectedStreamChannel>() { // @Override // public void set(ChannelListener<? super ConnectedStreamChannel> listener) { // readListener = listener; // } // }; // // private final ChannelListener.Setter<ConnectedStreamChannel> writeListenerSetter = new ChannelListener.Setter<ConnectedStreamChannel>() { // @Override // public void set(ChannelListener<? super ConnectedStreamChannel> listener) { // writeListener = listener; // } // }; // // private final ChannelListener.Setter<ConnectedStreamChannel> closeListenerSetter = new ChannelListener.Setter<ConnectedStreamChannel>() { // @Override // public void set(ChannelListener<? super ConnectedStreamChannel> listener) { // closeListener = listener; // } // }; /** * Feeds {@code readData} to read clients. * @param readData data that will be available for reading */ public void setReadData(String... readData) { final Thread waiter; synchronized (this) { int totalLength = 0; for (String data: readData) { totalLength += data.length(); } int position = readBuffer.position(); boolean resetPosition = false; if (!readBuffer.hasRemaining()) { readBuffer.compact(); } else if(readBuffer.position() > 0 || readBuffer.limit() != readBuffer.capacity()) { if (readBuffer.capacity() - readBuffer.limit() < totalLength) { if (readBuffer.position() > 0 && readBuffer.capacity() - readBuffer.limit() + readBuffer.position() >= totalLength) { readBuffer.compact(); } throw new RuntimeException("ReadBuffer is full - not enough space to add more read data"); } int limit = readBuffer.limit(); readBuffer.position(limit); readBuffer.limit(limit += totalLength); resetPosition = true; } for (String data: readData) { try { readBuffer.put(data.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } readBuffer.flip(); if (resetPosition) { readBuffer.position(position); } if (readWaiter == null || totalLength == 0 || !readsEnabled) { return; } waiter = readWaiter; readWaiter = null; } LockSupport.unpark(waiter); } /** * Feeds {@code readData} to read clients. * @param readData data that will be available for reading */ public void setReadDataWithLength(String... readData) { final Thread waiter; synchronized (this) { if (eof == true) { throw new IllegalStateException("Cannot add read data once eof is set"); } int totalLength = 0; for (String data: readData) { totalLength += data.length(); } int position = readBuffer.position(); boolean resetPosition = false; if (!readBuffer.hasRemaining()) { readBuffer.compact(); } else if(readBuffer.position() > 0 || readBuffer.limit() != readBuffer.capacity()) { if (readBuffer.capacity() - readBuffer.limit() + 4 < totalLength) { if (readBuffer.position() > 0 && readBuffer.capacity() - readBuffer.limit() + readBuffer.position() + 4 >= totalLength) { readBuffer.compact(); } throw new RuntimeException("ReadBuffer is full - not enough space to add more read data"); } int limit = readBuffer.limit(); readBuffer.position(limit); readBuffer.limit(limit += totalLength + 4); resetPosition = true; } readBuffer.putInt(totalLength); for (String data: readData) { try { readBuffer.put(data.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } readBuffer.flip(); if (resetPosition) { readBuffer.position(position); } if (readWaiter == null || totalLength == 0 || !readsEnabled) { return; } waiter = readWaiter; } LockSupport.unpark(waiter); } /** * Feeds {@code readData} to read clients. * @param readData data that will be available for reading on this channel mock */ public void setReadDataWithLength(int length, String... readData) { final Thread waiter; synchronized (this) { if (eof == true) { throw new IllegalStateException("Cannot add read data once eof is set"); } int totalLength = 0; for (String data: readData) { totalLength += data.length(); } int position = readBuffer.position(); boolean resetPosition = false; if (!readBuffer.hasRemaining()) { readBuffer.compact(); } else if(readBuffer.position() > 0 || readBuffer.limit() != readBuffer.capacity()) { if (readBuffer.capacity() - readBuffer.limit() + 4 < totalLength) { if (readBuffer.position() > 0 && readBuffer.capacity() - readBuffer.limit() + readBuffer.position() + 4 >= totalLength) { readBuffer.compact(); } throw new RuntimeException("ReadBuffer is full - not enough space to add more read data"); } int limit = readBuffer.limit(); readBuffer.position(limit); readBuffer.limit(limit += totalLength + 4); resetPosition = true; } readBuffer.putInt(length); for (String data: readData) { try { readBuffer.put(data.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } readBuffer.flip(); if (resetPosition) { readBuffer.position(position); } if (readWaiter == null || totalLength == 0 || !readsEnabled) { return; } waiter = readWaiter; } LockSupport.unpark(waiter); } /** * Marks the eof for read operations. Once eof is set, all read operations will return -1 as soon as there is no * read data available. */ public void setEof() { final Thread waiter; synchronized (this) { eof = true; if (readWaiter == null || !readsEnabled) { return; } waiter = readWaiter; } LockSupport.unpark(waiter); } /** * Indicates if has all read data been consumed by read operations. */ public synchronized boolean allReadDataConsumed() { return readBuffer.position() == readBuffer.limit(); } /** * Enables and disables read operations. If read operations are disabled, read will always return 0, even if * there is {@link #setReadData(String...) read data available} in the local buffer. * <p> * Read operations are disabled by default. * * @param enable {@code false} for disabling reads, {@code true} for enabling. */ public void enableReads(boolean enable) { final Thread waiter; synchronized (this) { readsEnabled = enable; if (readWaiter == null || !readsEnabled || !((readBuffer.hasRemaining() && readBuffer.limit() != readBuffer.capacity()) || eof)) { return; } waiter = readWaiter; } LockSupport.unpark(waiter); } /** * Enables and disables write operations. If write operations are disabled, write will always return 0. * <p> * Write operations are enabled by default. * * @param enable {@code false} for disabling writes, {@code true} for enabling. */ public void enableWrites(boolean enable) { final Thread waiter; synchronized (this) { writesEnabled = enable; waiter = writeWaiter; } if (waiter != null) { LockSupport.unpark(waiter); } } /** * Enables check for closed. This will result in a ClosedChannelException is an attempt to execute an operation * is performed when this conduit is closed. If closed check is disabled, any operation can be performed on this * mock regardless of whether it is closed. * <p> * This check is enabled by default. * * @param enable {@code true} for enabling the closed check, {@code false} for disabling it */ public synchronized void enableClosedCheck(boolean enable) { checkClosed = enable; } /** * Returns all the bytes that have been written to this conduit mock. * * @return the written bytes in the form of a UTF-8 string */ public String getWrittenText() { if (writeBuffer.position() == 0 && writeBuffer.limit() == writeBuffer.capacity()) { return ""; } writeBuffer.flip(); return Buffers.getModifiedUtf8(writeBuffer); } /** * Returns the written bytes buffer * * @return the buffer containing all the bytes that have been written to this conduit mock */ public ByteBuffer getWrittenBytes() { return writeBuffer; } /** * Indicates if all data written to this conduit has been flushed. * @return */ public boolean isFlushed() { return flushed; } /** * Enables and disables flush. If flush is disabled, requests to flush data are ignored. * <p> * Flush is enabled by default. * * @param enable {@code true} for enabling flush, {@code false} for disabling */ public synchronized void enableFlush(boolean enable) { flushEnabled = enable; } /** * Changes the worker associated with this conduit mock. */ public void setWorker(XnioWorker worker) { this.worker = worker; } @Override public OptionMap getOptionMap() { return optionMap; } @Override public String getInfo() { return info; } @Override public void setInfo(String i) { info = i; } public boolean isOpen() { return !writesTerminated || !readsTerminated; } private OptionMap optionMap; // review this // @Override // public boolean supportsOption(Option<?> option) { // return optionMap == null? false: optionMap.contains(option); // } // // @Override // public <T> T getOption(Option<T> option) throws IOException { // return optionMap == null? null: optionMap.get(option); // } // // @Override // public <T> T setOption(Option<T> option, T value) throws IllegalArgumentException, IOException { // final OptionMap.Builder optionMapBuilder = OptionMap.builder(); // T previousValue = null; // if (optionMap != null) { // optionMapBuilder.addAll(optionMap); // previousValue = optionMap.get(option); // } // optionMapBuilder.set(option, value); // optionMap = optionMapBuilder.getMap(); // return previousValue; // } public void setOptionMap(OptionMap optionMap) { this.optionMap = optionMap; } @Override public void suspendReads() { readsAwaken = false; readsResumed = false; } @Override public void resumeReads() { readsResumed = true; } @Override public boolean isReadResumed() { return readsResumed; } @Override public void terminateReads() throws IOException { readsTerminated = true; return; } @Override public synchronized boolean isReadShutdown() { return readsTerminated; } /** * This mock does not support more than one read thread waiter at the same time. */ @Override public void awaitReadable() throws IOException { synchronized(this) { if (readWaiter != null) { throw new IllegalStateException("ConnectedStreamChannelMock can be used only with one read waiter thread at most... there is already a waiting thread" + readWaiter); } if (((readBuffer.hasRemaining() && readBuffer.capacity() != readBuffer.limit()) || eof) && readsEnabled) { return; } readWaiter = Thread.currentThread(); } LockSupport.park(readWaiter); synchronized(this) { readWaiter = null; } } /** * This mock does not support more than one read thread waiter at the same time. */ @Override public void awaitReadable(long time, TimeUnit timeUnit) throws IOException { synchronized (this) { if (readWaiter != null) { throw new IllegalStateException("ConnectedStreamChannelMock can be used only with one read waiter thread at most... there is already a waiting thread" + readWaiter); } if (((readBuffer.hasRemaining() && readBuffer.capacity() != readBuffer.limit()) || eof) && readsEnabled) { return; } readWaiter = Thread.currentThread(); } // FIXME assertSame("ConnectedStreamChannelMock.awaitReadable(long, TimeUnit) can be used only with TimeUnit.NANOSECONDS", TimeUnit.MILLISECONDS, timeUnit); LockSupport.parkNanos(readWaiter, timeUnit.toNanos(time)); synchronized (this) { readWaiter = null; } } @Override public void suspendWrites() { writesAwaken = false; writesResumed = false; } @Override public void resumeWrites() { writesResumed = true; } @Override public boolean isWriteResumed() { return writesResumed; } @Override public synchronized void terminateWrites() throws IOException { if (!allowTerminateWrites) { return; } writesTerminated = true; final Thread waiter; synchronized (this) { if (readWaiter == null) { return; } waiter = readWaiter; } LockSupport.unpark(waiter); return; } @Override public synchronized void truncateWrites() throws IOException { terminateWrites(); writesTruncated = true; } @Override public synchronized boolean isWriteShutdown() { return writesTerminated; } public synchronized boolean isWriteTruncated() { return writesTruncated; } /** * This mock does not support more than one read thread waiter at the same time. */ @Override public void awaitWritable() throws IOException { synchronized(this) { if (writeWaiter != null) { throw new IllegalStateException("ConnectedStreamChannelMock can be used only with one write waiter thread at most... there is already a waiting thread" + writeWaiter); } if (writesEnabled) { return; } writeWaiter = Thread.currentThread(); } LockSupport.park(writeWaiter); synchronized(this) { writeWaiter = null; } } /** * This mock does not support more than one write thread waiter at the same time. */ @Override public void awaitWritable(long time, TimeUnit timeUnit) throws IOException { synchronized (this) { if (writeWaiter != null) { throw new IllegalStateException("ConnectedStreamChannelMock can be used only with one write waiter thread at most... there is already a waiting thread" + writeWaiter); } if (writesEnabled) { return; } writeWaiter = Thread.currentThread(); } // FIXME assertSame("ConnectedStreamChannelMock.awaitWritable(long, TimeUnit) can be used only with TimeUnit.NANOSECONDS", TimeUnit.NANOSECONDS, timeUnit); LockSupport.parkNanos(writeWaiter, timeUnit.toNanos(time)); synchronized (this) { writeWaiter = null; } } @Override public XnioIoThread getWriteThread() { return executor; } @Override public synchronized boolean flush() throws IOException { if (flushEnabled) { flushed = true; } return flushed; } @Override public long transferFrom(FileChannel src, long position, long count) throws IOException { if (writesEnabled) { final StreamConnection connection = new StreamConnectionMock(this); final AssembledConnectedStreamChannel assembledChannel = new AssembledConnectedStreamChannel(connection, connection.getSourceChannel(), connection.getSinkChannel()); return src.transferTo(position, count, assembledChannel); } return 0; } @Override public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { if (writesEnabled) { final StreamConnection connection = new StreamConnectionMock(this); final AssembledConnectedStreamChannel assembledChannel = new AssembledConnectedStreamChannel(connection, connection.getSourceChannel(), connection.getSinkChannel()); IoUtils.transfer(source, count, throughBuffer, assembledChannel); } return 0; } @Override public synchronized int write(ByteBuffer src) throws IOException { if (closed && checkClosed) { throw new ClosedChannelException(); } if (writesEnabled) { if (writeBuffer.limit() < writeBuffer.capacity()) { writeBuffer.limit(writeBuffer.capacity()); } int bytes = Buffers.copy(writeBuffer, src); if (bytes > 0) { flushed = false; } return bytes; } return 0; } @Override public synchronized long write(ByteBuffer[] srcs, int offset, int length) throws IOException { if (closed && checkClosed) { throw new ClosedChannelException(); } if (writesEnabled) { if (writeBuffer.limit() < writeBuffer.capacity()) { writeBuffer.limit(writeBuffer.capacity()); } int bytes = Buffers.copy(writeBuffer, srcs, offset, length); if (bytes > 0) { flushed = false; } return bytes; } return 0; } @Override public int writeFinal(ByteBuffer src) throws IOException { return Conduits.writeFinalBasic(this, src); } @Override public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { return Conduits.writeFinalBasic(this, srcs, offset, length); } @Override public long transferTo(long position, long count, FileChannel target) throws IOException { if (readsEnabled) { final StreamConnection connection = new StreamConnectionMock(this); final AssembledConnectedStreamChannel assembledChannel = new AssembledConnectedStreamChannel(connection, connection.getSourceChannel(), connection.getSinkChannel()); return target.transferFrom(assembledChannel, position, count); } return 0; } @Override public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { if (readsEnabled) { final StreamConnection connection = new StreamConnectionMock(this); final AssembledConnectedStreamChannel assembledChannel = new AssembledConnectedStreamChannel(connection, connection.getSourceChannel(), connection.getSinkChannel()); return IoUtils.transfer(assembledChannel, count, throughBuffer, target); } return 0; } @Override public synchronized int read(ByteBuffer dst) throws IOException { if (closed && checkClosed) { throw new ClosedChannelException(); } if (readsEnabled) { try { if ((!readBuffer.hasRemaining() || readBuffer.position() == 0 && readBuffer.limit() == readBuffer.capacity()) && eof) { return -1; } if (readBuffer.limit() == readBuffer.capacity() && readBuffer.position() == 0) { return 0; } return Buffers.copy(dst, readBuffer); } catch (RuntimeException e) { System.out.println("Got exception at attempt of copying contents of dst "+ dst.remaining() + " into read buffer " + readBuffer.remaining()); throw e; } } return 0; } @Override public synchronized long read(ByteBuffer[] dsts, int offset, int length) throws IOException { if (closed && checkClosed) { throw new ClosedChannelException(); } if (readsEnabled) { if ((!readBuffer.hasRemaining() || readBuffer.position() == 0 && readBuffer.limit() == readBuffer.capacity()) && eof) { return -1; } if (readBuffer.limit() == readBuffer.capacity() && readBuffer.position() == 0) { return 0; } return Buffers.copy(dsts, offset, length, readBuffer); } return 0; } @Override public XnioWorker getWorker() { return worker; } @Override public void wakeupReads() { readsAwaken = true; readsResumed = true; } public boolean isReadAwaken() { return readsAwaken; } @Override public void wakeupWrites() { writesAwaken = true; writesResumed = true; } public boolean isWriteAwaken() { return writesAwaken; } // implement this when needed // @Override // public Setter<? extends StreamConnection> getReadSetter() { // return readListenerSetter; // } // // @Override // public Setter<? extends ConnectedStreamChannel> getWriteSetter() { // return writeListenerSetter; // } // // @Override // public Setter<? extends ConnectedStreamChannel> getCloseSetter() { // return closeListenerSetter; // } // public ChannelListener<? super ConnectedStreamChannel> getReadListener() { // return readListener; // } // // public ChannelListener<? super ConnectedStreamChannel> getWriteListener() { // return writeListener; // } // // public ChannelListener<? super ConnectedStreamChannel> getCloseListener() { // return closeListener; // } @Override // make ready handler active when needed public synchronized void setWriteReadyHandler(WriteReadyHandler handler) { writeReadyHandler = handler; } @Override // make ready handler active when needed public void setReadReadyHandler(ReadReadyHandler handler) { readReadyHandler = handler; } @Override public XnioIoThread getReadThread() { return executor; } }