/*=============================================================================#
# Copyright (c) 2008-2016 Stephan Wahlbrink (WalWare.de) and others.
# All rights reserved. This program and the accompanying materials
# are made available under the terms of either (per the licensee's choosing)
# - the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html, or
# - the GNU Lesser General Public License v2.1 or newer
# which accompanies this distribution, and is available at
# http://www.gnu.org/licenses/lgpl.html
#
# Contributors:
# Stephan Wahlbrink - initial API and implementation
#=============================================================================*/
package de.walware.rj.server;
import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.OutputStream;
import java.rmi.Remote;
import de.walware.rj.RjException;
/**
* Communication exchange object for a binary array/file
*/
public class BinExchange implements RjsComObject, Externalizable {
private final static int C2S = 0x00010000;
private final static int S2C = 0x00020000;
private final static int OM_2 = 0x000f0000;
private final static int OC_2 = ~OM_2;
private final static int UPLOAD = 0x00100000;
private final static int DOWNLOAD = 0x00200000;
private final static int OM_TYPE = 0x00f00000;
private static final int OM_CUSTOM = 0x0000ffff;
private final static int DEFAULT_BUFFER_SIZE = 8192;
private final static AutoIdMap<OutputStream> gCOutList = new AutoIdMap<>();
static RjsComConfig.PathResolver gSPathResolver = new RjsComConfig.PathResolver() {
@Override
public File resolve(final Remote client, final String path) throws RjException {
final File file = new File(path);
if (!file.isAbsolute()) {
throw new RjException("Relative path not supported.");
}
return file;
}
};
private int options;
private Remote ref;
private String remoteFilePath;
private RjsStatus status;
private long inputLength;
private InputStream inputStream;
private int outputId;
private byte[] bytes;
/**
* Constructor for clients to upload a file
* @param in input stream to read the content from
* @param length length
* @param path remote file path
*/
public BinExchange(final InputStream in, final long length, final String path, final Remote ref, final int options) {
if (path == null || in == null || ref == null) {
throw new NullPointerException();
}
if (path.length() <= 0) {
throw new IllegalArgumentException("Invalid path: empty.");
}
if (length < 0) {
throw new IllegalArgumentException("Invalid length: negative.");
}
this.options = ((C2S | UPLOAD) | (OM_CUSTOM & options));
this.ref = ref;
this.remoteFilePath = path;
this.inputLength = length;
this.inputStream = in;
this.outputId = 0;
}
/**
* Constructor for clients to download a file
* @param out output stream to write the content to
* @param path remote file path
* @param options
*/
public BinExchange(final OutputStream out, final String path, final Remote ref, final int options) {
if (path == null || out == null || ref == null) {
throw new NullPointerException();
}
if (path.length() == 0) {
throw new IllegalArgumentException("Illegal path argument.");
}
this.options = (C2S | DOWNLOAD) | (OM_CUSTOM & options);
this.ref = ref;
this.inputLength = -1;
this.inputStream = null;
this.outputId = gCOutList.put(out);
this.remoteFilePath = path;
}
/**
* Constructor for clients to download a file into a byte array
* @param path remote file path
* @param options
*/
public BinExchange(final String path, final Remote ref, final int options) {
if (path == null || ref == null) {
throw new NullPointerException();
}
if (path.length() == 0) {
throw new IllegalArgumentException("Illegal path argument.");
}
this.options = (C2S | DOWNLOAD) | (OM_CUSTOM & options);
this.ref = ref;
this.inputLength = -1;
this.inputStream = null;
this.outputId = 0;
this.remoteFilePath = path;
}
/**
* Constructor for automatic deserialization
*/
public BinExchange() {
}
@Override
public int getComType() {
return T_FILE_EXCHANGE;
}
public boolean isOK() {
return (this.status == null || this.status.getSeverity() == RjsStatus.OK);
}
public RjsStatus getStatus() {
return this.status;
}
public String getFilePath() {
return this.remoteFilePath;
}
public byte[] getBytes() {
return this.bytes;
}
public void clear() {
gCOutList.remove(this.outputId);
}
@Override
public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
final int readOptions = in.readInt();
this.remoteFilePath = in.readUTF();
switch (readOptions & (OM_TYPE | OM_2)) {
case C2S | UPLOAD: {
this.ref = (Remote) in.readObject();
long length = this.inputLength = in.readLong();
FileOutputStream output = null;
try {
final File file = (gSPathResolver != null) ? gSPathResolver.resolve(this.ref, this.remoteFilePath) : new File(this.remoteFilePath);
output = new FileOutputStream(file, false);
final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
while (length > 0L) {
final int n = in.read(buffer, 0, (int) Math.min(DEFAULT_BUFFER_SIZE, length));
if (n == -1) {
throw new IOException("Unexcepted end of stream.");
}
output.write(buffer, 0, n);
length -= n;
}
this.status = RjsStatus.OK_STATUS;
}
catch (final RjException e) {
this.status = new RjsStatus(RjsStatus.ERROR, 0, e.getMessage());
throw new IOException("Failed to resolve file path.");
}
catch (final IOException e) {
this.status = new RjsStatus(RjsStatus.ERROR, 0, e.getMessage());
throw new IOException("Failed to write stream to file.");
}
finally {
if (output != null) {
try {
output.close();
}
catch (final IOException e) {}
}
}
this.options = (readOptions & OC_2) | S2C;
return; }
case C2S | DOWNLOAD: {
this.ref = (Remote) in.readObject();
this.outputId = in.readInt();
this.options = (readOptions & OC_2) | S2C;
return; }
case S2C | UPLOAD: {
this.status = new RjsStatus(in);
this.options = (readOptions & OC_2);
return; }
case S2C | DOWNLOAD: {
this.outputId = in.readInt();
this.status = new RjsStatus(in);
if (this.status.getSeverity() == RjsStatus.OK) {
long length = this.inputLength = in.readLong();
boolean writing = false;
try {
if (this.outputId > 0) {
final OutputStream out = gCOutList.get(this.outputId);
final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
while (length > 0) {
final int n = in.read(buffer, 0, (int) Math.min(DEFAULT_BUFFER_SIZE, length));
if (n == -1) {
throw new IOException("Unexcepted end of stream.");
}
length -= n;
writing = true;
out.write(buffer, 0, n);
writing = false;
}
}
else {
if (length > Integer.MAX_VALUE) {
throw new UnsupportedOperationException();
}
this.bytes = new byte[(int) length];
while (length > 0) {
final int n = in.read(this.bytes, (int) (this.inputLength-length), (int) length);
if (n == -1) {
throw new IOException("Unexcepted end of stream.");
}
length -= n;
}
}
}
catch (IOException e) {
if (writing) {
this.status = new RjsStatus(RjsStatus.ERROR, 0, "Writing download to stream failed: " + e.getMessage());
try {
while (length > 0) {
final long n = in.skip(length);
if (n == -1) {
throw new IOException("Unexcepted end of stream.");
}
length -= n;
}
return;
}
catch (final IOException e2) {
e = e2;
}
}
throw e;
}
}
this.options = (readOptions & OC_2);
return; }
default:
throw new IllegalStateException();
}
}
@Override
public void writeExternal(final ObjectOutput out) throws IOException {
out.writeInt(this.options);
out.writeUTF(this.remoteFilePath);
switch (this.options & (OM_TYPE | OM_2)) {
case C2S | UPLOAD: {
out.writeObject(this.ref);
out.writeLong(this.inputLength);
if (this.inputStream != null) {
final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
long length = this.inputLength;
while (length > 0) {
final int n = this.inputStream.read(buffer, 0, (int) Math.min(DEFAULT_BUFFER_SIZE, length));
if (n == -1) {
throw new IOException("Unexcepted end of stream.");
}
out.write(buffer, 0, n);
length -= n;
}
}
else if (this.bytes != null) {
out.writeLong(this.inputLength);
out.write(this.bytes, 0, (int) this.inputLength);
}
else {
throw new IOException("Missing file content.");
}
return; }
case C2S | DOWNLOAD: {
out.writeObject(this.ref);
out.writeInt(this.outputId);
return; }
case S2C | UPLOAD: {
this.status.writeExternal(out);
return; }
case S2C | DOWNLOAD: {
out.writeInt(this.outputId);
FileInputStream input = null;
try {
final File file = (gSPathResolver != null) ? gSPathResolver.resolve(this.ref, this.remoteFilePath) : new File(this.remoteFilePath);
try {
input = new FileInputStream(file);
input.available();
}
catch (final IOException e) {
if (input != null) {
try {
input.close();
input = null;
}
catch (final IOException ignore) {}
}
if (!file.exists() || e instanceof FileNotFoundException) {
new RjsStatus(RjsStatus.ERROR, 0, "Failed to find file '"+ this.remoteFilePath + "'.").writeExternal(out);
}
else {
new RjsStatus(RjsStatus.ERROR, 0, "Failed to open file '"+ this.remoteFilePath + "'.").writeExternal(out);
}
}
if (input != null) {
RjsStatus.OK_STATUS.writeExternal(out);
long length = this.inputLength = file.length();
out.writeLong(length);
final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
while (length > 0) {
final int n = input.read(buffer, 0, (int) Math.min(DEFAULT_BUFFER_SIZE, length));
if (n == -1) {
throw new IOException("Unexcepted end of file content.");
}
length -= n;
out.write(buffer, 0, n);
}
}
}
catch (final RjException e) {
this.status = new RjsStatus(RjsStatus.ERROR, 0, e.getMessage());
throw new IOException("Failed to resolve file path.");
}
finally {
if (input != null) {
try {
input.close();
}
catch (final IOException ignore) {}
}
}
return; }
default:
throw new IllegalStateException();
}
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(128);
sb.append("DataCmdItem ");
switch (this.options & OM_TYPE) {
case UPLOAD:
sb.append("UPLOAD");
break;
case DOWNLOAD:
sb.append("DOWNLOAD");
break;
default:
sb.append((this.options & OM_TYPE));
break;
}
sb.append("\n\t").append("direction= ");
switch (this.options & OM_2) {
case C2S:
sb.append("CLIENT-2-SERVER");
break;
case S2C:
sb.append("SERVER-2-CLIENT");
break;
default:
sb.append((this.options & OM_2));
}
sb.append("\n\tlength= ");
sb.append(this.inputLength);
if (this.status != null) {
sb.append("\n<STATUS>\n");
this.status.getCode();
sb.append("\n</STATUS>");
}
return sb.toString();
}
}