/*
* R Service Bus
*
* Copyright (c) Copyright of Open Analytics NV, 2010-2015
*
* ===========================================================================
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.openanalytics.rsb.message;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.MessageSource;
import org.springframework.util.FileCopyUtils;
import org.stringtemplate.v4.ST;
import eu.openanalytics.rsb.Constants;
import eu.openanalytics.rsb.Util;
/**
* Represents a RSB job that consists of multiple files.
*
* @author "OpenAnalytics <rsb.development@openanalytics.eu>"
*/
public class MultiFilesJob extends AbstractJob
{
private static final long serialVersionUID = 1L;
private final File temporaryDirectory;
private File rScriptFile;
public MultiFilesJob(final Source source,
final String applicationName,
final String userName,
final UUID jobId,
final GregorianCalendar submissionTime,
final Map<String, Serializable> meta) throws IOException
{
super(source, applicationName, userName, jobId, submissionTime, meta);
this.temporaryDirectory = Util.createTemporaryDirectory("job");
}
public void addFile(final String name, final InputStream is) throws IOException
{
if (Constants.MULTIPLE_FILES_JOB_CONFIGURATION.equals(name))
{
loadJobConfiguration(is);
}
else
{
addJobFile(name, is);
}
}
private void addJobFile(final String name, final InputStream is)
throws FileNotFoundException, IOException
{
final File jobFile = new File(temporaryDirectory, name);
if (StringUtils.equalsIgnoreCase(FilenameUtils.getExtension(name), Constants.R_SCRIPT_FILE_EXTENSION))
{
if (rScriptFile != null)
{
throw new IllegalArgumentException("Only one R script is allowed per job");
}
rScriptFile = jobFile;
}
final FileOutputStream fos = new FileOutputStream(jobFile);
IOUtils.copy(is, fos);
IOUtils.closeQuietly(fos);
}
private void loadJobConfiguration(final InputStream is) throws IOException
{
final Properties jobConfiguration = new Properties();
jobConfiguration.load(is);
final Map<String, Serializable> mergedMeta = new HashMap<String, Serializable>();
for (final Entry<?, ?> e : jobConfiguration.entrySet())
{
mergedMeta.put(e.getKey().toString(), e.getValue().toString());
}
// give priority to pre-existing metas by overriding the ones from the config
// file
mergedMeta.putAll(getMeta());
getMeta().clear();
getMeta().putAll(mergedMeta);
}
@Override
protected void releaseResources()
{
try
{
FileUtils.forceDelete(temporaryDirectory);
}
catch (final IOException ioe)
{
throw new RuntimeException("Can't release resources of: " + this, ioe);
}
}
public MultiFilesResult buildSuccessResult() throws IOException
{
return new MultiFilesResult(getSource(), getApplicationName(), getUserName(), getJobId(),
getSubmissionTime(), getMeta(), true);
}
@Override
public MultiFilesResult buildErrorResult(final Throwable t, final MessageSource messageSource)
throws IOException
{
final String message = messageSource.getMessage(getErrorMessageId(), null, null);
final ST template = Util.newStringTemplate(message);
template.add("job", this);
template.add("throwable", t);
final MultiFilesResult result = new MultiFilesResult(getSource(), getApplicationName(),
getUserName(), getJobId(), getSubmissionTime(), getMeta(), false);
final File resultFile = result.createNewResultFile(getJobId() + "."
+ Util.getResourceType(Constants.TEXT_MIME_TYPE));
FileCopyUtils.copy(template.render(), new FileWriter(resultFile));
return result;
}
public File getRScriptFile()
{
return rScriptFile;
}
public File[] getFiles()
{
return temporaryDirectory.listFiles();
}
/**
* Add a stream to a job, exploding it if it is a Zip input. Closes the provided
* data stream.
*
* @param contentType
* @param name
* @param data
* @param job
* @throws IOException
*/
public static void addDataToJob(final String contentType,
final String name,
final InputStream data,
final MultiFilesJob job) throws IOException
{
// some browsers send zip file as application/octet-stream, forcing a
// fallback to an
// extension check
if ((Constants.ZIP_CONTENT_TYPES.contains(contentType) || (StringUtils.endsWithIgnoreCase(
FilenameUtils.getExtension(name), Constants.ZIP_MIME_TYPE.getSubType()))))
{
addZipFilesToJob(data, job);
}
else
{
job.addFile(name, data);
}
}
/**
* Adds all the files contained in a Zip archive to a job. Rejects Zips that
* contain sub-directories.
*
* @param data
* @param job
* @throws IOException
*/
public static void addZipFilesToJob(final InputStream data, final MultiFilesJob job) throws IOException
{
final ZipInputStream zis = new ZipInputStream(data);
ZipEntry ze = null;
while ((ze = zis.getNextEntry()) != null)
{
if (ze.isDirectory())
{
job.destroy();
throw new IllegalArgumentException(
"Invalid zip archive: nested directories are not supported");
}
job.addFile(ze.getName(), zis);
zis.closeEntry();
}
IOUtils.closeQuietly(zis);
}
}