/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.catalog.resource.download; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.io.CountingOutputStream; /** * ReliableResourceCallable is responsible for reading product data from its @InputStream and then writing that data * to a @FileBackedOutputStream (that will be concurrently read by a client), and optionally caching the product to * the file system. It is a @Callable that is started via a @Future by the @ReliableResourceDownloadManager class. * * The client uses the @ReliableResourceInputStream to read from the @FileBackedOutputStream. * * This class will read bytes in chunks (whose size is specified by the caller) until it either reaches the EOF or it * is interrupted (either by an @IOException or the @CachedResource). * * If the @InputStream is read until the EOF, then a -1 is returned indicating the entire stream was read successfully. * Otherwise, the number of bytes read thus far is returned so that the caller can react accordingly (e.g., reopen the * product @InputStream and skip forward that many bytes already read). * */ public class ReliableResourceCallable implements Callable<ReliableResourceStatus> { private static final Logger LOGGER = LoggerFactory.getLogger(ReliableResourceCallable.class); private static final int END_OF_FILE = -1; private final Object lock; private InputStream input = null; private CountingOutputStream countingFbos; private FileOutputStream cacheFileOutputStream = null; /** * Since this long is read by a different thread needs to be AtomicLong to make setting its * value thread-safe. */ private AtomicLong bytesRead = new AtomicLong(0); private ReliableResourceStatus reliableResourceStatus; private int chunkSize; private boolean interruptDownload = false; private boolean cancelDownload = false; /** * Used when only downloading, no caching to @FileOutputStream because caching was disabled or * had previous failed attempt trying to cache the product. * * @param input * @param countingFbos * @param chunkSize */ public ReliableResourceCallable(InputStream input, CountingOutputStream countingFbos, int chunkSize, Object lock) { this(input, countingFbos, null, chunkSize, lock); } /** * Used when only caching, no writing to @FileBackedOutputStream because no client is * reading from it. * * @param input * @param fos * @param chunkSize */ public ReliableResourceCallable(InputStream input, FileOutputStream fos, int chunkSize, Object lock) { this(input, null, fos, chunkSize, lock); } /** * Used when downloading and caching the product. * * @param input the product @InputStream * @param countingFbos the FileBackedOutputStream that is written to, number of bytes written to it are counted * @param fos the @FileOutputStream that the cached product is written to * @param chunkSize the number of bytes to read from the product @InputStream per chunk */ public ReliableResourceCallable(InputStream input, CountingOutputStream countingFbos, FileOutputStream fos, int chunkSize, Object lock) { this.input = input; this.countingFbos = countingFbos; this.cacheFileOutputStream = fos; this.chunkSize = chunkSize; this.lock = lock; } /** * Returns the number of bytes read from the product's @InputStream. * * @return */ public long getBytesRead() { return bytesRead.get(); } /** * Called when a new Callable is created for new retry attempt at product retrieval * and the bytesRead from previous attempt need to be skipped forward. This method * allows caller to account for bytes read in previous attempt(s) in case another * retry is attempted and the cumulative amount of bytes read can be maintained. * * @param bytesRead */ public void setBytesRead(long bytesRead) { LOGGER.debug("Setting bytesRead = {}", bytesRead); this.bytesRead.set(bytesRead); } /** * Returns the current status of the resource download, e.g., COMPLETED, INTERRUPTED, * CANCELED, etc. * * @return */ public ReliableResourceStatus getReliableResourceStatus() { return reliableResourceStatus; } /** * Set to true to indicate that the current resource download should be interrupted. Usually * set by the @ResourceRetrievalMonitor when there has been a pause of n seconds where no bytes * have been read from the resource's @InputStream. * * @param interruptDownload */ public void setInterruptDownload(boolean interruptDownload) { LOGGER.debug("Setting interruptDownload = {}", interruptDownload); this.interruptDownload = interruptDownload; // Set caching status here because it takes time for the Future running this // Callable to be canceled and the ReliableResourceStatus may be retrieved before // the call() method is interrupted LOGGER.debug("Download interrupted - returning {} bytes read", bytesRead); reliableResourceStatus = new ReliableResourceStatus(DownloadStatus.RESOURCE_DOWNLOAD_INTERRUPTED, bytesRead.get()); reliableResourceStatus.setMessage( "Download interrupted - returning " + bytesRead + " bytes read"); } /** * Set to true when the current resource download should be canceled. Usually set by the * @ReliableResourceInputStream when it is closed by the client. * * @param cancelDownload */ public void setCancelDownload(boolean cancelDownload) { LOGGER.debug("Setting cancelDownload = {}", cancelDownload); this.cancelDownload = cancelDownload; // Set caching status here because it takes time for the Future running this // Callable to be canceled and the ReliableResourceStatus may be retrieved before // the call() method is interrupted LOGGER.debug("Download canceled - returning {} bytes read", bytesRead); reliableResourceStatus = new ReliableResourceStatus(DownloadStatus.RESOURCE_DOWNLOAD_CANCELED, bytesRead.get()); reliableResourceStatus.setMessage( "Download canceled - returning " + bytesRead + " bytes read"); } @Override public ReliableResourceStatus call() { int chunkCount = 0; byte[] buffer = new byte[chunkSize]; int n = 0; while (!interruptDownload && !cancelDownload && !Thread.interrupted() && input != null) { chunkCount++; try { // Note that this blocking read() cannot be interrupted - this is why the // ResourceRetrievalMonitor must cancel the Future that this Callable is running in. // Otherwise, this read will block until the original resource request, usually by // CXF, times out waiting for bytes to be written to the FBOS. n = input.read(buffer); } catch (IOException e) { if (interruptDownload || Thread.interrupted()) { if (reliableResourceStatus != null) { reliableResourceStatus.setMessage( "IOException during read of product's InputStream"); } return reliableResourceStatus; } LOGGER.info("IOException during read of product's InputStream - bytesRead = {}", bytesRead.get(), e); reliableResourceStatus = new ReliableResourceStatus(DownloadStatus.PRODUCT_INPUT_STREAM_EXCEPTION, bytesRead.get()); return reliableResourceStatus; } LOGGER.trace("AFTER read() - n = {}", n); if (n == END_OF_FILE) { break; } // Synchronized to prevent being interrupted in the middle of writing to the // OutputStreams synchronized (lock) { // If download was interrupted or canceled break now so that the bytesRead count does // not // get out of sync with the bytesWritten counts. If this count gets out of sync // then potentially the output streams will be one chunk off from the input stream // when a retry is attempted and the InputStream is skipped forward. if (cancelDownload || interruptDownload || Thread.interrupted()) { LOGGER.debug("Breaking from download loop due to cancel or interrupt received"); if (reliableResourceStatus != null) { reliableResourceStatus.setMessage( "Breaking from download loop due to cancel or interrupt received"); } return reliableResourceStatus; } bytesRead.addAndGet(n); if (cacheFileOutputStream != null) { try { cacheFileOutputStream.write(buffer, 0, n); } catch (IOException e) { LOGGER.info("IOException during write to cached file's OutputStream", e); reliableResourceStatus = new ReliableResourceStatus(DownloadStatus.CACHED_FILE_OUTPUT_STREAM_EXCEPTION, bytesRead.get()); } } if (countingFbos != null) { try { countingFbos.write(buffer, 0, n); countingFbos.flush(); } catch (IOException e) { LOGGER.info( "IOException during write to FileBackedOutputStream for client to read", e); reliableResourceStatus = new ReliableResourceStatus(DownloadStatus.CLIENT_OUTPUT_STREAM_EXCEPTION, bytesRead.get()); } } // Return status here so that each stream can be attempted to be updated regardless of // which one might have had an exception if (reliableResourceStatus != null) { return reliableResourceStatus; } } LOGGER.trace("chunkCount = {}, bytesRead = {}", chunkCount, bytesRead.get()); } if (!interruptDownload && !cancelDownload && !Thread.interrupted()) { LOGGER.debug("Entire file downloaded successfully"); reliableResourceStatus = new ReliableResourceStatus(DownloadStatus.RESOURCE_DOWNLOAD_COMPLETE, bytesRead.get()); reliableResourceStatus.setMessage("Download completed successfully"); } return reliableResourceStatus; } }