blob: 10b0077cdec01facd015d14743f5ba3124ce1975 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.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 org.eclipse.statet.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();
}
}