/*
* Copyright 2010, Red Hat, Inc. and individual contributors as indicated by the
* @author tags. See the copyright.txt file in the distribution for a full
* listing of individual contributors.
*
* 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 2.1 of the License, or (at your option)
* any later version.
*
* This software 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 software; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
* site: http://www.fsf.org.
*/
package org.zanata.rest.service;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.ws.rs.Path;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.zanata.adapter.FileFormatAdapter;
import org.zanata.adapter.po.PoWriter2;
import org.zanata.common.ContentState;
import org.zanata.common.LocaleId;
import org.zanata.dao.DocumentDAO;
import org.zanata.file.FilePersistService;
import org.zanata.file.GlobalDocumentId;
import org.zanata.file.RawDocumentContentAccessException;
import org.zanata.file.SourceDocumentUpload;
import org.zanata.file.TranslationDocumentUpload;
import org.zanata.model.HDocument;
import org.zanata.rest.DocumentFileUploadForm;
import org.zanata.rest.StringSet;
import org.zanata.rest.dto.resource.Resource;
import org.zanata.rest.dto.resource.TextFlowTarget;
import org.zanata.rest.dto.resource.TranslationsResource;
import org.zanata.service.FileSystemService;
import org.zanata.service.FileSystemService.DownloadDescriptorProperties;
import org.zanata.service.TranslationFileService;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
@Name("fileService")
@Path(FileResource.FILE_RESOURCE)
public class FileService implements FileResource {
private static final String FILE_TYPE_OFFLINE_PO = "offlinepo";
private static final String FILE_TYPE_OFFLINE_PO_TEMPLATE = "offlinepot";
@In
private DocumentDAO documentDAO;
@In(create = true)
private TranslatedDocResourceService translatedDocResourceService;
@In
private FileSystemService fileSystemServiceImpl;
@In
private TranslationFileService translationFileServiceImpl;
@In
private ResourceUtils resourceUtils;
@In
private VirusScanner virusScanner;
@In(value = "sourceDocumentUploader", create = true)
private SourceDocumentUpload sourceUploader;
@In(value = "translationDocumentUploader", create = true)
private TranslationDocumentUpload translationUploader;
@In("filePersistService")
private FilePersistService filePersistService;
@Override
public Response acceptedFileTypes() {
StringSet acceptedTypes = new StringSet("");
acceptedTypes.addAll(translationFileServiceImpl
.getSupportedExtensions());
return Response.ok(acceptedTypes.toString()).build();
}
@Override
public Response uploadSourceFile(String projectSlug, String iterationSlug,
String docId, DocumentFileUploadForm uploadForm) {
GlobalDocumentId id =
new GlobalDocumentId(projectSlug, iterationSlug, docId);
return sourceUploader.tryUploadSourceFile(id, uploadForm);
}
@Override
public Response uploadTranslationFile(String projectSlug,
String iterationSlug, String localeId, String docId, String merge,
DocumentFileUploadForm uploadForm) {
GlobalDocumentId id =
new GlobalDocumentId(projectSlug, iterationSlug, docId);
return translationUploader.tryUploadTranslationFile(id, localeId,
merge, uploadForm);
}
@Override
public Response downloadSourceFile(String projectSlug,
String iterationSlug, String fileType, String docId) {
// TODO scan (again) for virus
HDocument document =
documentDAO.getByProjectIterationAndDocId(projectSlug,
iterationSlug, docId);
if (document == null) {
return Response.status(Status.NOT_FOUND).build();
}
if (FILETYPE_RAW_SOURCE_DOCUMENT.equals(fileType)) {
if (document.getRawDocument() == null) {
return Response.status(Status.NOT_FOUND).build();
}
InputStream fileContents;
try {
fileContents =
filePersistService
.getRawDocumentContentAsStream(document
.getRawDocument());
} catch (RawDocumentContentAccessException e) {
e.printStackTrace();
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e)
.build();
}
StreamingOutput output =
new InputStreamStreamingOutput(fileContents);
return Response
.ok()
.header("Content-Disposition",
"attachment; filename=\"" + document.getName()
+ "\"").entity(output).build();
} else if ("pot".equals(fileType)
|| FILE_TYPE_OFFLINE_PO_TEMPLATE.equals(fileType)) {
// Note: could give 404 or unsupported media type for "pot" in
// non-po projects,
// and suggest using offlinepo
Resource res = resourceUtils.buildResource(document);
StreamingOutput output =
new POTStreamingOutput(res,
FILE_TYPE_OFFLINE_PO_TEMPLATE.equals(fileType));
return Response
.ok()
.header("Content-Disposition",
"attachment; filename=\"" + document.getName()
+ ".pot\"").type(MediaType.TEXT_PLAIN)
.entity(output).build();
} else {
return Response.status(Status.UNSUPPORTED_MEDIA_TYPE).build();
}
}
@Override
public Response downloadTranslationFile(String projectSlug,
String iterationSlug, String locale, String fileType, String docId) {
GlobalDocumentId id =
new GlobalDocumentId(projectSlug, iterationSlug, docId);
// TODO scan (again) for virus
final Response response;
HDocument document =
this.documentDAO.getByProjectIterationAndDocId(projectSlug,
iterationSlug, docId);
if (document == null) {
response = Response.status(Status.NOT_FOUND).build();
} else if ("po".equals(fileType)
|| FILE_TYPE_OFFLINE_PO.equals(fileType)) {
// Note: could return 404 or Unsupported media type for "po" in
// non-po projects,
// and suggest to use offlinepo
final Set<String> extensions = new HashSet<String>();
extensions.add("gettext");
extensions.add("comment");
// Perform translation of Hibernate DTOs to JAXB DTOs
TranslationsResource transRes =
(TranslationsResource) this.translatedDocResourceService
.getTranslations(docId, new LocaleId(locale),
extensions, true, null).getEntity();
Resource res = this.resourceUtils.buildResource(document);
StreamingOutput output =
new POStreamingOutput(res, transRes,
FILE_TYPE_OFFLINE_PO.equals(fileType));
response =
Response.ok()
.header("Content-Disposition",
"attachment; filename=\""
+ document.getName() + ".po\"")
.type(MediaType.TEXT_PLAIN).entity(output).build();
} else if (FILETYPE_TRANSLATED_APPROVED.equals(fileType)
|| FILETYPE_TRANSLATED_APPROVED_AND_FUZZY.equals(fileType)) {
if (!filePersistService.hasPersistedDocument(id)) {
return Response.status(Status.NOT_FOUND).build();
}
final Set<String> extensions = Collections.<String> emptySet();
TranslationsResource transRes =
(TranslationsResource) this.translatedDocResourceService
.getTranslations(docId, new LocaleId(locale),
extensions, true, null).getEntity();
// Filter to only provide translated targets. "Preview" downloads
// include fuzzy.
// New list is used as transRes list appears not to be a modifiable
// implementation.
Map<String, TextFlowTarget> translations =
new HashMap<String, TextFlowTarget>();
boolean useFuzzy =
FILETYPE_TRANSLATED_APPROVED_AND_FUZZY.equals(fileType);
for (TextFlowTarget target : transRes.getTextFlowTargets()) {
// TODO rhbz953734 - translatedDocResourceService will map
// review content state to old state. For now this is
// acceptable. Once we have new REST options, we should review
// this
if (target.getState() == ContentState.Approved
|| (useFuzzy && target.getState() == ContentState.NeedReview)) {
translations.put(target.getResId(), target);
}
}
HDocument hDocument =
documentDAO.getByProjectIterationAndDocId(projectSlug,
iterationSlug, docId);
InputStream inputStream;
try {
inputStream =
filePersistService
.getRawDocumentContentAsStream(hDocument
.getRawDocument());
} catch (RawDocumentContentAccessException e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e)
.build();
}
File tempFile =
translationFileServiceImpl.persistToTempFile(inputStream);
String name = projectSlug + ":" + iterationSlug + ":" + docId;
// TODO damason: this file is not transmitted, but used to generate
// a file later
// the generated file should be scanned instead
virusScanner.scan(tempFile, name);
URI uri = tempFile.toURI();
FileFormatAdapter adapter =
translationFileServiceImpl.getAdapterFor(hDocument
.getRawDocument().getType());
String rawParamString =
hDocument.getRawDocument().getAdapterParameters();
Optional<String> params =
Optional.<String> fromNullable(Strings
.emptyToNull(rawParamString));
StreamingOutput output =
new FormatAdapterStreamingOutput(uri, translations, locale,
adapter, params);
response =
Response.ok()
.header("Content-Disposition",
"attachment; filename=\""
+ document.getName() + "\"")
.entity(output).build();
// TODO damason: remove more immediately, but make sure response has
// finished with the file
// Note: may not be necessary when file storage is on disk.
tempFile.deleteOnExit();
} else {
response = Response.status(Status.UNSUPPORTED_MEDIA_TYPE).build();
}
return response;
}
@Override
public Response download(String downloadId) {
// TODO scan (again) for virus
try {
// Check that the download exists by looking at the download
// descriptor
Properties descriptorProps =
this.fileSystemServiceImpl
.findDownloadDescriptorProperties(downloadId);
if (descriptorProps == null) {
return Response.status(Status.NOT_FOUND).build();
} else {
File toDownload =
this.fileSystemServiceImpl.findDownloadFile(downloadId);
if (toDownload == null) {
return Response.status(Status.NOT_FOUND).build();
} else {
return Response
.ok()
.header("Content-Disposition",
"attachment; filename=\""
+ descriptorProps
.getProperty(DownloadDescriptorProperties.DownloadFileName
.toString()) + "\"")
.header("Content-Length", toDownload.length())
.entity(new FileStreamingOutput(toDownload))
.build();
}
}
} catch (IOException e) {
return Response.serverError().status(Status.INTERNAL_SERVER_ERROR)
.build();
}
}
/*
* Private class that implements PO file streaming of a document.
*/
private class POStreamingOutput implements StreamingOutput {
private Resource resource;
private TranslationsResource transRes;
private boolean offlinePo;
/**
* @param offlinePo
* true if text flow id should be inserted into msgctxt to
* allow reverse mapping.
*/
public POStreamingOutput(Resource resource,
TranslationsResource transRes, boolean offlinePo) {
this.resource = resource;
this.transRes = transRes;
this.offlinePo = offlinePo;
}
@Override
public void write(OutputStream output) throws IOException,
WebApplicationException {
PoWriter2 writer = new PoWriter2(false, offlinePo);
writer.writePo(output, "UTF-8", this.resource, this.transRes);
}
}
private class POTStreamingOutput implements StreamingOutput {
private Resource resource;
private boolean offlinePot;
/**
* @param offlinePot
* true if text flow id should be inserted into msgctxt to
* allow reverse mapping
*/
public POTStreamingOutput(Resource resource, boolean offlinePot) {
this.resource = resource;
this.offlinePot = offlinePot;
}
@Override
public void write(OutputStream output) throws IOException,
WebApplicationException {
PoWriter2 writer = new PoWriter2(false, offlinePot);
writer.writePot(output, "UTF-8", resource);
}
}
private class InputStreamStreamingOutput implements StreamingOutput {
private InputStream input;
public InputStreamStreamingOutput(InputStream input) {
this.input = input;
}
@Override
public void write(OutputStream output) throws IOException,
WebApplicationException {
byte[] buffer = new byte[4096]; // To hold file contents
int bytesRead; // How many bytes in buffer
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
}
}
private class FormatAdapterStreamingOutput implements StreamingOutput {
private Map<String, TextFlowTarget> translations;
private String locale;
private URI original;
private FileFormatAdapter adapter;
private Optional<String> params;
public FormatAdapterStreamingOutput(URI originalDoc,
Map<String, TextFlowTarget> translations, String locale,
FileFormatAdapter adapter, Optional<String> params) {
this.translations = translations;
this.locale = locale;
this.original = originalDoc;
this.adapter = adapter;
this.params = params;
}
@Override
public void write(OutputStream output) throws IOException,
WebApplicationException {
// FIXME should the generated file be virus scanned?
adapter.writeTranslatedFile(output, original, translations, locale,
params);
}
}
/*
* Private class that implements downloading from a previously prepared
* file.
*/
private class FileStreamingOutput implements StreamingOutput {
private File file;
public FileStreamingOutput(File file) {
this.file = file;
}
@Override
public void write(@Nonnull OutputStream output) throws IOException,
WebApplicationException {
FileInputStream input = new FileInputStream(this.file);
try {
ByteStreams.copy(input, output);
} finally {
input.close();
}
}
}
}