/******************************************************************************* * Imixs Workflow Technology * Copyright (C) 2001, 2008 Imixs Software Solutions GmbH, * http://www.imixs.com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free 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 GNU * General Public License for more details. * * You can receive a copy of the GNU General Public * License at http://www.gnu.org/licenses/gpl.html * * Contributors: * Imixs Software Solutions GmbH - initial API and implementation * Ralph Soika *******************************************************************************/ package com.alexanderlogistics; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Optional; import java.util.logging.Logger; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPSClient; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.imixs.workflow.FileData; import org.imixs.workflow.exceptions.PluginException; import jakarta.ejb.Stateless; import jakarta.inject.Inject; /** * The FTPConnector service provides methods to push invoices to cargosoft * * @version 1.0 * @author rsoika */ @Stateless public class FTPConnector { public static final String FTP_ERROR = "FTP_ERROR"; public static final String ENV_EXPORT_FTP_HOST = "cargosoft.export.ftp.host"; public static final String ENV_EXPORT_FTP_PATH = "cargosoft.export.ftp.path"; public static final String ENV_EXPORT_FTP_PORT = "cargosoft.export.ftp.port"; public static final String ENV_EXPORT_FTP_USER = "cargosoft.export.ftp.user"; public static final String ENV_EXPORT_FTP_PASSWORD = "cargosoft.export.ftp.password"; public static final String ENV_EXPORT_FTP_MAX_ATTACHMENT_SIZED = "cargosoft.export.ftp.maxattachmentsize"; private static Logger logger = Logger.getLogger(FTPConnector.class.getName()); @Inject @ConfigProperty(name = ENV_EXPORT_FTP_HOST) Optional ftpServer; @Inject @ConfigProperty(name = ENV_EXPORT_FTP_PATH) Optional ftpPath; @Inject @ConfigProperty(name = ENV_EXPORT_FTP_PORT, defaultValue = "21") Optional ftpPort; @Inject @ConfigProperty(name = ENV_EXPORT_FTP_USER) Optional ftpUser; @Inject @ConfigProperty(name = ENV_EXPORT_FTP_PASSWORD) Optional ftpPassword; /** * This method transfers a file to a FTP server using atomic upload. * The file is first uploaded with a temporary name and then renamed * to avoid race conditions with the receiver. * * @param fileData object containing the file to upload * @throws PluginException if the upload fails */ public void put(FileData fileData) throws PluginException { if (!ftpServer.isPresent()) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: no ftp host name provided (" + ENV_EXPORT_FTP_HOST + ")!"); } String fileName = fileData.getName(); // Create temporary filename to avoid race conditions String tempFileName = fileName + ".part"; // Compute file path String ftpWorkingPath = ftpPath.get(); if (!ftpWorkingPath.startsWith("/")) { ftpWorkingPath = "/" + ftpWorkingPath; } if (!ftpWorkingPath.endsWith("/")) { ftpWorkingPath = ftpWorkingPath + "/"; } InputStream writer = null; FTPClient ftpClient = null; try { logger.info("├── 🔜 uploading " + fileName + " to FTP server: " + ftpServer + " ..."); logger.info("│ ├── port=" + ftpPort.get().intValue()); logger.info("│ ├── working directory=" + ftpWorkingPath); logger.info("│ ├── user=" + ftpUser.get()); // logger.info("│ ├── password=" + ftpPassword.get()); ftpClient = new FTPSClient("TLS", false); ftpClient.setBufferSize(8192); ftpClient.connect(ftpServer.get(), ftpPort.get().intValue()); if (ftpClient.login(ftpUser.get(), ftpPassword.get()) == false) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: login failed!"); } ftpClient.enterLocalPassiveMode(); ftpClient.setFileType(FTP.ASCII_FILE_TYPE); ftpClient.setControlEncoding("UTF-8"); // Verify directories if (!ftpClient.changeWorkingDirectory(ftpWorkingPath)) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: missing working directory '" + ftpWorkingPath + "' : " + ftpClient.getReplyString()); } // Upload file to FTP server with temporary name writer = new ByteArrayInputStream(fileData.getContent()); if (!ftpClient.storeFile(tempFileName, writer)) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: unable to write '" + ftpWorkingPath + tempFileName + "' : " + ftpClient.getReplyString()); } // Rename to final name - this is an atomic operation logger.info("│ ├── rename '" + tempFileName + "' to '" + fileName + " ..."); if (!ftpClient.rename(tempFileName, fileName)) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: unable to rename '" + tempFileName + "' to '" + fileName + "' : " + ftpClient.getReplyString()); } logger.info("│ └── ✓ ftp transfer completed."); } catch (IOException | PluginException e) { // Log the real cause immediately, before the finally block runs - // otherwise a cleanup failure could overwrite/hide this exception. logger.warning("│ ├── ⚠️ FTP connection error: " + e.getMessage()); if (e instanceof PluginException) { throw (PluginException) e; } throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: " + e.getMessage(), e); } finally { // Cleanup - never let a cleanup failure mask the primary exception try { if (writer != null) { writer.close(); } if (ftpClient != null && ftpClient.isConnected()) { ftpClient.logout(); ftpClient.disconnect(); } } catch (IOException cleanupException) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed during cleanup: " + cleanupException.getMessage(), cleanupException); } } } /** * This method reads data form the current working directory * * @param snapshot * @throws ArchiveException * @return data */ public byte[] get(FTPClient ftpClient, String fileName) throws PluginException { if (ftpClient == null) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: no ftpClient provided!"); } long l = System.currentTimeMillis(); ByteArrayOutputStream bos = null; try { bos = new ByteArrayOutputStream(); ftpClient.retrieveFile(fileName, bos); byte[] result = bos.toByteArray(); logger.finest("......" + fileName + " transfered successfull from " + ftpServer + " in " + (System.currentTimeMillis() - l) + "ms"); return result; } catch (IOException e) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: " + e.getMessage(), e); } finally { // do logout.... try { if (bos != null) { bos.close(); } } catch (IOException e) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: " + e.getMessage(), e); } } } /** * This method changes the current working sub-directy. If no corresponding * directory exits the method creats one. * * @throws ArchiveException */ @SuppressWarnings("unused") private void changeWorkingDirectory(FTPClient ftpClient, String subDirectory) throws PluginException { // test if we have the subdreictory try { if (!ftpClient.changeWorkingDirectory(subDirectory)) { // try to creat it.... if (!ftpClient.makeDirectory(subDirectory)) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP Error: unable to create sub-directory '" + subDirectory + "' : " + ftpClient.getReplyString()); } ftpClient.changeWorkingDirectory(subDirectory); } } catch (IOException e) { throw new PluginException(CargosoftExportAdapter.class.getSimpleName(), FTP_ERROR, "FTP file transfer failed: " + e.getMessage(), e); } } }