/** * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019) * * contact.vitam@culture.gouv.fr * * This software is a computer program whose purpose is to implement a digital archiving back-office system managing * high volumetry securely and efficiently. * * This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free * software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as * circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info". * * As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license, * users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the * successive licensors have only limited liability. * * In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or * developing or reproducing the software by the user in light of its specific status of free software, that may mean * that it is complicated to manipulate, and that also therefore means that it is reserved for developers and * experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the * software's suitability as regards their requirements in conditions enabling the security of their systems and/or data * to be ensured and, more generally, to use and operate it in the same conditions as regards security. * * The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you * accept its terms. */ package fr.gouv.vitam.worker.core.impl; 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.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.core.Response; import com.fasterxml.jackson.databind.JsonNode; import fr.gouv.vitam.common.FileUtil; import fr.gouv.vitam.common.ParametersChecker; import fr.gouv.vitam.common.PropertiesUtils; import fr.gouv.vitam.common.client.DefaultClient; import fr.gouv.vitam.common.exception.InvalidParseOperationException; import fr.gouv.vitam.common.json.JsonHandler; import fr.gouv.vitam.common.logging.SysErrLogger; import fr.gouv.vitam.common.logging.VitamLogger; import fr.gouv.vitam.common.logging.VitamLoggerFactory; import fr.gouv.vitam.common.model.VitamAutoCloseable; import fr.gouv.vitam.common.stream.StreamUtils; import fr.gouv.vitam.logbook.common.parameters.LogbookLifeCyclesClientHelper; import fr.gouv.vitam.logbook.lifecycles.client.LogbookLifeCyclesClient; import fr.gouv.vitam.logbook.lifecycles.client.LogbookLifeCyclesClientFactory; import fr.gouv.vitam.processing.common.exception.ProcessingException; import fr.gouv.vitam.processing.common.model.IOParameter; import fr.gouv.vitam.processing.common.model.ProcessingUri; import fr.gouv.vitam.worker.common.HandlerIO; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageNotFoundException; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageServerException; import fr.gouv.vitam.workspace.client.WorkspaceClient; import fr.gouv.vitam.workspace.client.WorkspaceClientFactory; /** * Handler input and output parameter */ public class HandlerIOImpl implements VitamAutoCloseable, HandlerIO { private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(HandlerIOImpl.class); /** * Not Enough Param */ public static final String NOT_ENOUGH_PARAM = "Input/Output io parameter list is not enough"; /** * Not Conform Param */ public static final String NOT_CONFORM_PARAM = "Input/Output io parameter is not correct"; private static final String HANDLER_INPUT_NOT_FOUND = "Handler input not found exception: "; private final List<Object> input = new ArrayList<>(); private final List<ProcessingUri> output = new ArrayList<>(); private final String containerName; private final String workerId; private final File localDirectory; private final Map<String, Object> memoryMap = new HashMap<>(); private final WorkspaceClient client; private final LogbookLifeCyclesClient lifecyclesClient; private final LogbookLifeCyclesClientHelper helper; /** * Constructor with local root path * * @param containerName * @param workerId */ public HandlerIOImpl(String containerName, String workerId) { this.containerName = containerName; this.workerId = workerId; localDirectory = PropertiesUtils.fileFromTmpFolder(containerName + "_" + workerId); localDirectory.mkdirs(); client = WorkspaceClientFactory.getInstance().getClient(); lifecyclesClient = LogbookLifeCyclesClientFactory.getInstance().getClient(); helper = new LogbookLifeCyclesClientHelper(); } @Override public LogbookLifeCyclesClient getLifecyclesClient() { return lifecyclesClient; } @Override public LogbookLifeCyclesClientHelper getHelper() { return helper; } @Override public void addInIOParameters(List<IOParameter> list) { for (final IOParameter in : list) { switch (in.getUri().getPrefix()) { case WORKSPACE: try { // TODO P1 : remove optional when lazy file loading is implemented input.add(findFileFromWorkspace(in.getUri().getPath(), in.getOptional())); break; } catch (final FileNotFoundException e) { throw new IllegalArgumentException(HANDLER_INPUT_NOT_FOUND + in.getUri().getPath(), e); } case MEMORY: input.add(memoryMap.get(in.getUri().getPath())); break; case VALUE: input.add(in.getUri().getPath()); break; default: throw new IllegalArgumentException( HANDLER_INPUT_NOT_FOUND + in.getUri().getPrefix() + ":" + in.getUri().getPath()); } } } @Override public void addOutIOParameters(List<IOParameter> list) { for (final IOParameter out : list) { switch (out.getUri().getPrefix()) { case WORKSPACE: case MEMORY: output.add(out.getUri()); break; default: throw new IllegalArgumentException("Handler Output not conform: " + out.getUri()); } } } @Override public void reset() { input.clear(); output.clear(); helper.clear(); } @Override public void close() { client.close(); lifecyclesClient.close(); partialClose(); } /** * Close the HandlerIO, including temporary files and directories at the end of the step Workflow execution, but do * not close the WorkspaceClient */ public void partialClose() { reset(); memoryMap.clear(); if (!FileUtil.deleteRecursive(localDirectory)) { LOGGER.warn("Cannot clear the temporary directory: " + localDirectory); } } @Override public List<Object> getInput() { return input; } @Override public Object getInput(int rank) { return input.get(rank); } @Override public List<ProcessingUri> getOutput() { return output; } @Override public ProcessingUri getOutput(int rank) { return output.get(rank); } @Override public HandlerIO addOuputResult(int rank, Object object) throws ProcessingException { return addOuputResult(rank, object, false); } @Override public HandlerIO addOuputResult(int rank, Object object, boolean deleteLocal) throws ProcessingException { final ProcessingUri uri = output.get(rank); if (uri == null) { throw new IllegalArgumentException(HANDLER_INPUT_NOT_FOUND + rank); } switch (uri.getPrefix()) { case MEMORY: memoryMap.put(uri.getPath(), object); break; case VALUE: // Ignore break; case WORKSPACE: if (!(object instanceof File)) { throw new ProcessingException("Not a File but WORKSPACE out parameter: " + uri); } transferFileToWorkspace(uri.getPath(), (File) object, deleteLocal); break; default: throw new IllegalArgumentException(HANDLER_INPUT_NOT_FOUND + uri); } return this; } @Override public String getContainerName() { return containerName; } @Override public String getWorkerId() { return workerId; } @Override public File getLocalPathRoot() { return localDirectory; } @Override public File getNewLocalFile(String name) { final File file = new File(localDirectory.getAbsolutePath() + "/" + name); file.getParentFile().mkdirs(); return file; } @Override public boolean checkHandlerIO(int outputNumber, List<Class<?>> clasz) { if (getInput().size() != clasz.size() || getOutput().size() != outputNumber) { LOGGER.error("InputSize shoul be {} but is {} OR OutputSize should be {} but is {}", clasz.size(), getInput().size(), outputNumber, getOutput().size()); return false; } for (int i = 0; i < getInput().size(); i++) { final Object object = getInput(i); if (object == null || !clasz.get(i).isInstance(object)) { LOGGER.error("Input class should be {} but is {}", clasz.get(i).getName(), object != null ? object.getClass().getName() : "Null object"); return false; } } return true; } @Override public void transferFileToWorkspace(String workspacePath, File sourceFile, boolean toDelete) throws ProcessingException { try { ParametersChecker.checkParameter("Workspace path is a mandatory parameter", workspacePath); ParametersChecker.checkParameter("Source file is a mandatory parameter", sourceFile); } catch (final IllegalArgumentException e) { throw new ProcessingException(e); } if (!sourceFile.canRead()) { throw new ProcessingException("Cannot found source file: " + sourceFile); } try (FileInputStream inputStream = new FileInputStream(sourceFile)) { transferInputStreamToWorkspace(workspacePath, inputStream); if (toDelete && !sourceFile.delete()) { LOGGER.warn("File could not be deleted: " + sourceFile); } } catch (final IOException e) { throw new ProcessingException("Cannot found or read source file: " + sourceFile, e); } } @Override public void transferInputStreamToWorkspace(String workspacePath, InputStream inputStream) throws ProcessingException { try { client.putObject(containerName, workspacePath, inputStream); } catch (final ContentAddressableStorageServerException e) { throw new ProcessingException("Cannot write to workspace: " + containerName + "/" + workspacePath, e); } } /** * Get the File associated with this filename, trying in this order: as fullpath, as in Vitam Config Folder, as * Resources file * * @param containerName container name * @param objectName object name * @param workerId worker id * @param optional if file is optional * @return file if found, if not found, null if optional * @throws FileNotFoundException if file is not found and not optional */ private final File findFileFromWorkspace(String objectName, boolean optional) throws FileNotFoundException { // First try as full path File file = null; // TODO P1 : this optional situation would be treated later when lazy file loading is implemented if (optional) { try { file = getFileFromWorkspace(objectName); } catch (final ContentAddressableStorageNotFoundException | ContentAddressableStorageServerException | IOException e) { LOGGER.debug(e); file = null; } if (file != null && !file.exists()) { file = null; } } else { try { file = getFileFromWorkspace(objectName); } catch (final ContentAddressableStorageNotFoundException | ContentAddressableStorageServerException | IOException e) { // need to rewrite the exception LOGGER.error(e); throw new FileNotFoundException("File not found: " + objectName); } if (!file.exists()) { throw new FileNotFoundException("File not found: " + objectName); } } return file; } @Override public File getFileFromWorkspace(String objectName) throws IOException, ContentAddressableStorageNotFoundException, ContentAddressableStorageServerException { final File file = getNewLocalFile(objectName); if (!file.exists()) { Response response = null; try { response = client.getObject(containerName, objectName); if (response != null) { try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { StreamUtils.copy((InputStream) response.getEntity(), fileOutputStream); } } } finally { client.consumeAnyEntityAndClose(response); } } return file; } @Override public InputStream getInputStreamFromWorkspace(String objectName) throws IOException, ContentAddressableStorageNotFoundException, ContentAddressableStorageServerException { return new FileInputStream(getFileFromWorkspace(objectName)); } @Override public Response getInputStreamNoCachedFromWorkspace(String objectName) throws ContentAddressableStorageNotFoundException, ContentAddressableStorageServerException { return client.getObject(containerName, objectName); } @Override public void consumeAnyEntityAndClose(Response response) { client.consumeAnyEntityAndClose(response); } @Override public JsonNode getJsonFromWorkspace(String jsonFilePath) throws ProcessingException { Response response = null; InputStream is = null; try { response = client.getObject(containerName, jsonFilePath); is = (InputStream) response.getEntity(); if (is != null) { return JsonHandler.getFromInputStream(is); } else { LOGGER.error("Json not found"); throw new ProcessingException("Json not found"); } } catch (final InvalidParseOperationException e) { LOGGER.debug("Json wrong format", e); throw new ProcessingException(e); } catch (ContentAddressableStorageNotFoundException | ContentAddressableStorageServerException e) { LOGGER.debug("Workspace Server Error", e); throw new ProcessingException(e); } finally { if (is != null) { StreamUtils.closeSilently(is); } DefaultClient.staticConsumeAnyEntityAndClose(response); } } @Override public boolean deleteLocalFile(String objectName) { final File file = getNewLocalFile(objectName); if (file.exists()) { return file.delete(); } return true; } @Override public List<URI> getUriList(String containerName, String folderName) throws ProcessingException { try { return client.getListUriDigitalObjectFromFolder(containerName, folderName); } catch (ContentAddressableStorageServerException e) { LOGGER.debug("Workspace Server Error", e); throw new ProcessingException(e); } } @Override public void transferJsonToWorkspace(String collectionName, String objectName, JsonNode jsonNode, boolean toDelete) throws ProcessingException { File file = getNewLocalFile(objectName); try { JsonHandler.writeAsFile(jsonNode, file); transferFileToWorkspace(collectionName + File.separator + objectName, file, toDelete); } catch (final InvalidParseOperationException e) { throw new ProcessingException("Invalid parse Exception: " + file, e); } } }