/*
 * R Service Bus
 * 
 * Copyright (c) Copyright of Open Analytics NV, 2010-2021
 * 
 * ===========================================================================
 * 
 * This file is part of R Service Bus.
 * 
 * R Service Bus is free software: you can redistribute it and/or modify
 * it under the terms of the Apache License as published by
 * The Apache Software Foundation, either version 2 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
 * Apache License for more details.
 * 
 * You should have received a copy of the Apache License
 * along with R Service Bus.  If not, see <http://www.apache.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 "Open Analytics &lt;rsb.development@openanalytics.eu&gt;"
 */
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 IllegalJobDataException, IOException {
		if (Constants.MULTIPLE_FILES_JOB_CONFIGURATION.equals(name)) {
			loadJobConfiguration(is);
		}
		else {
			addJobFile(name, is);
		}
	}
	
	/**
	 * Adds all the files contained in a Zip archive to a job. Rejects Zips that contain
	 * sub-directories.
	 * 
	 * @param data input stream of zip file
	 */
	public void addFilesFromZip(final InputStream data)
			throws IllegalJobDataException, IOException {
		try (final ZipInputStream zis = new ZipInputStream(data)) {
			ZipEntry ze= null;
			
			while ((ze= zis.getNextEntry()) != null) {
				if (ze.isDirectory()) {
					destroy();
					throw new IllegalJobDataException("Invalid zip archive: nested directories are not supported");
				}
				addFile(ze.getName(), zis);
				zis.closeEntry();
			}
		}
	}
	
	private void addJobFile(final String name, final InputStream is)
			throws IllegalJobDataException, FileNotFoundException, IOException {
		final File jobFile= new File(this.temporaryDirectory, name);
		
		if (StringUtils.equalsIgnoreCase(FilenameUtils.getExtension(name), Constants.R_SCRIPT_FILE_EXTENSION)) {
			if (this.rScriptFile != null) {
				throw new IllegalJobDataException("Only one R script is allowed per job");
			}
			this.rScriptFile= jobFile;
		}
		
		try (final FileOutputStream fos= new FileOutputStream(jobFile)) {
			IOUtils.copy(is, 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 {
			if (temporaryDirectory.isDirectory()) {
				FileUtils.forceDelete(temporaryDirectory);
			}
		} catch (final IOException e) {
			throw new RuntimeException("Can't release resources of: " + this, e);
		}
	}
	
    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
     * @throws IllegalJobDataException 
     */
    public static void addDataToJob(final String contentType,
                                    final String name,
                                    final InputStream data,
                                    final MultiFilesJob job) throws IOException, IllegalJobDataException
    {
        // 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()))))
        {
            job.addFilesFromZip(data);
        }
        else
        {
            job.addFile(name, data);
        }
    }
    
}

@@ -976,26 +976,27 @@
 java.io.
-FileWriter
+IOException
 ;%0Aimport
@@ -1001,34 +1001,63 @@
 rt java.io.I
-OException
+nputStream;%0Aimport java.io.Serializable
 ;%0Aimport jav
@@ -1054,38 +1054,52 @@
 import java.
+n
 io.
-InputStream
+charset.StandardCharsets
 ;%0Aimport jav
@@ -1096,39 +1096,38 @@
 import java.
+n
 io.
-Serializab
+file.Fi
 le
+s
 ;%0Aimport jav
@@ -1570,55 +1570,8 @@
 ce;%0A
-import org.springframework.util.FileCopyUtils;%0A
 impo
@@ -5829,68 +5829,84 @@
 File
-CopyUtils.copy(template.render(), new FileWriter(resultFile)
+s.writeString(resultFile.toPath(), template.render(), StandardCharsets.UTF_8
 );%0A 

Change encoding of error file to UTF-8