Skip to content

Commit

Permalink
feat(JobQueue) Implement import content job rest endpoint #30669 (#30698
Browse files Browse the repository at this point in the history
)

### Proposed Changes  
* **Added a new REST resource (`ContentImportResource`)** to handle
content import job operations, including creating and enqueuing content
import jobs.
* **Added a helper class (`ContentImportHelper`)** to manage content
import operations, including methods for creating and managing jobs.
* **Created a bean class (`ContentImportParams`)** to encapsulate
multipart form parameters for content import operations.
* **Introduced a form object (`ContentImportForm`)** to represent JSON
parameters for content import operations.

### Checklist  
- [x] Tests  
- [x] Translations  
- [x] Security Implications Contemplated (add notes if applicable)  

### Additional Info  
These changes aim to provide a REST endpoint for managing content import
jobs, enabling operations like job creation and queueing.
  • Loading branch information
valentinogiardino authored Nov 25, 2024
1 parent 4f46e9e commit f4195a7
Show file tree
Hide file tree
Showing 12 changed files with 1,796 additions and 66 deletions.
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 {

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() {
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)) {
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();
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

0 comments on commit f4195a7

Please sign in to comment.