org.kawanfw.file.api.client.RemoteInputStream Maven / Gradle / Ivy
Show all versions of awake-file-server Show documentation
/*
* This file is part of Awake FILE.
* Awake file: Easy file upload & download over HTTP with Java.
* Copyright (C) 2015, KawanSoft SAS
* (http://www.kawansoft.com). All rights reserved.
*
* Awake FILE 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 2.1 of the License, or (at your option) any later version.
*
* Awake FILE 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Any modifications to this file must keep this entire header
* intact.
*/
package org.kawanfw.file.api.client;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.ConnectException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.logging.Level;
import org.apache.commons.io.FileUtils;
import org.kawanfw.commons.api.client.SessionParameters;
import org.kawanfw.commons.api.client.InvalidLoginException;
import org.kawanfw.commons.api.client.RemoteException;
import org.kawanfw.commons.client.http.HttpTransferUtil;
import org.kawanfw.commons.util.ClientLogger;
import org.kawanfw.commons.util.DefaultParms;
import org.kawanfw.commons.util.FrameworkDebug;
import org.kawanfw.commons.util.Tag;
import org.kawanfw.commons.util.TransferStatus;
import org.kawanfw.file.api.util.client.ApiInputStreamDownloader;
import org.kawanfw.file.api.util.client.ChunkUtil;
import org.kawanfw.file.api.util.client.ExceptionThrower;
import org.kawanfw.file.api.util.client.FileChunkStore;
import org.kawanfw.file.api.util.client.UniqueFileCreator;
import org.kawanfw.file.util.parms.ReturnCode;
/**
*
* A RemoteInputStream
obtains input bytes
* from a remote file.
*
* The remote file bytes are read with standards {@code InputStream} read
* methods and can thus be downloaded into a local file.
*
* Large streams are split in chunks that are downloaded in sequence. The
* default chunk length is 10Mb. You can change the default value with
* {@link SessionParameters#setDownloadChunkLength(long)} before passing
* {@code SessionParameters} to {@code RemoteSession} constructor.
* Note that streams chunking requires all chunks to be downloaded from to the
* same web server. Thus, file chunking does not support true stateless
* architecture with multiple identical web servers. If you want to set a full
* stateless architecture with multiple identical web servers, you must disable
* file chunking. This is done by setting a 0 download chunk length value using
* {@link SessionParameters#setDownloadChunkLength(long)}.
*
* A recovery mechanism allows - in case of failure - to start again in the same
* JVM run the download from the last non-downloaded chunk.
* See User Guide for more information.
*
* Note that {@code read} methods throw following subclasses of
* {@code IOException}:
*
* -
* {@code InvalidLoginException} the session has been closed by a
* {@code RemoteSession.logoff()}.
* - {@code UnknownHostException} if host URL (http://www.acme.org) does not
* exists or no Internet connection.
* - {@code ConnectException} if the Host is correct but the ServerFileManager
* Servlet is not reachable (http://www.acme.org/ServerFileManager) and access
* failed with a status != OK (200). (If the host is incorrect, or is impossible
* to connect to - Tomcat down - the {@code ConnectException} will be the sub
* exception {@code HttpHostConnectException}.)
* - {@code SocketException} if network failure during transmission.
* - {@link RemoteException} an exception has been thrown on the server side.
*
*
* Example:
*
* // Define URL of the path to the {@code ServerFileManager} servlet
* String url = "https://www.acme.org/ServerFileManager";
*
* // The login info for strong authentication on server side:
* String username = "myUsername";
* char[] password = { 'm', 'y', 'P', 'a', 's', 's', 'w', 'o', 'r', 'd' };
*
* // Establish a session with the remote server
* RemoteSession remoteSession = new RemoteSession(url, username, password);
*
* File file = new File("C:\\Users\\Mike\\Koala.jpg");
* String pathname = "/Koala.jpg";
*
* InputStream in = null;
* OutputStream out = null;
*
* try {
*
* // Get an InputStream from the file located on our server
* in = new RemoteInputStream(remoteSession, pathname);
*
* // Get an OutputSream from our local file
* out = new FileOutputStream(file);
*
* // Download the remote file reading the InpuStream and save it to our
* // local file
* byte[] buffer = new byte[1024 * 4];
* int n = 0;
* while ((n = in.read(buffer)) != -1) {
* out.write(buffer, 0, n);
* }
*
* } finally {
* if (in != null)
* in.close();
* if (out != null)
* out.close();
* }
*
*
* @see org.kawanfw.file.api.client.RemoteFile
* @see org.kawanfw.file.api.client.RemoteOutputStream
*
* @author Nicolas de Pomereu
* @since 2.0
*/
public class RemoteInputStream extends InputStream {
/** For debug info */
private static boolean DEBUG = FrameworkDebug
.isSet(RemoteInputStream.class);
/** The file session in use */
private RemoteSession remoteSession = null;
/** The remote file's pathname */
private String pathname = null;
/** The stream containing the datas downloaded */
private InputStream in = null;
/** The remote file length */
private long remoteFileLength = -1;
/** The unique file used as container and reference for download */
private File fileUnique = null;
/** The counter to use use for file chunks */
private int cpt = 0;
/** Total length for a download */
private long totalLength = 0;
/** The beginning String, used to analyse the response of the server. */
private String beginString = "";
/** If true, continue input stream beginning content analysis */
private boolean continueInputStreamStartAnalysis = true;
/**
* Creates a RemoteInputStream
by
* opening a connection to an actual remote file,
* the file named by the path name pathname
* in the remote file system.
*
* The real path of the remote file depends on the Awake FILE
* configuration on the server. See User Documentation.
*
* @param remoteSession
* the current remote session
* @param pathname
* the pathname on host with "/" as file separator. Must be absolute.
*
* @throws IllegalArgumentException
* if remoteSession or pathname is null
* @throws InvalidLoginException
* the session has been closed by a {@code logoff()}
* @throws IOException
* if an I/O error occurs.
*
* @since 3.0
*/
public RemoteInputStream(RemoteSession remoteSession, String pathname)
throws IOException {
initConstructor(remoteSession, pathname);
}
/**
* Creates a RemoteInputStream
by
* opening a connection to an actual remote file,
* the file named by the path name pathname
* in the remote file system.
*
* The real path of the remote file depends on the Awake FILE
* configuration on the server. See User Documentation.
*
* @param fileSession
* the current file session
* @param pathname
* the pathname on host with "/" as file separator. Must be absolute.
*
* @throws IllegalArgumentException
* if fileSession or pathname is null
* @throws InvalidLoginException
* the session has been closed by a {@code logoff()}
* @throws IOException
* if an I/O error occurs.
*
* @deprecated As of version 3.0, replaced by: {@link RemoteInputStream#RemoteInputStream(RemoteSession, String)}
*
* @since 2.0
*/
public RemoteInputStream(FileSession fileSession, String pathname)
throws IOException {
if (fileSession == null) {
throw new IllegalArgumentException("fileSession is null!");
}
initConstructor(fileSession.getRemoteSession(), pathname);
}
/**
* Creates a RemoteInputStream
by
* opening a connection to an actual remote file,
* the file named by the RemoteFile
* object file
in the remote file system.
*
* @param remoteFile
* the remote file
*
* @throws IllegalArgumentException
* if remoteSession or remoteFile is null
* @throws InvalidLoginException
* the session has been closed by a {@code logoff()}
* @throws IOException
* if an I/O error occurs.
*
* @since 3.0
*/
public RemoteInputStream(RemoteFile remoteFile)
throws IOException {
if (remoteFile == null) {
throw new IllegalArgumentException("remoteFile is null!");
}
initConstructor(remoteFile.getRemoteSession(), remoteFile.getPath());
}
/**
* Init done in constructors.
*
* @param remoteSession
* @param pathname
* @throws InvalidLoginException
* @throws IOException
*/
private void initConstructor(RemoteSession remoteSession,
String pathname) throws InvalidLoginException, IOException {
if (remoteSession == null) {
throw new IllegalArgumentException("remoteSession is null!");
}
if (remoteSession.getUsername() == null
|| remoteSession.getAuthenticationToken() == null) {
throw new InvalidLoginException(RemoteSession.REMOTE_SESSION_IS_CLOSED);
}
if (pathname == null) {
throw new IllegalArgumentException("pathname is null!");
}
if (! pathname.startsWith("/")) {
throw new IllegalArgumentException("pathname must be asbsolute and start with \"/\": " + pathname);
}
this.remoteSession = remoteSession;
this.pathname = pathname;
// Create the unique filename corresponding to username & pathname
// name Must be done in constructor because close() uses fileUnique
fileUnique = UniqueFileCreator.createUnique(
this.remoteSession.getUsername(), pathname);
}
/**
* init is done once inside read()
*
* @throws UnknownHostException
* @throws ConnectException
* @throws RemoteException
* @throws IOException
* @throws InvalidLoginException
* @throws FileNotFoundException
*/
private void init() throws UnknownHostException, ConnectException,
RemoteException, IOException, InvalidLoginException,
FileNotFoundException {
if (in != null) {
throw new IllegalStateException(
"init can be called only if in InputStream is null!");
}
// Get the remote file length
this.remoteFileLength = this.length();
// Read up to chunk length and put it in a buffer
debug("chunkLength : "
+ ChunkUtil.getDownloadChunkLength(this.remoteSession));
debug("remoteFileLength: " + remoteFileLength);
cpt++;
in = downloadAndCreateInputStream(this.remoteSession, pathname,
remoteFileLength, fileUnique, cpt);
}
/**
* Returns the length of this remote input stream (which is the underlying remote
* file length)
*
* @return the remote input stream length in bytes
*
* @throws FileNotFoundException
* if the remote file is not found on server
* @throws InvalidLoginException
* the session has been closed by a {@code logoff()}
* @throws UnknownHostException
* if host URL (http://www.acme.org) does not exists or no
* Internet Connection.
* @throws ConnectException
* if the Host is correct but the {@code ServerFileManager}
* Servlet is not reachable
* (http://www.acme.org/ServerFileManager) and access failed
* with a status != OK (200). (If the host is incorrect, or is
* impossible to connect to - Tomcat down - the
* {@code ConnectException} will be the sub exception
* {@code HttpHostConnectException}.)
* @throws SocketException
* if network failure during transmission
* @throws RemoteException
* an exception has been thrown on the server side
* @throws SecurityException
* the url is not secured with https (SSL)
* @throws IOException
* for all other IO / Network / System Error
*/
public long length() throws ConnectException, IllegalArgumentException,
InvalidLoginException, UnknownHostException, SocketException,
RemoteException, IOException {
// Do not do it twice
if (this.remoteFileLength != -1) {
return this.remoteFileLength;
}
RemoteFile remoteFile = new RemoteFile(remoteSession, pathname);
boolean remoteFileExists = remoteFile.exists();
if (!remoteFileExists) {
throw new FileNotFoundException("Remote file does not exists: "
+ pathname);
}
// Read up to chunk length and put it in a buffer
// Get the remote file length
this.remoteFileLength = remoteFile.length();
return remoteFileLength;
}
/**
* Reads up to len
bytes of data from the input stream into an
* array of bytes. An attempt is made to read as many as len
* bytes, but a smaller number may be read. The number of bytes actually
* read is returned as an integer.
*
*
* This method blocks until input data is available, end of file is
* detected, or an exception is thrown.
*
*
* If len
is zero, then no bytes are read and 0
is
* returned; otherwise, there is an attempt to read at least one byte. If no
* byte is available because the stream is at end of file, the value
* -1
is returned; otherwise, at least one byte is read and
* stored into b
.
*
*
* The first byte read is stored into element b[off]
, the next
* one into b[off+1]
, and so on. The number of bytes read is,
* at most, equal to len
. Let k be the number of bytes
* actually read; these bytes will be stored in elements b[off]
* through b[off+
k-1]
, leaving elements
* b[off+
k]
through
* b[off+len-1]
unaffected.
*
*
* In every case, elements b[0]
through b[off]
and
* elements b[off+len]
through b[b.length-1]
are
* unaffected.
*
*
* The read(b,
off,
len)
method for
* class InputStream
simply calls the method
* read()
repeatedly. If the first such call results in an
* IOException
, that exception is returned from the call to the
* read(b,
off,
len)
method. If any
* subsequent call to read()
results in a
* IOException
, the exception is caught and treated as if it
* were end of file; the bytes read up to that point are stored into
* b
and the number of bytes read before the exception occurred
* is returned. The implementation of this method blocks until the requested
* amount of input data len
has been read, end of file is
* detected, or an exception is thrown.
*
* @param b
* the buffer into which the data is read.
* @param off
* the start offset in array b
at which the data is
* written.
* @param len
* the maximum number of bytes to read.
* @return the total number of bytes read into the buffer, or
* -1
if there is no more data because the end of the
* stream has been reached.
* @exception IOException
* If the first byte cannot be read for any reason other than
* end of file, or if the input stream has been closed, or if
* some other I/O error occurs.
* @exception NullPointerException
* If b
is null
.
* @exception IndexOutOfBoundsException
* If off
is negative, len
is
* negative, or len
is greater than
* b.length - off
* @see java.io.InputStream#read()
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
// First read: we create the input stream
if (in == null) {
init();
}
int intRead = in.read(b, off, len);
if (intRead != -1) {
if (continueInputStreamStartAnalysis) {
analyseInputStreamStart(b);
}
totalLength += intRead;
return intRead;
}
// So intRead = -1 ==> No more to read on *this* InputStream.
// Check if must read from a new on or stop it all.
if (totalLength >= remoteFileLength) {
// We are done, all is read from remote server.
in.close();
return -1;
} else {
// Read next file chunk
in.close();
cpt++;
// debug("cpt " + cpt + " " + new Date());
in = downloadAndCreateInputStream(remoteSession, pathname,
remoteFileLength, fileUnique, cpt);
intRead = in.read(b, off, len);
totalLength += intRead;
if (continueInputStreamStartAnalysis) {
analyseInputStreamStart(b);
}
}
return intRead;
}
//
// This is the only private method using global variables
//
/**
* Analyses the InputStream beginning content to check for Exceptions.
*
* @param b
* the buffer into which the data is read.
* @throws IOException
* If the first byte cannot be read for any reason other than
* end of file, or if the input stream has been closed, or if
* some other I/O error occurs.
*/
private void analyseInputStreamStart(byte[] b) throws IOException {
byte[] bytes = new byte[b.length];
for (int i = 0; i < b.length; i++) {
bytes[i] = b[i];
}
String string = new String(bytes);
beginString += string;
// debug("beginString: " + beginString);
if (totalLength > TransferStatus.SEND_FAILED.length()) {
// SEND_OK may happen if: 1) Invalid Login 2) FileNotFound
if (beginString.startsWith(TransferStatus.SEND_OK)) {
String content = getContentAsString(in, beginString);
StringReader stringReader = new StringReader(content);
BufferedReader bufferedReader = new BufferedReader(stringReader);
bufferedReader.readLine(); // Read The status line
String receive = bufferedReader.readLine();
if (receive.length() > 1) {
if (receive
.startsWith(ReturnCode.INVALID_LOGIN_OR_PASSWORD)) {
throw new InvalidLoginException(Tag.PRODUCT
+ " File Session is closed.");
}
if (receive.startsWith(Tag.FileNotFoundException)) {
throw new FileNotFoundException(
"Remote file does not exists: " + pathname);
}
// Should never happen
throw new IOException(Tag.PRODUCT_PRODUCT_FAIL
+ " Invalid received buffer: " + receive);
}
} else if (beginString.startsWith(TransferStatus.SEND_FAILED)) {
String content = getContentAsString(in, beginString);
StringReader stringReader = new StringReader(content);
HttpTransferUtil.throwTheRemoteException(new BufferedReader(
stringReader));
} else {
continueInputStreamStartAnalysis = false;
}
}
}
//
// All following private methods do *not* use global variables.
// Keept it clean this way.
//
/**
* Returns the input stream created from the download.
*
* @param remoteSession
* the file session ins use
* @param pathname
* the remote file path
* @param remoteFileLength
* ghe remote file length
* @param fileUnique
* the unique file identifier
* @param cpt
* the counter for file chunks
*
* @return the input stream created from the download
*
* @throws UnknownHostException
* @throws ConnectException
* @throws RemoteException
* @throws IOException
* @throws InvalidLoginException
* @throws FileNotFoundException
*/
private InputStream downloadAndCreateInputStream(RemoteSession remoteSession,
String remoteFile, long remoteFileLength, File fileUnique, int cpt)
throws UnknownHostException, ConnectException, RemoteException,
IOException, InvalidLoginException, FileNotFoundException {
long chunkLength = ChunkUtil.getDownloadChunkLength(remoteSession);
InputStream in = null;
File fileChunk = null;
ApiInputStreamDownloader apiInputStreamDownloader = new ApiInputStreamDownloader(
remoteSession.getUsername(),
remoteSession.getAuthenticationToken(),
remoteSession.getHttpTransfer());
if (remoteFileLength <= chunkLength) {
in = apiInputStreamDownloader.downloadOneChunk(fileChunk,
remoteFile, chunkLength);
} else {
FileChunkStore fileChunkStore = new FileChunkStore(
remoteSession.getUsername(), fileUnique, remoteFile);
String remoteFileChunk = remoteFile + "." + cpt + ".kawanfw.chunk";
String fileChunkStr = fileUnique.toString() + "." + cpt
+ ".kawanfw.chunk";
ExceptionThrower.throwSocketExceptionIfFlagFileExists();
fileChunk = new File(fileChunkStr);
// No re-download if file chunk exists and is complete
if (fileChunkStore.alreadyDownloaded(fileChunk)) {
debug("fileChunk exists, no download: " + fileChunk);
in = new BufferedInputStream(new FileInputStream(fileChunk));
} else {
debug("downloadOneChunk " + remoteFileChunk + " " + fileChunk);
in = apiInputStreamDownloader.downloadOneChunk(fileChunk,
remoteFileChunk, chunkLength);
fileChunkStore.add(fileChunk);
}
// debug("fileChunk: " + fileChunk.toString());
return in;
}
return in;
}
/**
* Returns full content as String and thus reads Input Stream until end.
*
* @param in
* the Input Stream to read
* @param beginString
* the beginning of input stream as string
*
* @return the full content as String
* @throws IOException
*/
private String getContentAsString(InputStream in, String beginString)
throws IOException {
int len;
int bufferSize = DefaultParms.DEFAULT_WRITE_BUFFER_SIZE;
// Get all content into a string and throw an analyzed exception
// Exception
byte[] buf = new byte[bufferSize];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = this.in.read(buf)) > 0) {
baos.write(buf, 0, len);
}
String content = beginString + new String(baos.toByteArray());
baos.close();
return content;
}
/**
* Returns the remote file's pathname
*
* @return the remote file's pathname
*/
public String getPathname() {
return pathname;
}
/**
* Reads some number of bytes from the input stream and stores them into the
* buffer array b
. The number of bytes actually read is
* returned as an integer. This method blocks until input data is available,
* end of file is detected, or an exception is thrown.
*
*
* If the length of b
is zero, then no bytes are read and
* 0
is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at the end
* of the file, the value -1
is returned; otherwise, at least
* one byte is read and stored into b
.
*
*
* The first byte read is stored into element b[0]
, the next
* one into b[1]
, and so on. The number of bytes read is, at
* most, equal to the length of b
. Let k be the number
* of bytes actually read; these bytes will be stored in elements
* b[0]
through b[
k-1]
,
* leaving elements b[
k]
through
* b[b.length-1]
unaffected.
*
*
* The read(b)
method for class InputStream
has
* the same effect as:
*
*
* read(b, 0, b.length)
*
*
* @param b
* the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or
* -1
is there is no more data because the end of the
* stream has been reached.
* @exception IOException
* If the first byte cannot be read for any reason other than
* the end of the file, if the input stream has been closed,
* or if some other I/O error occurs.
* @exception NullPointerException
* if b
is null
.
* @see java.io.InputStream#read(byte[], int, int)
*/
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
/**
* The method is not implemented in this Awake File version and will throw
* an {@code IOException}.
*
* @return the next byte of data, or -1
if the end of the
* stream is reached.
* @exception IOException
* if an I/O error occurs.
*/
@Override
public int read() throws IOException {
throw new IOException(Tag.PRODUCT + " read method is not supported.");
}
/**
* The method is not implemented in this Awake File version and will throw
* an {@code IOException}.
*
* @param n
* the number of bytes to be skipped.
* @return the actual number of bytes skipped.
* @exception IOException
* if the stream does not support seek, or if some other I/O
* error occurs.
*/
@Override
public long skip(long n) throws IOException {
throw new IOException(Tag.PRODUCT + " skip method is not supported.");
}
/**
* Returns an estimate of the number of bytes that can be read (or skipped
* over) from this input stream without blocking by the next invocation of a
* method for this input stream. The next invocation might be the same
* thread or another thread. A single read or skip of this many bytes will
* not block, but may read or skip fewer bytes.
*
* @return an estimate of the number of bytes that can be read (or skipped
* over) from this input stream without blocking or {@code 0} when
* it reaches the end of the input stream.
* @exception IOException
* if an I/O error occurs.
*/
@Override
public int available() throws IOException {
return in.available();
}
/**
* Closes this input stream and releases any system resources associated
* with the stream.
*
* @exception IOException
* if an I/O error occurs.
*/
@Override
public void close() throws IOException {
// if (in == null) : it has been closed already so escape now
if (in == null) {
return;
}
in.close();
// We immediately set out to null to avoid recall of this method
in = null;
debug("totalLength : " + totalLength);
debug("remoteFileLength: " + remoteFileLength);
// Delete temp storage only if all is done
if (totalLength >= remoteFileLength) {
// Delete the temporary files downloaded/created, if any
FileChunkStore fileChunkStore = new FileChunkStore(
remoteSession.getUsername(), fileUnique, pathname);
fileChunkStore.remove();
// Delete the file unique downloaded/created, if any
FileUtils.deleteQuietly(fileUnique);
}
}
/**
* Calls {@code InputStream} implementation, so does nothing.
*
* @param readlimit
* the maximum limit of bytes that can be read before the mark
* position becomes invalid.
* @see java.io.InputStream#reset()
*/
@Override
public synchronized void mark(int readlimit) {
super.mark(readlimit);
}
/**
* The method is not implemented in this Awake File version and will throw
* an {@code IOException}.
*/
@Override
public synchronized void reset() throws IOException {
throw new IOException(Tag.PRODUCT + " reset method is not supported.");
}
/**
* Returns false
.
*
* @return true
if this stream instance supports the mark and
* reset methods; false
otherwise.
* @see java.io.InputStream#mark(int)
* @see java.io.InputStream#reset()
*/
@Override
public boolean markSupported() {
return false;
}
/**
* debug tool
*/
private void debug(String s) {
if (DEBUG) {
ClientLogger.getLogger().log(Level.WARNING, s);
}
}
}