All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.xlrit.gears.engine.form.GearsFormEngineImpl Maven / Gradle / Ivy

package com.xlrit.gears.engine.form;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

import com.xlrit.gears.base.content.ContentStore;
import com.xlrit.gears.base.execution.Execution;
import com.xlrit.gears.base.execution.ExecutionFactory;
import com.xlrit.gears.base.choice.Choices;
import com.xlrit.gears.base.form.Form;
import com.xlrit.gears.base.meta.Attributes;
import com.xlrit.gears.base.meta.Input;
import com.xlrit.gears.base.meta.Multiple;
import com.xlrit.gears.engine.meta.MetaManager;
import com.xlrit.gears.engine.meta.PrintOptions;
import com.xlrit.gears.engine.meta.Property;
import com.xlrit.gears.engine.meta.TypeInfo;
import com.xlrit.gears.engine.util.EngineUtils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import lombok.RequiredArgsConstructor;
import org.flowable.engine.form.FormData;
import org.flowable.engine.form.StartFormData;
import org.flowable.engine.form.TaskFormData;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import static com.google.common.base.Preconditions.checkNotNull;

@Component
@RequiredArgsConstructor
public class GearsFormEngineImpl implements GearsFormEngine {
	private static final Logger LOG = LoggerFactory.getLogger(GearsFormEngineImpl.class);
	private static final Map NO_VARS = Collections.emptyMap();

	private final MetaManager      metaManager;
	private final CompanionManager companionManager;
	private final ExecutionFactory executionFactory;
	private final ContentStore     contentStore;
	private final ObjectMapper     objectMapper;

	@Override
	public ExecutionFactory getExecutionFactory() {
		return executionFactory;
	}

	@Override
	public ObjectNode renderStartForm(StartFormData startForm) {
		Execution execution = createStartExecution(startForm.getProcessDefinition());
		return renderForm(startForm, RenderOptions.DEFAULT, execution, null);
	}

	@Override
	public ObjectNode renderTaskForm(TaskFormData taskForm) {
		TaskEntity taskEntity = (TaskEntity) taskForm.getTask();
		Execution execution = createTaskExecution(taskEntity);
		return renderForm(taskForm, RenderOptions.DEFAULT, execution, taskEntity.getVariablesLocal());
	}

	@Override
	public Execution createStartExecution(ProcessDefinition processDefinition) {
		Objects.requireNonNull(processDefinition, "processDefinition is required");
		Execution execution = executionFactory.basic();

		String actorName = EngineUtils.getStarterActorName((ProcessDefinitionEntity) processDefinition);
		EngineUtils.initializeProcessStarter(execution, actorName);

		return execution;
	}

	@Override
	public Execution createTaskExecution(TaskEntity taskEntity) {
		Objects.requireNonNull(taskEntity, "task is required");
		ExecutionEntity executionEntity = CommandContextUtil.getExecutionEntityManager().findById(taskEntity.getExecutionId());
		String loggerName = "process." + executionEntity.getProcessDefinitionKey() + "." + taskEntity.getName();
		return executionFactory.wrap(executionEntity, loggerName);
	}

	@Override
	public ObjectNode renderForm(FormData formData, RenderOptions options, Execution execution, Map properties) {
		checkNotNull(formData, "formData required");
		checkNotNull(execution, "execution required");

		return renderForm(getFormInfo(formData.getFormKey()), options, execution, properties);
	}

	@Override
	public Form submitForm(String deploymentId, String formKey, Execution execution, Map properties) {
		checkNotNull(formKey, "formKey required");
		checkNotNull(execution, "execution required");

		return submitForm(getFormInfo(formKey), execution, properties);
	}

	// === rendering === //

	ObjectNode renderForm(FormInfo formInfo, RenderOptions renderOptions, Execution execution, Map vars) {
		JsonNode savedValues = getFormValuesNode(vars);
		LOG.debug("renderForm: formInfo={}, savedValues={}", formInfo, savedValues);

		RenderContext ctx = new RenderContext(objectMapper, metaManager, renderOptions, PrintOptions.DEFAULT);
		Object basedOnValue = formInfo.getBasedOnValue(execution);

		ObjectNode formNode = objectMapper.createObjectNode();
		formNode.set("fields", ctx.renderView(formInfo.getFields(), execution));
		formNode.set("values", ctx.renderModel(formInfo.getFields(), execution, basedOnValue));
		formNode.set("saved",  savedValues);
		formNode.set("aux",    ctx.renderAux(formInfo.getFields(), execution));
		return formNode;
	}

	@Override
	public Choices getChoices(String formKey, Execution initialExecution, String path) {
		return getChoices(getFormInfo(formKey), initialExecution, path);
	}

	@VisibleForTesting
	public Choices getChoices(FormInfo formInfo, Execution initialExecution, String path) {
		LOG.debug("getChoices: formInfo={}, path={}", formInfo, path);
		return formInfo.evaluate(path, initialExecution, (execution, field) -> ((InputFormField) field).getChoices(execution));
	}

	// === submission === //

