Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(JobQueue) Implement import content job rest endpoint #30669 #30698

Merged
merged 38 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f14fa05
#30669 add ContentImportForm
valentinogiardino Nov 19, 2024
3f04f62
#30669 add contentImport params
valentinogiardino Nov 19, 2024
3631fd4
#30669 add contentimport helper
valentinogiardino Nov 19, 2024
a33f47c
#30669 add contentImport resource
valentinogiardino Nov 19, 2024
e3d982b
Merge branch 'main' into issue-30669-create-import-job-rest-endpoint
valentinogiardino Nov 19, 2024
40311d9
#30669 add params validation
valentinogiardino Nov 19, 2024
c6e620f
#30669 fix extract jobQueueManagerAPI init and destroy
valentinogiardino Nov 19, 2024
ea4fa2e
Update JobQueueHelper.java
valentinogiardino Nov 19, 2024
aeb5c8e
Merge branch 'main' into issue-30669-create-import-job-rest-endpoint
valentinogiardino Nov 20, 2024
f6814a2
#30669 add ContentUploadResourceIntegrationTest
valentinogiardino Nov 21, 2024
9c82d66
#30669 remove boolean param
valentinogiardino Nov 21, 2024
b01f970
Merge branch 'main' into issue-30669-create-import-job-rest-endpoint
valentinogiardino Nov 21, 2024
b7c9c9d
#30669 add response to initBuilder
valentinogiardino Nov 21, 2024
8880be6
Merge branch 'issue-30669-create-import-job-rest-endpoint' of https:/…
valentinogiardino Nov 21, 2024
126f0ea
#30669 update JobQueueManagerHelper
valentinogiardino Nov 21, 2024
5db3bd8
#30669 add catch for JobValidationException
valentinogiardino Nov 21, 2024
dc3ad8c
#30669 move checkValid call
valentinogiardino Nov 21, 2024
186a28b
#30669 update integreation tests
valentinogiardino Nov 21, 2024
7a076c1
#30669 add javadoc to JobQueueManagerHelper
valentinogiardino Nov 21, 2024
6f4deeb
#30669 refactor JobQueueManagerHelper
valentinogiardino Nov 21, 2024
ff3d04b
#30669 add doc for ContentImportResource
valentinogiardino Nov 21, 2024
c59048a
#30669 update doc
valentinogiardino Nov 22, 2024
b4da6c6
#30669 fix toString
valentinogiardino Nov 22, 2024
be133fe
#30669 add ContentImportHelper doc
valentinogiardino Nov 22, 2024
68942f0
#30669 add log
valentinogiardino Nov 22, 2024
5df1df3
#30669 use constants
valentinogiardino Nov 22, 2024
fb10066
#30669 rename package
valentinogiardino Nov 22, 2024
c6d72ba
#30669 add postman tests
valentinogiardino Nov 22, 2024
33b256e
Merge branch 'main' into issue-30669-create-import-job-rest-endpoint
valentinogiardino Nov 22, 2024
78525cd
#30669 set to final
valentinogiardino Nov 22, 2024
b15005b
#30669 fix postman tests
valentinogiardino Nov 22, 2024
6cd497d
#30669 fix unnecessary defining methods as static
valentinogiardino Nov 22, 2024
adc213a
#30669 fix add new integration test class to suit
valentinogiardino Nov 22, 2024
3ccb323
#30669 fix it
valentinogiardino Nov 22, 2024
9563b64
typo
valentinogiardino Nov 22, 2024
9fc6270
#30669 fix it
valentinogiardino Nov 23, 2024
b01e94d
#30669 add cleanUp
valentinogiardino Nov 23, 2024
1a4e1c1
Merge branch 'main' into issue-30669-create-import-job-rest-endpoint
valentinogiardino Nov 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions dotCMS/src/main/java/com/dotcms/rest/api/v1/JobQueueManagerHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.dotcms.rest.api.v1;

import com.dotcms.jobs.business.api.JobProcessorScanner;
import com.dotcms.jobs.business.api.JobQueueManagerAPI;
import com.dotcms.jobs.business.processor.JobProcessor;
import com.dotcms.jobs.business.processor.Queue;
import com.dotcms.util.AnnotationUtils;
import com.dotmarketing.util.Logger;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Objects;

