package net.sf.expectit; /* * #%L * ExpectIt * %% * Copyright (C) 2014 Alexey Gavrilov and contributors * %% * 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. * #L% */ import static net.sf.expectit.Utils.toDebugString; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.Pipe; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.charset.Charset; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.expectit.filter.Filter; import net.sf.expectit.matcher.Matcher; /** * Represents a single inputs. */ class SingleInputExpect { private static final Logger LOG = Logger.getLogger(SingleInputExpect.class.getName()); public static final int BUFFER_SIZE = 1024; private final InputStream input; private final StringBuilder buffer; private final Charset charset; private final Appendable echoInput; private final Filter filter; private Future<Object> copierFuture; private final Pipe.SourceChannel source; private final Pipe.SinkChannel sink; private final int bufferSize; private final boolean autoFlushEcho; protected SingleInputExpect( final Pipe.SourceChannel source, final Pipe.SinkChannel sink, final InputStream input, final Charset charset, final Appendable echoInput, final Filter filter, final int bufferSize, final boolean autoFlushEcho) throws IOException { this.input = input; this.charset = charset; this.echoInput = echoInput; this.filter = filter; this.bufferSize = bufferSize; this.autoFlushEcho = autoFlushEcho; this.source = source; this.sink = sink; source.configureBlocking(false); buffer = new StringBuilder(); } public void start(ExecutorService executor) { if (LOG.isLoggable(Level.FINE)) { LOG.fine( String.format( "Starting expect thread: input=%s, charset=%s, echoInput=%s, " + "filter=%s, bufferSize=%d", input, charset, echoInput, filter, bufferSize ) ); } copierFuture = executor.submit( new InputStreamCopier( sink, input, bufferSize, echoInput, charset, autoFlushEcho)); } public <R extends Result> R expect(long timeoutMs, Matcher<R> matcher) throws IOException { if (copierFuture == null) { throw new IllegalStateException("Not started"); } final long timeToStop = System.currentTimeMillis() + timeoutMs; final boolean isInfiniteTimeout = timeoutMs == ExpectImpl.INFINITE_TIMEOUT; long timeElapsed = timeoutMs; ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE); Selector selector = Selector.open(); try { source.register(selector, SelectionKey.OP_READ); R result = matcher.matches(buffer.toString(), copierFuture.isDone()); if (LOG.isLoggable(Level.FINE)) { LOG.fine( String.format( "Initial matcher %s result: %s", toDebugString(matcher), toDebugString(result))); } while (!(result.isSuccessful() || result.canStopMatching()) && (isInfiniteTimeout || timeElapsed > 0)) { int keys = isInfiniteTimeout ? selector.select() : selector.select(timeElapsed); // if thread was interrupted the selector returns immediately // and keep the thread status, so we need to check it if (Thread.currentThread().isInterrupted()) { LOG.fine("Thread was interrupted"); throw new ClosedByInterruptException(); } if (!isInfiniteTimeout) { timeElapsed = timeToStop - System.currentTimeMillis(); } if (keys == 0) { LOG.fine("Selector returns 0 key"); continue; } selector.selectedKeys().clear(); int len = source.read(byteBuffer); if (len > 0) { String string = new String(byteBuffer.array(), 0, len, charset); processString(string); byteBuffer.clear(); } result = matcher.matches(buffer.toString(), len == -1); if (LOG.isLoggable(Level.FINE)) { LOG.fine( String.format( "Matcher %s result: %s. Operation time: %d ms", toDebugString(matcher), toDebugString(result), timeoutMs - timeElapsed)); } } if (result.isSuccessful()) { buffer.delete(0, result.end()); } else if (copierFuture.isDone() && buffer.length() == 0) { throw new EOFException("Input closed"); } return result; } finally { selector.close(); } } private void processString(String string) throws IOException { if (LOG.isLoggable(Level.FINE)) { LOG.fine("Processing string: " + toDebugString(string)); } if (filter != null) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("Before append filter: " + toDebugString(filter)); } string = filter.beforeAppend(string, buffer); } if (string != null) { buffer.append(string); if (filter != null) { LOG.fine("After append filter: " + toDebugString(filter)); filter.afterAppend(buffer); } } } public void stop() throws IOException { if (LOG.isLoggable(Level.FINE)) { LOG.fine("Releasing resources for input: " + this.input); } if (copierFuture != null) { copierFuture.cancel(true); } sink.close(); source.close(); if (autoFlushEcho) { Utils.flushAppendable(echoInput); } } StringBuilder getBuffer() { return buffer; } }