/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package edu.harvard.iq.dataverse.api;
import java.lang.reflect.Type;
import java.lang.annotation.Annotation;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import edu.harvard.iq.dataverse.DataFile;
import edu.harvard.iq.dataverse.DataverseRequestServiceBean;
import edu.harvard.iq.dataverse.dataaccess.*;
import edu.harvard.iq.dataverse.datavariable.DataVariable;
import edu.harvard.iq.dataverse.engine.command.Command;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
import edu.harvard.iq.dataverse.engine.command.impl.CreateGuestbookResponseCommand;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
*
* @author Leonid Andreev
*/
@Provider
public class DownloadInstanceWriter implements MessageBodyWriter<DownloadInstance> {
private static final Logger logger = Logger.getLogger(DownloadInstanceWriter.class.getCanonicalName());
@Override
public boolean isWriteable(Class<?> clazz, Type type, Annotation[] annotation, MediaType mediaType) {
return clazz == DownloadInstance.class;
}
@Override
public long getSize(DownloadInstance di, Class<?> clazz, Type type, Annotation[] annotation, MediaType mediaType) {
return -1;
//return getFileSize(di);
}
@Override
public void writeTo(DownloadInstance di, Class<?> clazz, Type type, Annotation[] annotation, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream outstream) throws IOException, WebApplicationException {
if (di.getDownloadInfo() != null && di.getDownloadInfo().getDataFile() != null) {
DataAccessRequest daReq = new DataAccessRequest();
DataFile sf = di.getDownloadInfo().getDataFile();
DataFileIO accessObject = DataAccess.createDataAccessObject(sf, daReq);
if (accessObject != null) {
accessObject.open();
if (di.getConversionParam() != null) {
// Image Thumbnail and Tabular data conversion:
// NOTE: only supported on local files, as of 4.0.2!
if (di.getConversionParam().equals("imageThumb") && accessObject.isLocalFile()) {
if ("".equals(di.getConversionParamValue())) {
accessObject = ImageThumbConverter.getImageThumb((FileAccessIO)accessObject);
} else {
try {
int size = new Integer(di.getConversionParamValue()).intValue();
if (size > 0) {
accessObject = ImageThumbConverter.getImageThumb((FileAccessIO)accessObject, size);
}
} catch (java.lang.NumberFormatException ex) {
accessObject = ImageThumbConverter.getImageThumb((FileAccessIO)accessObject);
}
}
}
if (sf.isTabularData()) {
if (di.getConversionParam().equals("noVarHeader")) {
accessObject.setNoVarHeader(Boolean.TRUE);
accessObject.setVarHeader(null);
} else if (di.getConversionParam().equals("format") && accessObject.isLocalFile()) {
if ("original".equals(di.getConversionParamValue())) {
accessObject = StoredOriginalFile.retreive(accessObject);
} else {
// Other format conversions:
String requestedMimeType = di.getServiceFormatType(di.getConversionParam(), di.getConversionParamValue());
if (requestedMimeType == null) {
// default mime type, in case real type is unknown;
// (this shouldn't happen in real life - but just in case):
requestedMimeType = "application/octet-stream";
}
accessObject =
DataFileConverter.performFormatConversion(
sf,
(FileAccessIO)accessObject,
di.getConversionParamValue(), requestedMimeType);
}
} else if (di.getConversionParam().equals("subset")) {
logger.fine("processing subset request.");
// TODO:
// If there are parameters on the list that are
// not valid variable ids, or if the do not belong to
// the datafile referenced - I simply skip them;
// perhaps I should throw an invalid argument exception
// instead.
// -- L.A. 4.0 beta 9
if (di.getExtraArguments() != null && di.getExtraArguments().size() > 0) {
logger.fine("processing extra arguments list of length "+di.getExtraArguments().size());
List <DataVariable> variableList = new ArrayList<>();
String subsetVariableHeader = null;
for (int i = 0; i < di.getExtraArguments().size(); i++) {
DataVariable variable = (DataVariable)di.getExtraArguments().get(i);
if (variable != null) {
if (variable.getDataTable().getDataFile().getId().equals(sf.getId())) {
logger.fine("adding variable id "+variable.getId()+" to the list.");
variableList.add(variable);
if (subsetVariableHeader == null) {
subsetVariableHeader = variable.getName();
} else {
subsetVariableHeader = subsetVariableHeader.concat("\t");
subsetVariableHeader = subsetVariableHeader.concat(variable.getName());
}
} else {
logger.warning("variable does not belong to this data file.");
}
}
}
if (variableList.size() > 0) {
TabularSubsetInputStream subsetInputStream = null;
try {
subsetInputStream = new TabularSubsetInputStream(sf, variableList);
} catch (IOException ioe) {
subsetInputStream = null;
}
if (subsetInputStream != null) {
logger.fine("successfully created subset output stream.");
accessObject.closeInputStream();
accessObject.setInputStream(subsetInputStream);
accessObject.setIsLocalFile(true);
// TODO: make noVarHeader option work for subsets too!
subsetVariableHeader = subsetVariableHeader.concat("\n");
accessObject.setVarHeader(subsetVariableHeader);
String tabularFileName = accessObject.getFileName();
if (tabularFileName != null && tabularFileName.endsWith(".tab")) {
tabularFileName = tabularFileName.replaceAll("\\.tab$", "-subset.tab");
} else if (tabularFileName != null && !"".equals(tabularFileName)) {
tabularFileName = tabularFileName.concat("-subset.tab");
} else {
tabularFileName = "subset.tab";
}
accessObject.setFileName(tabularFileName);
}
}
} else {
logger.fine("empty list of extra arguments.");
}
}
}
if (accessObject == null) {
throw new WebApplicationException(Response.Status.SERVICE_UNAVAILABLE);
}
}
InputStream instream = accessObject.getInputStream();
if (instream != null) {
// headers:
String fileName = accessObject.getFileName();
String mimeType = accessObject.getMimeType();
// Provide both the "Content-disposition" and "Content-Type" headers,
// to satisfy the widest selection of browsers out there.
httpHeaders.add("Content-disposition", "attachment; filename=\"" + fileName + "\"");
httpHeaders.add("Content-Type", mimeType + "; name=\"" + fileName + "\"");
long contentSize;
boolean useChunkedTransfer = false;
//if ((contentSize = getFileSize(di, accessObject.getVarHeader())) > 0) {
if ((contentSize = getContentSize(accessObject)) > 0) {
logger.fine("Content size (retrieved from the AccessObject): "+contentSize);
httpHeaders.add("Content-Length", contentSize);
} else {
//httpHeaders.add("Transfer-encoding", "chunked");
//useChunkedTransfer = true;
}
// (the httpHeaders map must be modified *before* writing any
// data in the output stream!)
int bufsize;
byte [] bffr = new byte[4*8192];
byte [] chunkClose = "\r\n".getBytes();
// before writing out any bytes from the input stream, flush
// any extra content, such as the variable header for the
// subsettable files: (??)4
if (accessObject.getVarHeader() != null) {
if (accessObject.getVarHeader().getBytes().length > 0) {
if (useChunkedTransfer) {
String chunkSizeLine = String.format("%x\r\n", accessObject.getVarHeader().getBytes().length);
outstream.write(chunkSizeLine.getBytes());
}
outstream.write(accessObject.getVarHeader().getBytes());
if (useChunkedTransfer) {
outstream.write(chunkClose);
}
}
}
while ((bufsize = instream.read(bffr)) != -1) {
if (useChunkedTransfer) {
String chunkSizeLine = String.format("%x\r\n", bufsize);
outstream.write(chunkSizeLine.getBytes());
}
outstream.write(bffr, 0, bufsize);
if (useChunkedTransfer) {
outstream.write(chunkClose);
}
}
if (useChunkedTransfer) {
String chunkClosing = "0\r\n\r\n";
outstream.write(chunkClosing.getBytes());
}
logger.fine("di conversion param: "+di.getConversionParam()+", value: "+di.getConversionParamValue());
// Downloads of thumbnail images (scaled down, low-res versions of graphic image files) and
// "preprocessed metadata" records for tabular data files are NOT considered "real" downloads,
// so these should not produce guestbook entries:
if (di.getGbr() != null && !(isThumbnailDownload(di) || isPreprocessedMetadataDownload(di))) {
try {
logger.fine("writing guestbook response.");
Command cmd = new CreateGuestbookResponseCommand(di.getDataverseRequestService().getDataverseRequest(), di.getGbr(), di.getGbr().getDataFile().getOwner());
di.getCommand().submit(cmd);
} catch (CommandException e) {
//if an error occurs here then download won't happen no need for response recs...
}
} else {
logger.fine("not writing guestbook response");
}
instream.close();
outstream.close();
return;
}
}
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
private boolean isThumbnailDownload(DownloadInstance downloadInstance) {
if (downloadInstance == null) return false;
if (downloadInstance.getConversionParam() == null) return false;
return downloadInstance.getConversionParam().equals("imageThumb");
}
private boolean isPreprocessedMetadataDownload(DownloadInstance downloadInstance) {
if (downloadInstance == null) return false;
if (downloadInstance.getConversionParam() == null) return false;
if (downloadInstance.getConversionParamValue() == null) return false;
return downloadInstance.getConversionParam().equals("format") && downloadInstance.getConversionParamValue().equals("prep");
}
private long getContentSize(DataFileIO accessObject) {
long contentSize = 0;
if (accessObject.getSize() > -1) {
contentSize+=accessObject.getSize();
if (accessObject.getVarHeader() != null) {
if (accessObject.getVarHeader().getBytes().length > 0) {
contentSize+=accessObject.getVarHeader().getBytes().length;
}
}
return contentSize;
}
return -1;
}
private long getFileSize(DownloadInstance di) {
return getFileSize(di, null);
}
private long getFileSize(DownloadInstance di, String extraHeader) {
if (di.getDownloadInfo() != null && di.getDownloadInfo().getDataFile() != null) {
DataFile df = di.getDownloadInfo().getDataFile();
// For non-tabular files, we probably know the file size:
// (except for when this is a thumbNail rquest on an image file -
// because the size will obviously be different... can still be
// figured out - but perhaps we shouldn't bother; since thumbnails
// are essentially guaranteed to be small)
if (!df.isTabularData() && (di.getConversionParam() == null || "".equals(di.getConversionParam()))) {
if (df.getFilesize() > 0) {
return df.getFilesize();
}
}
// For Tabular files:
// If it's just a straight file download, it's pretty easy - we
// already know the size of the file on disk (just like in the
// fragment above); we just need to make sure if we are also supplying
// the additional variable name header - then we need to add its
// size to the total... But the cases when it's a format conversion
// and, especially, subsets are of course trickier. (these are not
// supported yet).
if (df.isTabularData() && (di.getConversionParam() == null || "".equals(di.getConversionParam()))) {
long fileSize = df.getFilesize();
if (fileSize > 0) {
if (extraHeader != null) {
fileSize += extraHeader.getBytes().length;
}
return fileSize;
}
}
}
return -1;
}
}