diff --git a/dotCMS/src/main/java/com/dotcms/jobs/business/processor/impl/ImportContentletProcessor.java b/dotCMS/src/main/java/com/dotcms/jobs/business/processor/impl/ImportContentletProcessor.java
new file mode 100644
index 00000000000..258cd61c353
--- /dev/null
+++ b/dotCMS/src/main/java/com/dotcms/jobs/business/processor/impl/ImportContentletProcessor.java
@@ -0,0 +1,760 @@
+package com.dotcms.jobs.business.processor.impl;
+
+import com.dotcms.api.web.HttpServletRequestThreadLocal;
+import com.dotcms.contenttype.model.type.ContentType;
+import com.dotcms.jobs.business.error.JobCancellationException;
+import com.dotcms.jobs.business.error.JobProcessingException;
+import com.dotcms.jobs.business.error.JobValidationException;
+import com.dotcms.jobs.business.job.Job;
+import com.dotcms.jobs.business.processor.Cancellable;
+import com.dotcms.jobs.business.processor.ExponentialBackoffRetryPolicy;
+import com.dotcms.jobs.business.processor.JobProcessor;
+import com.dotcms.jobs.business.processor.Queue;
+import com.dotcms.jobs.business.util.JobUtil;
+import com.dotcms.mock.request.FakeHttpRequest;
+import com.dotcms.mock.request.MockHeaderRequest;
+import com.dotcms.mock.request.MockSessionRequest;
+import com.dotcms.repackage.com.csvreader.CsvReader;
+import com.dotcms.rest.api.v1.temp.DotTempFile;
+import com.dotmarketing.beans.Host;
+import com.dotmarketing.business.APILocator;
+import com.dotmarketing.db.HibernateUtil;
+import com.dotmarketing.exception.DotDataException;
+import com.dotmarketing.exception.DotHibernateException;
+import com.dotmarketing.exception.DotSecurityException;
+import com.dotmarketing.portlets.contentlet.action.ImportAuditUtil;
+import com.dotmarketing.util.AdminLogger;
+import com.dotmarketing.util.FileUtil;
+import com.dotmarketing.util.ImportUtil;
+import com.dotmarketing.util.Logger;
+import com.dotmarketing.util.UtilMethods;
+import com.dotmarketing.util.WebKeys;
+import com.google.common.hash.Hashing;
+import com.liferay.portal.model.User;
+import com.liferay.portal.util.Constants;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.LongConsumer;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Processor implementation for handling content import operations in dotCMS. This class provides
+ * functionality to import content from CSV files, with support for both preview and publish
+ * operations, as well as multilingual content handling.
+ *
+ *
The processor implements both {@link JobProcessor} and {@link Cancellable} interfaces to
+ * provide job processing and cancellation capabilities. It's annotated with {@link Queue} to
+ * specify the queue name and {@link ExponentialBackoffRetryPolicy} to define retry behavior.
+ *
+ * Key features:
+ *
+ * - Support for both preview and publish operations
+ * - Multilingual content import capabilities
+ * - Progress tracking during import
+ * - Cancellation support
+ * - Validation of import parameters and content
+ *
+ *
+ * @see JobProcessor
+ * @see Cancellable
+ * @see Queue
+ * @see ExponentialBackoffRetryPolicy
+ */
+@Queue("importContentlet")
+@ExponentialBackoffRetryPolicy(
+ maxRetries = 0
+)
+public class ImportContentletProcessor implements JobProcessor, Cancellable {
+
+ private static final String PARAMETER_LANGUAGE = "language";
+ private static final String PARAMETER_FIELDS = "fields";
+ private static final String PARAMETER_USER_ID = "userId";
+ private static final String PARAMETER_SITE_IDENTIFIER = "siteIdentifier";
+ private static final String PARAMETER_SITE_NAME = "siteName";
+ private static final String PARAMETER_CONTENT_TYPE = "contentType";
+ private static final String PARAMETER_WORKFLOW_ACTION_ID = "workflowActionId";
+ private static final String PARAMETER_CMD = Constants.CMD;
+ private static final String CMD_PREVIEW = com.dotmarketing.util.Constants.PREVIEW;
+ private static final String CMD_PUBLISH = com.dotmarketing.util.Constants.PUBLISH;
+
+ private static final String LANGUAGE_CODE_HEADER = "languageCode";
+ private static final String COUNTRY_CODE_HEADER = "countryCode";
+
+ /**
+ * Flag to track cancellation requests for the current import operation.
+ */
+ private final AtomicBoolean cancellationRequested = new AtomicBoolean(false);
+
+ /**
+ * Storage for metadata about the import operation results.
+ */
+ private Map resultMetadata = new HashMap<>();
+
+ /**
+ * Processes a content import job. This method serves as the main entry point for the import
+ * operation and handles both preview and publish modes.
+ *
+ * The method performs the following steps:
+ *
+ * - Validates the input parameters and retrieves the necessary user information
+ * - Retrieves and validates the import file
+ * - Sets up progress tracking
+ * - Executes either preview or publish operation based on the command
+ * - Ensures proper progress updates throughout the process
+ *
+ *
+ * @param job The job containing import parameters and configuration
+ * @throws JobProcessingException if any error occurs during processing
+ */
+ @Override
+ public void process(final Job job) throws JobProcessingException {
+
+ final String command = getCommand(job);
+
+ final User user;
+ try {
+ user = getUser(job);
+ } catch (Exception e) {
+ Logger.error(this, "Error retrieving user", e);
+ throw new JobProcessingException(job.id(), "Error retrieving user", e);
+ }
+
+ Logger.info(this, String.format("Processing import contentlets job [%s], "
+ + "with command [%s] and user [%s]", job.id(), command, user.getUserId()));
+
+ // Retrieving the import file
+ Optional tempFile = JobUtil.retrieveTempFile(job);
+ if (tempFile.isEmpty()) {
+ Logger.error(this.getClass(), "Unable to retrieve the import file. Quitting the job.");
+ throw new JobValidationException(job.id(), "Unable to retrieve the import file.");
+ }
+
+ // Validate the job has the required data
+ validate(job);
+
+ final var language = getLanguage(job);
+ final var fileToImport = tempFile.get().file;
+ final long totalLines = totalLines(job, fileToImport);
+ final Charset charset = language == -1 ?
+ Charset.defaultCharset() : FileUtil.detectEncodeType(fileToImport);
+
+ // Create a progress callback function
+ final var progressTracker = job.progressTracker().orElseThrow(
+ () -> new JobProcessingException(job.id(), "Progress tracker not found")
+ );
+ final LongConsumer progressCallback = processedLines -> {
+ float progressPercentage = (float) processedLines / totalLines;
+ // This ensures the progress is between 0.0 and 1.0
+ progressTracker.updateProgress(Math.min(1.0f, Math.max(0.0f, progressPercentage)));
+ };
+
+ if (CMD_PREVIEW.equals(command)) {
+ handlePreview(job, language, fileToImport, charset, user, progressCallback);
+ } else if (CMD_PUBLISH.equals(command)) {
+ handlePublish(job, language, fileToImport, charset, user, progressCallback);
+ }
+
+ if (!cancellationRequested.get()) {
+ // Ensure the progress is at 100% when the job is done
+ progressTracker.updateProgress(1.0f);
+ }
+ }
+
+ /**
+ * Handles cancellation requests for the import operation. When called, it marks the operation
+ * for cancellation.
+ *
+ * @param job The job to be cancelled
+ * @throws JobCancellationException if any error occurs during cancellation
+ */
+ @Override
+ public void cancel(Job job) throws JobCancellationException {
+
+ Logger.info(this.getClass(), "Job cancellation requested: " + job.id());
+ cancellationRequested.set(true);
+
+ final var importId = jobIdToLong(job.id());
+ ImportAuditUtil.cancelledImports.put(importId, Calendar.getInstance().getTime());
+ }
+
+ /**
+ * Retrieves metadata about the import operation results.
+ *
+ * @param job The job whose metadata is being requested
+ * @return A map containing result metadata, or an empty map if no metadata is available
+ */
+ @Override
+ public Map getResultMetadata(Job job) {
+
+ if (resultMetadata.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ return resultMetadata;
+ }
+
+ /**
+ * Handles the preview phase of content import. This method analyzes the CSV file and provides
+ * information about potential issues without actually importing the content.
+ *
+ * @param job The import job configuration
+ * @param language The target language for import
+ * @param fileToImport The CSV file to be imported
+ * @param charset The character encoding of the import file
+ * @param user The user performing the import
+ * @param progressCallback Callback for tracking import progress
+ */
+ private void handlePreview(final Job job, long language, final File fileToImport,
+ final Charset charset, final User user, final LongConsumer progressCallback) {
+
+ try {
+ try (Reader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(fileToImport), charset))) {
+
+ CsvReader csvReader = createCsvReader(reader);
+ CsvHeaderInfo headerInfo = processHeadersBasedOnLanguage(job, language, csvReader);
+
+ final var previewResult = generatePreview(job, user,
+ headerInfo.headers, csvReader, headerInfo.languageCodeColumn,
+ headerInfo.countryCodeColumn, progressCallback);
+ resultMetadata = new HashMap<>(previewResult);
+ }
+ } catch (Exception e) {
+
+ try {
+ HibernateUtil.rollbackTransaction();
+ } catch (DotHibernateException he) {
+ Logger.error(this, he.getMessage(), he);
+ }
+
+ final var errorMessage = "An error occurred when analyzing the CSV file.";
+ Logger.error(this, errorMessage, e);
+ throw new JobProcessingException(job.id(), errorMessage, e);
+ }
+ }
+
+ /**
+ * Handles the publish phase of content import. This method performs the actual content import
+ * operation, creating or updating content based on the CSV file.
+ *
+ * @param job The import job configuration
+ * @param language The target language for import
+ * @param fileToImport The CSV file to be imported
+ * @param charset The character encoding of the import file
+ * @param user The user performing the import
+ * @param progressCallback Callback for tracking import progress
+ */
+ private void handlePublish(final Job job, long language, final File fileToImport,
+ final Charset charset, final User user, final LongConsumer progressCallback) {
+
+ AdminLogger.log(
+ ImportContentletProcessor.class, "process",
+ "Importing Contentlets", user
+ );
+
+ try {
+ try (Reader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(fileToImport), charset))) {
+
+ CsvReader csvReader = createCsvReader(reader);
+ CsvHeaderInfo headerInfo = readPublishHeaders(language, csvReader);
+
+ final var importResults = processFile(job, user, headerInfo.headers, csvReader,
+ headerInfo.languageCodeColumn, headerInfo.countryCodeColumn,
+ progressCallback);
+ resultMetadata = new HashMap<>(importResults);
+ }
+ } catch (Exception e) {
+
+ try {
+ HibernateUtil.rollbackTransaction();
+ } catch (DotHibernateException he) {
+ Logger.error(this, he.getMessage(), he);
+ }
+
+ final var errorMessage = "An error occurred when importing the CSV file.";
+ Logger.error(this, errorMessage, e);
+ throw new JobProcessingException(job.id(), errorMessage, e);
+ } finally {
+ final var importId = jobIdToLong(job.id());
+ ImportAuditUtil.cancelledImports.remove(importId);
+ }
+ }
+
+ /**
+ * Reads and analyzes the content of the CSV import file to determine potential errors,
+ * inconsistencies or warnings, and provide the user with useful information regarding the
+ * contents of the file.
+ *
+ * @param job - The {@link Job} being processed.
+ * @param user - The {@link User} performing this action.
+ * @param csvHeaders - The headers that make up the CSV file.
+ * @param csvReader - The actual data contained in the CSV file.
+ * @param languageCodeHeaderColumn - The column name containing the language code.
+ * @param countryCodeHeaderColumn - The column name containing the country code.
+ * @param progressCallback - The callback function to update the progress of the job.
+ * @throws DotDataException An error occurred when analyzing the CSV file.
+ */
+ private Map> generatePreview(final Job job, final User user,
+ final String[] csvHeaders, final CsvReader csvReader,
+ final int languageCodeHeaderColumn, int countryCodeHeaderColumn,
+ final LongConsumer progressCallback) throws DotDataException {
+
+ final var currentSiteId = getSiteIdentifier(job);
+ final var currentSiteName = getSiteName(job);
+ final var contentType = getContentType(job);
+ final var fields = getFields(job);
+ final var language = getLanguage(job);
+ final var workflowActionId = getWorkflowActionId(job);
+ final var httpReq = getRequest(user, currentSiteName);
+
+ Logger.info(this, "-------- Starting Content Import Preview -------- ");
+ Logger.info(this, String.format("-> Content Type ID: %s", contentType));
+
+ return ImportUtil.importFile(0L, currentSiteId, contentType, fields, true,
+ (language == -1), user, language, csvHeaders, csvReader, languageCodeHeaderColumn,
+ countryCodeHeaderColumn, workflowActionId, httpReq, progressCallback);
+ }
+
+ /**
+ * Executes the content import process after the review process has been run and displayed to
+ * the user.
+ *
+ * @param job - The {@link Job} being processed.
+ * @param user - The {@link User} performing this action.
+ * @param csvHeaders - The headers that make up the CSV file.
+ * @param csvReader - The actual data contained in the CSV file.
+ * @param languageCodeHeaderColumn - The column name containing the language code.
+ * @param countryCodeHeaderColumn - The column name containing the country code.
+ * @param progressCallback - The callback function to update the progress of the job.
+ * @return The status of the content import performed by dotCMS. This provides information
+ * regarding inconsistencies, errors, warnings and/or precautions to the user.
+ * @throws DotDataException An error occurred when importing the CSV file.
+ */
+ private HashMap> processFile(final Job job, final User user,
+ final String[] csvHeaders, final CsvReader csvReader,
+ final int languageCodeHeaderColumn, final int countryCodeHeaderColumn,
+ final LongConsumer progressCallback) throws DotDataException {
+
+ final var currentSiteId = getSiteIdentifier(job);
+ final var currentSiteName = getSiteName(job);
+ final var contentType = getContentType(job);
+ final var fields = getFields(job);
+ final var language = getLanguage(job);
+ final var workflowActionId = getWorkflowActionId(job);
+ final var httpReq = getRequest(user, currentSiteName);
+ final var importId = jobIdToLong(job.id());
+
+ Logger.info(this, "-------- Starting Content Import Process -------- ");
+ Logger.info(this, String.format("-> Content Type ID: %s", contentType));
+
+ return ImportUtil.importFile(importId, currentSiteId, contentType, fields, false,
+ (language == -1), user, language, csvHeaders, csvReader, languageCodeHeaderColumn,
+ countryCodeHeaderColumn, workflowActionId, httpReq, progressCallback);
+ }
+
+ /**
+ * Retrieve the command from the job parameters
+ *
+ * @param job input job
+ * @return the command from the job parameters, if not found, return the default value "preview"
+ */
+ private String getCommand(final Job job) {
+
+ if (!job.parameters().containsKey(PARAMETER_CMD)) {
+ return CMD_PREVIEW;
+ }
+
+ return (String) job.parameters().get(PARAMETER_CMD);
+ }
+
+ /**
+ * Retrieve the user from the job parameters
+ *
+ * @param job input job
+ * @return the user from the job parameters
+ * @throws DotDataException if an error occurs during the user retrieval
+ * @throws DotSecurityException if we don't have the necessary permissions to retrieve the user
+ */
+ private User getUser(final Job job) throws DotDataException, DotSecurityException {
+ final var userId = (String) job.parameters().get(PARAMETER_USER_ID);
+ return APILocator.getUserAPI().loadUserById(userId);
+ }
+
+ /**
+ * Retrieves the site identifier from the job parameters.
+ *
+ * @param job The job containing the parameters
+ * @return The site identifier string, or null if not present in parameters
+ */
+ private String getSiteIdentifier(final Job job) {
+ return (String) job.parameters().get(PARAMETER_SITE_IDENTIFIER);
+ }
+
+ /**
+ * Retrieves the site name from the job parameters.
+ *
+ * @param job The job containing the parameters
+ * @return The site name string, or null if not present in parameters
+ */
+ private String getSiteName(final Job job) {
+ return (String) job.parameters().get(PARAMETER_SITE_NAME);
+ }
+
+ /**
+ * Retrieves the content type from the job parameters.
+ *
+ * @param job The job containing the parameters
+ * @return The content type string, or null if not present in parameters
+ */
+ private String getContentType(final Job job) {
+ return (String) job.parameters().get(PARAMETER_CONTENT_TYPE);
+ }
+
+ /**
+ * Retrieves the workflow action ID from the job parameters.
+ *
+ * @param job The job containing the parameters
+ * @return The workflow action ID string, or null if not present in parameters
+ */
+ private String getWorkflowActionId(final Job job) {
+ return (String) job.parameters().get(PARAMETER_WORKFLOW_ACTION_ID);
+ }
+
+ /**
+ * Retrieves the language setting from the job parameters. Handles both string and long
+ * parameter types.
+ *
+ * @param job The job containing the parameters
+ * @return The language ID as a long, or -1 if not specified
+ */
+ private long getLanguage(final Job job) {
+
+ if (!job.parameters().containsKey(PARAMETER_LANGUAGE)
+ || job.parameters().get(PARAMETER_LANGUAGE) == null) {
+ return -1;
+ }
+
+ final Object language = job.parameters().get(PARAMETER_LANGUAGE);
+
+ if (language instanceof String) {
+ return Long.parseLong((String) language);
+ }
+
+ return (long) language;
+ }
+
+ /**
+ * Retrieves the fields array from the job parameters.
+ *
+ * @param job The job containing the parameters
+ * @return An array of field strings, or an empty array if no fields are specified
+ */
+ public String[] getFields(final Job job) {
+
+ if (!job.parameters().containsKey(PARAMETER_FIELDS)
+ || job.parameters().get(PARAMETER_FIELDS) == null) {
+ return new String[0];
+ }
+
+ return (String[]) job.parameters().get(PARAMETER_FIELDS);
+ }
+
+ /**
+ * Validates the job parameters and content type. Performs security checks to prevent
+ * unauthorized host imports.
+ *
+ * @param job The job to validate
+ * @throws JobValidationException if validation fails
+ * @throws JobProcessingException if an error occurs during content type validation
+ */
+ private void validate(final Job job) {
+
+ if (getContentType(job) != null && getContentType(job).isEmpty()) {
+ Logger.error(this.getClass(), "A Content Type is required");
+ throw new JobValidationException(job.id(), "A Content Type is required");
+ } else if (getWorkflowActionId(job) != null && getWorkflowActionId(job).isEmpty()) {
+ Logger.error(this.getClass(), "Workflow action type is required");
+ throw new JobValidationException(job.id(), "Workflow action type is required");
+ }
+
+ // Security measure to prevent invalid attempts to import a host.
+ try {
+ final ContentType hostContentType = APILocator.getContentTypeAPI(
+ APILocator.systemUser()).find(Host.HOST_VELOCITY_VAR_NAME
+ );
+ final boolean isHost = (hostContentType.id().equals(getContentType(job)));
+ if (isHost) {
+ Logger.error(this, "Invalid attempt to import a host.");
+ throw new JobValidationException(job.id(), "Invalid attempt to import a host.");
+ }
+ } catch (DotSecurityException | DotDataException e) {
+ throw new JobProcessingException(job.id(), "Error validating content type", e);
+ }
+ }
+
+ /**
+ * Creates or retrieves an HttpServletRequest for the import operation. Uses thread-local
+ * request if available, otherwise creates a mock request with the specified user and site
+ * information.
+ *
+ * @param user The user performing the import
+ * @param siteName The name of the site for the import
+ * @return An HttpServletRequest instance configured for the import operation
+ */
+ private HttpServletRequest getRequest(final User user, final String siteName) {
+
+ if (null != HttpServletRequestThreadLocal.INSTANCE.getRequest()) {
+ return HttpServletRequestThreadLocal.INSTANCE.getRequest();
+ }
+
+ final HttpServletRequest requestProxy = new MockSessionRequest(
+ new MockHeaderRequest(
+ new FakeHttpRequest(siteName, "/").request(),
+ "referer",
+ "https://" + siteName + "/fakeRefer")
+ .request());
+ requestProxy.setAttribute(WebKeys.CMS_USER, user);
+ requestProxy.getSession().setAttribute(WebKeys.CMS_USER, user);
+ requestProxy.setAttribute(com.liferay.portal.util.WebKeys.USER_ID,
+ UtilMethods.extractUserIdOrNull(user));
+
+ return requestProxy;
+ }
+
+ /**
+ * Utility method to convert a job ID to a long value for internal processing. Uses FarmHash for
+ * efficient hash generation and distribution.
+ *
+ * @param jobId The string job identifier
+ * @return A long value representing the job ID
+ */
+ public static long jobIdToLong(final String jobId) {
+
+ // Use FarmHash for good distribution and speed
+ long hashValue = Hashing.farmHashFingerprint64()
+ .hashString(jobId, StandardCharsets.UTF_8).asLong();
+
+ // Ensure the value is positive (in the upper half of the bigint range)
+ return Math.abs(hashValue);
+ }
+
+ /**
+ * Count the number of lines in the file
+ *
+ * @param dotTempFile temporary file
+ * @return the number of lines in the file
+ */
+ private Long totalLines(final Job job, final File dotTempFile) {
+
+ long totalCount = 0;
+ try (BufferedReader reader = new BufferedReader(new FileReader(dotTempFile))) {
+ totalCount = reader.lines().count();
+ if (totalCount == 0) {
+ Logger.info(this.getClass(),
+ "No lines in CSV import file: " + dotTempFile.getName());
+ }
+ } catch (Exception e) {
+ Logger.error(this.getClass(),
+ "Error calculating total lines in CSV import file: " + e.getMessage());
+ throw new JobProcessingException(job.id(),
+ "Error calculating total lines in CSV import file", e);
+ }
+
+ return totalCount;
+ }
+
+ /**
+ * Reads and processes headers for publishing operation.
+ *
+ * @param language The target language for import
+ * @param csvreader The CSV reader containing the file data
+ * @return CsvHeaderInfo containing processed header information
+ * @throws IOException if an error occurs reading the CSV file
+ */
+ private CsvHeaderInfo readPublishHeaders(long language, CsvReader csvreader)
+ throws IOException {
+ if (language == -1 && csvreader.readHeaders()) {
+ return findLanguageColumnsInHeaders(csvreader.getHeaders());
+ }
+ return new CsvHeaderInfo(null, -1, -1);
+ }
+
+ /**
+ * Locates language-related columns in CSV headers.
+ *
+ * @param headers Array of CSV header strings
+ * @return CsvHeaderInfo containing the positions of language and country code columns
+ */
+ private CsvHeaderInfo findLanguageColumnsInHeaders(String[] headers) {
+
+ int languageCodeColumn = -1;
+ int countryCodeColumn = -1;
+
+ for (int column = 0; column < headers.length; ++column) {
+ if (headers[column].equals(LANGUAGE_CODE_HEADER)) {
+ languageCodeColumn = column;
+ }
+ if (headers[column].equals(COUNTRY_CODE_HEADER)) {
+ countryCodeColumn = column;
+ }
+ if (languageCodeColumn != -1 && countryCodeColumn != -1) {
+ break;
+ }
+ }
+
+ return new CsvHeaderInfo(headers, languageCodeColumn, countryCodeColumn);
+ }
+
+ /**
+ * Creates a CSV reader with appropriate configuration for import operations.
+ *
+ * @param reader The source reader for CSV content
+ * @return A configured CsvReader instance
+ */
+ private CsvReader createCsvReader(final Reader reader) {
+ CsvReader csvreader = new CsvReader(reader);
+ csvreader.setSafetySwitch(false);
+ return csvreader;
+ }
+
+ /**
+ * Processes CSV headers based on the specified language configuration.
+ *
+ * @param job The current import job
+ * @param language The target language for import
+ * @param csvReader The CSV reader to process headers from
+ * @return CsvHeaderInfo containing processed header information
+ * @throws IOException if an error occurs reading the CSV file
+ */
+ private CsvHeaderInfo processHeadersBasedOnLanguage(final Job job, final long language,
+ final CsvReader csvReader) throws IOException {
+ if (language != -1) {
+ validateLanguage(job, language);
+ return new CsvHeaderInfo(null, -1, -1);
+ }
+
+ return processMultilingualHeaders(job, csvReader);
+ }
+
+ /**
+ * Validates the language configuration for import operations.
+ *
+ * @param job The current import job
+ * @param language The language identifier to validate
+ */
+ private void validateLanguage(Job job, long language) {
+ if (language == 0) {
+ final var errorMessage = "Please select a valid Language.";
+ Logger.error(this, errorMessage);
+ throw new JobValidationException(job.id(), errorMessage);
+ }
+ }
+
+ /**
+ * Processes headers for multilingual content imports.
+ *
+ * @param job The current import job
+ * @param csvReader The CSV reader to process headers from
+ * @return CsvHeaderInfo containing processed multilingual header information
+ * @throws IOException if an error occurs reading the CSV file
+ */
+ private CsvHeaderInfo processMultilingualHeaders(final Job job, final CsvReader csvReader)
+ throws IOException {
+
+ if (getFields(job).length == 0) {
+ final var errorMessage =
+ "A key identifying the different Language versions of the same "
+ + "content must be defined when importing multilingual files.";
+ Logger.error(this, errorMessage);
+ throw new JobValidationException(job.id(), errorMessage);
+ }
+
+ if (!csvReader.readHeaders()) {
+ final var errorMessage = "An error occurred when attempting to read the CSV file headers.";
+ Logger.error(this, errorMessage);
+ throw new JobProcessingException(job.id(), errorMessage);
+ }
+
+ String[] headers = csvReader.getHeaders();
+ return findLanguageColumns(job, headers);
+ }
+
+ /**
+ * Locates language-related columns in CSV headers.
+ *
+ * @param headers Array of CSV header strings
+ * @return CsvHeaderInfo containing the positions of language and country code columns
+ */
+ private CsvHeaderInfo findLanguageColumns(Job job, String[] headers)
+ throws JobProcessingException {
+
+ int languageCodeColumn = -1;
+ int countryCodeColumn = -1;
+
+ for (int column = 0; column < headers.length; ++column) {
+ if (headers[column].equals(LANGUAGE_CODE_HEADER)) {
+ languageCodeColumn = column;
+ }
+ if (headers[column].equals(COUNTRY_CODE_HEADER)) {
+ countryCodeColumn = column;
+ }
+ if (languageCodeColumn != -1 && countryCodeColumn != -1) {
+ break;
+ }
+ }
+
+ validateLanguageColumns(job, languageCodeColumn, countryCodeColumn);
+ return new CsvHeaderInfo(headers, languageCodeColumn, countryCodeColumn);
+ }
+
+ /**
+ * Performs validation of language columns for multilingual imports.
+ *
+ * @param job The current import job
+ * @param languageCodeColumn The index of the language code column
+ * @param countryCodeColumn The index of the country code column
+ * @throws JobValidationException if the required language columns are not found
+ */
+ private void validateLanguageColumns(Job job, int languageCodeColumn, int countryCodeColumn)
+ throws JobProcessingException {
+ if (languageCodeColumn == -1 || countryCodeColumn == -1) {
+ final var errorMessage = "languageCode and countryCode fields are mandatory in the CSV "
+ + "file when importing multilingual content.";
+ Logger.error(this, errorMessage);
+ throw new JobValidationException(job.id(), errorMessage);
+ }
+ }
+
+ /**
+ * Container class for CSV header information, particularly for handling language-related
+ * columns in multilingual imports.
+ */
+ private static class CsvHeaderInfo {
+
+ final String[] headers;
+ final int languageCodeColumn;
+ final int countryCodeColumn;
+
+ CsvHeaderInfo(String[] headers, int languageCodeColumn, int countryCodeColumn) {
+ this.headers = headers;
+ this.languageCodeColumn = languageCodeColumn;
+ this.countryCodeColumn = countryCodeColumn;
+ }
+ }
+
+}
diff --git a/dotCMS/src/main/java/com/dotmarketing/util/ImportUtil.java b/dotCMS/src/main/java/com/dotmarketing/util/ImportUtil.java
index cd3ea121d7c..9f9c41203d8 100644
--- a/dotCMS/src/main/java/com/dotmarketing/util/ImportUtil.java
+++ b/dotCMS/src/main/java/com/dotmarketing/util/ImportUtil.java
@@ -53,20 +53,27 @@
import com.liferay.portal.language.LanguageUtil;
import com.liferay.portal.model.User;
import com.liferay.util.StringPool;
-
import java.io.File;
-import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
import java.util.function.Function;
+import java.util.function.LongConsumer;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
-
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
@@ -109,6 +116,78 @@ public class ImportUtil {
private static final SimpleDateFormat DATE_FIELD_FORMAT = new SimpleDateFormat("yyyyMMdd");
+ /**
+ * Imports the data contained in a CSV file into dotCMS. The data can be
+ * either new or an update for existing content. The {@code preview}
+ * parameter determines the behavior of this method:
+ *
+ * - {@code preview == true}: This is the ideal approach. The data
+ * contained in the CSV file is previously analyzed and evaluated
+ * BEFORE actually committing any changes to existing contentlets or
+ * adding new ones. This way, users can perform the appropriate corrections
+ * (if needed) before submitting the new contents.
+ * - {@code preview == false}: Setting the parameter this way will make
+ * the system try to import the contents right away. The method will also
+ * return a summary with the status of the operation.
+ *
+ *
+ * @param importId
+ * - The ID of this data import.
+ * @param currentSiteId
+ * - The ID of the Site where the content will be added/updated.
+ * @param contentTypeInode
+ * - The Inode of the Content Type that the content is associated
+ * to.
+ * @param keyfields
+ * - The Inodes of the fields used to associated existing dotCMS
+ * contentlets with the information in this file. Can be empty.
+ * @param preview
+ * - Set to {@code true} if an analysis and evaluation of the
+ * imported data will be generated before actually
+ * importing the data. Otherwise, set to {@code false}.
+ * @param isMultilingual
+ * - If set to {@code true}, the CSV file will import contents in
+ * more than one language. Otherwise, set to {@code false}.
+ * @param user
+ * - The {@link User} performing this action.
+ * @param language
+ * - The language ID for the contents. If the ID equals -1, the
+ * columns for language code and country code will be used to
+ * infer the language ID.
+ * @param csvHeaders
+ * - The headers for each column in the CSV file.
+ * @param csvreader
+ * - The actual data contained in the CSV file.
+ * @param languageCodeHeaderColumn
+ * - The column name containing the language code.
+ * @param countryCodeHeaderColumn
+ * - The column name containing the country code.
+ * @param reader
+ * - The character streams reader.
+ * @param wfActionId
+ * - The workflow Action Id to execute on the import
+ * @param request
+ * - The request object.
+ * @return The resulting analysis performed on the CSV file. This provides
+ * information regarding inconsistencies, errors, warnings and/or
+ * precautions to the user.
+ * @throws DotRuntimeException
+ * An error occurred when analyzing the CSV file.
+ * @throws DotDataException
+ * An error occurred when analyzing the CSV file.
+ */
+ public static HashMap> importFile(
+ Long importId, String currentSiteId, String contentTypeInode, String[] keyfields,
+ boolean preview, boolean isMultilingual, User user, long language,
+ String[] csvHeaders, CsvReader csvreader, int languageCodeHeaderColumn,
+ int countryCodeHeaderColumn, Reader reader, String wfActionId,
+ final HttpServletRequest request) throws DotRuntimeException, DotDataException {
+
+ return importFile(importId, currentSiteId, contentTypeInode, keyfields, preview,
+ isMultilingual, user, language, csvHeaders, csvreader, languageCodeHeaderColumn,
+ countryCodeHeaderColumn, wfActionId, request, null);
+ }
+
/**
* Imports the data contained in a CSV file into dotCMS. The data can be
* either new or an update for existing content. The {@code preview}
@@ -155,10 +234,12 @@ public class ImportUtil {
* - The column name containing the language code.
* @param countryCodeHeaderColumn
* - The column name containing the country code.
- * @param reader
- * - The character streams reader.
* @param wfActionId
* - The workflow Action Id to execute on the import
+ * @param request
+ * - The request object.
+ * @param progressCallback
+ * - A callback function to report progress.
* @return The resulting analysis performed on the CSV file. This provides
* information regarding inconsistencies, errors, warnings and/or
* precautions to the user.
@@ -167,7 +248,11 @@ public class ImportUtil {
* @throws DotDataException
* An error occurred when analyzing the CSV file.
*/
- public static HashMap> importFile(Long importId, String currentSiteId, String contentTypeInode, String[] keyfields, boolean preview, boolean isMultilingual, User user, long language, String[] csvHeaders, CsvReader csvreader, int languageCodeHeaderColumn, int countryCodeHeaderColumn, Reader reader, String wfActionId, final HttpServletRequest request)
+ public static HashMap> importFile(Long importId, String currentSiteId,
+ String contentTypeInode, String[] keyfields, boolean preview, boolean isMultilingual,
+ User user, long language, String[] csvHeaders, CsvReader csvreader,
+ int languageCodeHeaderColumn, int countryCodeHeaderColumn, String wfActionId,
+ final HttpServletRequest request, final LongConsumer progressCallback)
throws DotRuntimeException, DotDataException {
HashMap> results = new HashMap<>();
@@ -293,6 +378,11 @@ public static HashMap> importFile(Long importId, String cur
errors++;
Logger.warn(ImportUtil.class, "Error line: " + lines + " (" + csvreader.getRawRecord()
+ "). Line Ignored.");
+ } finally {
+ // Progress callback
+ if (progressCallback != null) {
+ progressCallback.accept(lines);
+ }
}
}
@@ -331,14 +421,6 @@ public static HashMap> importFile(Long importId, String cur
} catch (final Exception e) {
Logger.error(ImportContentletsAction.class, String.format("An error occurred when parsing CSV file in " +
"line #%s: %s", lineNumber, e.getMessage()), e);
- } finally {
- if (reader != null) {
- try {
- reader.close();
- } catch (IOException e) {
- // Reader could not be closed. Continue
- }
- }
}
final String action = preview ? "Content preview" : "Content import";
String statusMsg = String.format("%s has finished, %d lines were read correctly.", action, lines);