/**
* Helper class for managing job queue processors in the JobQueueManagerAPI.
* <p>
* This class is responsible for discovering job processors, registering them with
* the JobQueueManagerAPI, and shutting down the JobQueueManagerAPI when needed.
*/
@ApplicationScoped
public class JobQueueManagerHelper {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved

private final JobQueueManagerAPI jobQueueManagerAPI;
private final JobProcessorScanner scanner;

/**
* Constructor that injects the {@link JobProcessorScanner} and {@link JobQueueManagerAPI}.
*
* @param scanner The JobProcessorScanner to discover job processors
* @param jobQueueManagerAPI The JobQueueManagerAPI instance to register processors with
*/
@Inject
public JobQueueManagerHelper(final JobProcessorScanner scanner, final JobQueueManagerAPI jobQueueManagerAPI) {
this.scanner = scanner;
this.jobQueueManagerAPI = jobQueueManagerAPI;
}

/**
* Default constructor required by CDI.
*/
public JobQueueManagerHelper() {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
this.scanner = null;
this.jobQueueManagerAPI = null;
}

/**
* Registers all discovered job processors with the JobQueueManagerAPI.
* If the JobQueueManagerAPI is not started, it starts the API before registering the processors.
*/
public void registerProcessors() {
if (!jobQueueManagerAPI.isStarted()) {
jobQueueManagerAPI.start();
Logger.info(this.getClass(), "JobQueueManagerAPI started");
}

List<Class<? extends JobProcessor>> processors = scanner.discoverJobProcessors();
processors.forEach(processor -> {
try {
if (!testInstantiation(processor)) {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
return;
}
Logger.info(this.getClass(), "Registering JobProcessor: " + processor.getName());
registerProcessor(processor);
} catch (Exception e) {
Logger.error(this.getClass(), "Unable to register JobProcessor ", e);
}
});
}

/**
* Tests whether a given job processor can be instantiated by attempting to
* create an instance of the processor using its default constructor.
*
* @param processor The processor class to test for instantiation
* @return true if the processor can be instantiated, false otherwise
*/
private boolean testInstantiation(final Class<? extends JobProcessor> processor) {
try {
Constructor<? extends JobProcessor> declaredConstructor = processor.getDeclaredConstructor();
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
declaredConstructor.newInstance();
return true;
} catch (Exception e) {
Logger.error(this.getClass(), String.format(" JobProcessor [%s] cannot be instantiated and will be ignored.", processor.getName()), e);
}
return false;
}

/**
* Registers a job processor with the JobQueueManagerAPI using the queue name specified
* in the {@link Queue} annotation, if present. If no annotation is found, the processor's
* class name is used as the queue name.
*
* @param processor the processor class to register
*/
private void registerProcessor(final Class<? extends JobProcessor> processor) {
Queue queue = AnnotationUtils.getBeanAnnotation(processor, Queue.class);
if (Objects.nonNull(queue)) {
jobQueueManagerAPI.registerProcessor(queue.value(), processor);
} else {
jobQueueManagerAPI.registerProcessor(processor.getName(), processor);
}
}

/**
* Shuts down the JobQueueManagerAPI if it is currently started.
* If the JobQueueManagerAPI is started, it attempts to close it gracefully.
* In case of an error during the shutdown process, the error is logged.
*/
public void shutdown() {
if (jobQueueManagerAPI.isStarted()) {
try {
jobQueueManagerAPI.close();
Logger.info(this.getClass(), "JobQueueManagerAPI successfully closed");
} catch (Exception e) {
Logger.error(this.getClass(), e.getMessage(), e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.dotcms.rest.api.v1.content._import;

import com.dotcms.repackage.javax.validation.constraints.NotNull;
import com.dotcms.rest.api.Validated;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;

/**
* Form object that represents the JSON parameters for content import operations.
*/
public class ContentImportForm extends Validated {

@NotNull(message = "A Content Type id or variable is required")
private final String contentType;

private final String language;

@NotNull(message = "A Workflow Action id is required")
private final String workflowActionId;

private final List<String> fields;

@JsonCreator
public ContentImportForm(
@JsonProperty("contentType") final String contentType,
@JsonProperty("language") final String language,
@JsonProperty("workflowActionId") final String workflowActionId,
@JsonProperty("fields") final List<String> fields) {
super();
this.contentType = contentType;
this.language = language;
this.workflowActionId = workflowActionId;
this.fields = fields;
this.checkValid();
}

public String getContentType() {
return contentType;
}

public String getLanguage() {
return language;
}

public String getWorkflowActionId() {
return workflowActionId;
}

public List<String> getFields() {
return fields;
}

@Override
public String toString() {
return "ContentImportForm{" +
"contentType='" + contentType + '\'' +
", language='" + language + '\'' +
", workflowActionId='" + workflowActionId + '\'' +
", fields=" + fields +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package com.dotcms.rest.api.v1.content._import;

import com.dotcms.jobs.business.api.JobQueueManagerAPI;
import com.dotcms.rest.api.v1.JobQueueManagerHelper;
import com.dotcms.rest.api.v1.temp.DotTempFile;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.web.WebAPILocator;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
import com.dotmarketing.util.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.liferay.portal.model.User;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
* Helper class for managing content import operations in the dotCMS application.
* <p>
* This class provides methods to create and manage jobs for importing content
* from external sources, such as CSV files, into the system. It handles the
* validation of import parameters, processes file uploads, and constructs
* the necessary job parameters to enqueue content import tasks in the job queue.
*/
@ApplicationScoped
public class ContentImportHelper {

private final JobQueueManagerAPI jobQueueManagerAPI;
private final JobQueueManagerHelper jobQueueManagerHelper;

/**
* Constructor for dependency injection.
*
* @param jobQueueManagerAPI The API for managing job queues.
* @param jobQueueManagerHelper Helper for job queue management.
*/
@Inject
public ContentImportHelper(final JobQueueManagerAPI jobQueueManagerAPI, final JobQueueManagerHelper jobQueueManagerHelper) {
this.jobQueueManagerAPI = jobQueueManagerAPI;
this.jobQueueManagerHelper = jobQueueManagerHelper;
}

/**
* Default constructor required for CDI.
*/
public ContentImportHelper() {
this.jobQueueManagerAPI = null;
this.jobQueueManagerHelper = null;
}

/**
* Initializes the helper by registering job processors during application startup.
*/
@PostConstruct
public void onInit() {
jobQueueManagerHelper.registerProcessors();
}

/**
* Cleans up resources and shuts down the helper during application shutdown.
*/
@PreDestroy
public void onDestroy() {
jobQueueManagerHelper.shutdown();
}

/**
* Creates a content import job with the provided parameters and submits it to the job queue.
*
* @param command The command indicating the type of operation (e.g., "preview" or "import").
* @param queueName The name of the queue to which the job should be submitted.
* @param params The content import parameters containing the details of the import operation.
* @param user The user initiating the import.
* @param request The HTTP request associated with the import operation.
* @return The ID of the created job.
* @throws DotDataException If there is an error creating the job.
* @throws JsonProcessingException If there is an error processing JSON data.
*/
public String createJob(
final String command,
final String queueName,
final ContentImportParams params,
final User user,
final HttpServletRequest request) throws DotDataException, JsonProcessingException {

params.checkValid();
final Map<String, Object> jobParameters = createJobParameters(command, params, user, request);
processFileUpload(params, jobParameters, request);

return jobQueueManagerAPI.createJob(queueName, jobParameters);
}

/**
* Constructs a map of job parameters based on the provided inputs.
*
* @param command The command indicating the type of operation.
* @param params The content import parameters.
* @param user The user initiating the import.
* @param request The HTTP request associated with the operation.
* @return A map containing the job parameters.
* @throws JsonProcessingException If there is an error processing JSON data.
*/
private Map<String, Object> createJobParameters(
final String command,
final ContentImportParams params,
final User user,
final HttpServletRequest request) throws JsonProcessingException {

final Map<String, Object> jobParameters = new HashMap<>();

// Add required parameters
jobParameters.put("cmd", command);
jobParameters.put("userId", user.getUserId());
jobParameters.put("contentType", params.getForm().getContentType());
jobParameters.put("workflowActionId", params.getForm().getWorkflowActionId());

// Add optional parameters
addOptionalParameters(params, jobParameters);

// Add site information
addSiteInformation(request, jobParameters);

return jobParameters;
}

/**
* Adds optional parameters to the job parameter map if they are present in the form.
*
* @param params The content import parameters.
* @param jobParameters The map of job parameters to which optional parameters are added.
* @throws JsonProcessingException If there is an error processing JSON data.
*/
private void addOptionalParameters(
final ContentImportParams params,
final Map<String, Object> jobParameters) throws JsonProcessingException {

final ContentImportForm form = params.getForm();

if (form.getLanguage() != null && !form.getLanguage().isEmpty()) {
jobParameters.put("language", form.getLanguage());
}
if (form.getFields() != null && !form.getFields().isEmpty()) {
jobParameters.put("fields", form.getFields());
}
}

/**
* Adds the current site information to the job parameters.
*
* @param request The HTTP request associated with the operation.
* @param jobParameters The map of job parameters to which site information is added.
*/
private void addSiteInformation(
final HttpServletRequest request,
final Map<String, Object> jobParameters){

final var currentHost = WebAPILocator.getHostWebAPI().getCurrentHostNoThrow(request);
jobParameters.put("siteName", currentHost.getHostname());
jobParameters.put("siteIdentifier", currentHost.getIdentifier());
}

/**
* Processes the file upload and adds the file-related parameters to the job.
*
* @param params The content import parameters.
* @param jobParameters The map of job parameters.
* @param request The HTTP request containing the uploaded file.
* @throws DotDataException If there is an error processing the file upload.
*/
private void processFileUpload(
final ContentImportParams params,
final Map<String, Object> jobParameters,
final HttpServletRequest request) throws DotDataException {

try {
final DotTempFile tempFile = APILocator.getTempFileAPI().createTempFile(
params.getContentDisposition().getFileName(),
request,
params.getFileInputStream()
);
jobParameters.put("tempFileId", tempFile.id);
jobParameters.put("requestFingerPrint", APILocator.getTempFileAPI().getRequestFingerprint(request));
} catch (DotSecurityException e) {
Logger.error(this, "Error handling file upload", e);
throw new DotDataException("Error processing file upload: " + e.getMessage());
}
}
}
Loading
Loading