	Form submitForm(FormInfo formInfo, Execution execution, Map properties) {
		SubmitContext ctx = createSubmitContext(properties);
		Form form = formInfo.convert(ctx, ctx.getFormValues());
		execution.setForm(form);
		Validation validation = formInfo.validate(ctx, execution, form);
		if (validation.isFailure()) {
			throw validation.toInputException();
		}
		return form;
	}

	// === (de)serialization === //

	public byte[] serializeForm(Form form) {
		try {
			FormInfo formInfo = getFormInfo(form.getClass());
			JsonNode json = serialize(formInfo, form);
			return objectMapper.writer().writeValueAsBytes(json);
		}
		catch (JsonProcessingException e) {
			throw new RuntimeException("Unable to serialize form " + form, e);
		}
	}

	@VisibleForTesting
	JsonNode serialize(FormInfo formInfo, Form form) {
		return formInfo.serialize(form, objectMapper, PrintOptions.NONE);
	}

	public Form deserializeForm(String formKey, byte[] bytes) {
		return deserializeForm(getFormClass(formKey), bytes);
	}

	@VisibleForTesting
	 T deserializeForm(Class formClass, byte[] bytes) {
		try {
			FormInfo formInfo = getFormInfo(formClass);
			JsonNode formNode = objectMapper.reader().readTree(bytes);
			Form form = deserialize(formInfo, formNode);
			return formClass.cast(form);
		}
		catch (IOException e) {
			throw new RuntimeException("Unable to deserialize form " + formClass, e);
		}
	}

	@VisibleForTesting
	Form deserialize(FormInfo formInfo, JsonNode formNode) {
		return (Form) formInfo.deserialize(formNode, objectMapper);
	}

	// === form info === //

	private final ConcurrentMap formInfos = new ConcurrentHashMap<>(16);

	@Override
	public FormInfo getFormInfo(String formKey) { return getFormInfo(getFormClass(formKey)); }

	protected  FormInfo getFormInfo(Class formClass) {
		return formInfos.computeIfAbsent(formClass, this::createFormInfo);
	}

	private  FormInfo createFormInfo(Class formClass) {
		CompanionAdapter ca = companionManager.getAdapter(formClass);
		ValueRef basedOnRef = ca.getBasedOnRef();
		return new FormInfo(formClass, createFieldInfos(formClass, ca, FormPath.ROOT), basedOnRef);
	}

	private List createFieldInfos(Class formClass, CompanionAdapter ca, FormPath parentPath) {
		Attributes attributesAnno = formClass.getAnnotation(Attributes.class);
		checkNotNull(attributesAnno, "%s must have an @Attributes annotation", formClass);
		String[] fieldNames = attributesAnno.value();
		return Arrays.stream(fieldNames)
			.map(name -> createFieldInfo(formClass, ca, parentPath, name))
			.collect(Collectors.toList());
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	FormField createFieldInfo(Class formClass, CompanionAdapter ca, FormPath parentPath, String name) {
		Property property = Property.byName(formClass, name);

		Input inputAnno = property.getAnnotation(Input.class);
		if (inputAnno != null) {
			TypeInfo typeInfo = metaManager.genericTypeToInfo(property.getGenericType());
			FormPath path = parentPath.input(name);
			ValueRef defaultRef = ca.getDefaultRef(path);
			ValueRef chosenFromRef = ca.getChoicesRef(path);
			return new InputFormField(path, typeInfo, property, defaultRef, chosenFromRef);
		}

		Multiple multipleAnno = property.getAnnotation(Multiple.class);
		if (multipleAnno != null) {
			FormPath path = parentPath.multiple(name);
			ValueRef basedOnRef = ca.getBasedOnRef(path);
			ValueRef collectionRef = ca.getCollectionRef(path);
			Class elementType = property.getElementType();
			LineInfo lineInfo = new LineInfo(elementType, createFieldInfos(elementType, ca, path.line(multipleAnno.elementName())));
			return new MultipleFormField(path, property, basedOnRef, collectionRef, lineInfo);
		}

		// fall through: neither @Input nor @Multiple is present
		throw new RuntimeException("Getter for '" + name + "' in " + formClass + " must be annotated with either @Input or @Multiple");
	}

	// === ugly stuff === //

	@SuppressWarnings("unchecked")
	private SubmitContext createSubmitContext(Map properties) {
		if (properties == null) properties = NO_VARS;
		JsonNode formValues = (JsonNode) properties.get("values");
		if (formValues == null) formValues = objectMapper.createObjectNode();
		List files = (List) properties.get("files");
		return new SubmitContext(objectMapper, contentStore, formValues, files);
	}

	private JsonNode getFormValuesNode(Map properties) {
		if (properties == null) properties = NO_VARS;
		JsonNode result = (JsonNode) properties.get("values");
		return result != null ? result : objectMapper.createObjectNode();
	}

	@SuppressWarnings("unchecked")
	private Class getFormClass(String formKey) {
		try {
			return (Class) Thread.currentThread().getContextClassLoader().loadClass(formKey);
		}
		catch (Exception e) {
			throw new RuntimeException("Unable to get form class with key '" + formKey + "'", e);
		}
	}
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy