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 9 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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.dotmarketing.util.Logger;

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

@ApplicationScoped
public class JobQueueManagerHelper {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved

private JobProcessorScanner scanner;
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved

@Inject
public JobQueueManagerHelper(JobProcessorScanner scanner) {
this.scanner = scanner;
}

public JobQueueManagerHelper() {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
}

public void registerProcessors(JobQueueManagerAPI jobQueueManagerAPI) {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
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;
}
registerProcessor(jobQueueManagerAPI, processor);
} catch (Exception e) {
Logger.error(this.getClass(), "Unable to register JobProcessor ", e);
}
});
}

/**
* Test if a processor can be instantiated
* @param processor The processor to tested
* @return true if the processor can be instantiated, false otherwise
*/
private boolean testInstantiation(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;
}

private void registerProcessor(JobQueueManagerAPI jobQueueManagerAPI, Class<? extends JobProcessor> processor) {
if (processor.isAnnotationPresent(Queue.class)) {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
Queue queue = processor.getAnnotation(Queue.class);
jobQueueManagerAPI.registerProcessor(queue.value(), processor);
} else {
jobQueueManagerAPI.registerProcessor(processor.getName(), processor);
}
}

public void shutdown(JobQueueManagerAPI jobQueueManagerAPI) {
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,63 @@
package com.dotcms.rest.api.v1.contentImport;

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 = "Content Type is required")
private final String contentType;

private final String language;

@NotNull(message = "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;
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
}

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,157 @@
package com.dotcms.rest.api.v1.contentImport;

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.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.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@ApplicationScoped
public class ContentImportHelper {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved

private static final String CMD_PREVIEW = "preview";
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
private static final String CMD_PUBLISH = "publish";

private JobQueueManagerAPI jobQueueManagerAPI;
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
private JobQueueManagerHelper jobQueueManagerHelper;

@Inject
public ContentImportHelper(JobQueueManagerAPI jobQueueManagerAPI, JobQueueManagerHelper jobQueueManagerHelper) {
this.jobQueueManagerAPI = jobQueueManagerAPI;
this.jobQueueManagerHelper = jobQueueManagerHelper;
}

public ContentImportHelper() {
valentinogiardino marked this conversation as resolved.
Show resolved Hide resolved
}

@PostConstruct
public void onInit() {
jobQueueManagerHelper.registerProcessors(jobQueueManagerAPI);
}

@PreDestroy
public void onDestroy() {
jobQueueManagerHelper.shutdown(jobQueueManagerAPI);
}

/**
* Creates a content import job with the provided parameters
*
* @param preview Whether this is a preview job
* @param queueName The name of the queue to submit the job to
* @param params The import parameters
* @param user The user initiating the import
* @param request The HTTP request
* @return The ID of the created job
*/
public String createJob(
final boolean preview,
final String queueName,
final ContentImportParams params,
final User user,
final HttpServletRequest request) throws DotDataException, JsonProcessingException {

params.checkValid();
params.getForm().checkValid();

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

return jobQueueManagerAPI.createJob(queueName, jobParameters);
}

/**
* Creates the job parameters map from the provided inputs
*/
private Map<String, Object> createJobParameters(
final boolean preview,
final com.dotcms.rest.api.v1.contentImport.ContentImportParams params,
final User user,
final HttpServletRequest request) throws JsonProcessingException, DotDataException {

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

// Add required parameters
jobParameters.put("cmd", preview ? CMD_PREVIEW : CMD_PUBLISH);
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 parameters map if they are present
*/
private void addOptionalParameters(
final com.dotcms.rest.api.v1.contentImport.ContentImportParams params,
final Map<String, Object> jobParameters) throws JsonProcessingException {

final com.dotcms.rest.api.v1.contentImport.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 current site information to the job parameters
*/
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 necessary parameters to the job
*/
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());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.dotcms.rest.api.v1.contentImport;

import com.dotcms.repackage.javax.validation.ValidationException;
import com.dotcms.repackage.javax.validation.constraints.NotNull;
import com.dotcms.rest.api.Validated;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.minidev.json.annotate.JsonIgnore;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;

import java.io.InputStream;

/**
* Bean class that encapsulates the multipart form parameters for content import operations.
*/
public class ContentImportParams extends Validated {

@NotNull(message = "The file is required.")
@FormDataParam("file")
private InputStream fileInputStream;

@JsonIgnore
@FormDataParam("file")
private FormDataContentDisposition contentDisposition;

@FormDataParam("form")
private ContentImportForm form;

@NotNull(message = "The form data is required.")
@FormDataParam("form")
private String jsonForm;

public InputStream getFileInputStream() {
return fileInputStream;
}

public void setFileInputStream(InputStream fileInputStream) {
this.fileInputStream = fileInputStream;
}

public FormDataContentDisposition getContentDisposition() {
return contentDisposition;
}

public void setContentDisposition(FormDataContentDisposition contentDisposition) {
this.contentDisposition = contentDisposition;
}

public String getJsonForm() {
return jsonForm;
}

public void setForm(ContentImportForm form) {
this.form = form;
}

/**
* Gets the parsed form object, lazily parsing the JSON if needed
* @return The ContentImportForm object
*/
public ContentImportForm getForm() throws JsonProcessingException {
if (null == form && (null != jsonForm && !jsonForm.isEmpty())) {
form = new ObjectMapper().readValue(jsonForm, com.dotcms.rest.api.v1.contentImport.ContentImportForm.class);
}
return form;
}

@Override
public String toString() {
return "ContentImportParams{" +
"form=" + form +
", hasFile=" + (fileInputStream != null) +
", fileName=" + (contentDisposition != null ? contentDisposition.getFileName() : "null") +
'}';
}

@Override
public void checkValid() {
super.checkValid();
if (contentDisposition == null || contentDisposition.getFileName() == null) {
throw new ValidationException("The file must have a valid file name.");
}
}
}
Loading